mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 13:24:54 +01:00
Merge branch 'develop' into feature/1180-maya-support-for-redshift-attributes-in-looks
This commit is contained in:
commit
bcf2e9b6db
507 changed files with 28437 additions and 5102 deletions
63
.github/workflows/documentation.yml
vendored
Normal file
63
.github/workflows/documentation.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
name: documentation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
types: [review_requested, ready_for_review]
|
||||
paths:
|
||||
- 'website/**'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'website/**'
|
||||
|
||||
jobs:
|
||||
check-build:
|
||||
if: github.event_name != 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
- name: Test Build
|
||||
run: |
|
||||
cd website
|
||||
if [ -e yarn.lock ]; then
|
||||
yarn install --frozen-lockfile
|
||||
elif [ -e package-lock.json ]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
npm run build
|
||||
deploy-website:
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 🚚 Get latest code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: 🔨 Build
|
||||
run: |
|
||||
cd website
|
||||
if [ -e yarn.lock ]; then
|
||||
yarn install --frozen-lockfile
|
||||
elif [ -e package-lock.json ]; then
|
||||
npm ci
|
||||
else
|
||||
npm i
|
||||
fi
|
||||
npm run build
|
||||
|
||||
- name: 📂 Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@4.0.0
|
||||
with:
|
||||
server: ftp.openpype.io
|
||||
username: ${{ secrets.ftp_user }}
|
||||
password: ${{ secrets.ftp_password }}
|
||||
local-dir: ./website/build/
|
||||
88
.github/workflows/test_build.yml
vendored
Normal file
88
.github/workflows/test_build.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# This workflow will upload a Python Package using Twine when a release is created
|
||||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries
|
||||
|
||||
name: Test Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
types: [review_requested, ready_for_review]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'website/**'
|
||||
- 'vendor/**'
|
||||
|
||||
jobs:
|
||||
Windows-latest:
|
||||
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🧵 Install Requirements
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tools/create_env.ps1
|
||||
|
||||
- name: 🔨 Build
|
||||
shell: pwsh
|
||||
run: |
|
||||
./tools/build.ps1
|
||||
|
||||
Ubuntu-latest:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.7]
|
||||
|
||||
steps:
|
||||
- name: 🚛 Checkout Code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: 🧵 Install Requirements
|
||||
run: |
|
||||
./tools/create_env.sh
|
||||
|
||||
- name: 🔨 Build
|
||||
run: |
|
||||
./tools/build.sh
|
||||
|
||||
# MacOS-latest:
|
||||
|
||||
# runs-on: macos-latest
|
||||
# strategy:
|
||||
# matrix:
|
||||
# python-version: [3.7]
|
||||
|
||||
# steps:
|
||||
# - name: 🚛 Checkout Code
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v2
|
||||
# with:
|
||||
# python-version: ${{ matrix.python-version }}
|
||||
|
||||
# - name: 🧵 Install Requirements
|
||||
# run: |
|
||||
# ./tools/create_env.sh
|
||||
|
||||
# - name: 🔨 Build
|
||||
# run: |
|
||||
# ./tools/build.sh
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
|
|
@ -67,7 +67,7 @@ coverage.xml
|
|||
|
||||
# Node JS packages
|
||||
##################
|
||||
node_modules/
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
openpype/premiere/ppro/js/debug.log
|
||||
|
|
@ -81,4 +81,15 @@ openpype/premiere/ppro/js/debug.log
|
|||
.vscode/
|
||||
.env
|
||||
dump.sql
|
||||
test_localsystem.txt
|
||||
test_localsystem.txt
|
||||
|
||||
# website
|
||||
##########
|
||||
website/translated_docs
|
||||
website/build/
|
||||
website/node_modules
|
||||
website/i18n/*
|
||||
|
||||
website/debug.log
|
||||
|
||||
website/.docusaurus
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
|
|
@ -1,13 +1,13 @@
|
|||
[submodule "repos/avalon-core"]
|
||||
path = repos/avalon-core
|
||||
url = git@github.com:pypeclub/avalon-core.git
|
||||
url = https://github.com/pypeclub/avalon-core.git
|
||||
branch = develop
|
||||
[submodule "repos/avalon-unreal-integration"]
|
||||
path = repos/avalon-unreal-integration
|
||||
url = git@github.com:pypeclub/avalon-unreal-integration.git
|
||||
url = https://github.com/pypeclub/avalon-unreal-integration.git
|
||||
[submodule "openpype/modules/ftrack/python2_vendor/ftrack-python-api"]
|
||||
path = openpype/modules/ftrack/python2_vendor/ftrack-python-api
|
||||
url = https://bitbucket.org/ftrack/ftrack-python-api.git
|
||||
[submodule "openpype/modules/ftrack/python2_vendor/arrow"]
|
||||
path = openpype/modules/ftrack/python2_vendor/arrow
|
||||
url = git@github.com:arrow-py/arrow.git
|
||||
url = https://github.com/arrow-py/arrow.git
|
||||
266
CHANGELOG.md
266
CHANGELOG.md
|
|
@ -1,18 +1,264 @@
|
|||
# Changelog
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-24)
|
||||
## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: comp renders mix up [\#1301](https://github.com/pypeclub/pype/pull/1301)
|
||||
- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297)
|
||||
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312)
|
||||
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303)
|
||||
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282)
|
||||
- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242)
|
||||
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226)
|
||||
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217)
|
||||
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214)
|
||||
- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204)
|
||||
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202)
|
||||
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199)
|
||||
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243)
|
||||
- Error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206)
|
||||
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194)
|
||||
|
||||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167)
|
||||
- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156)
|
||||
- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152)
|
||||
- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146)
|
||||
- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142)
|
||||
- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138)
|
||||
- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127)
|
||||
- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124)
|
||||
- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117)
|
||||
- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106)
|
||||
- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101)
|
||||
- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093)
|
||||
- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088)
|
||||
- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080)
|
||||
- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078)
|
||||
- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072)
|
||||
- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008)
|
||||
- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073)
|
||||
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174)
|
||||
- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166)
|
||||
- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163)
|
||||
- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125)
|
||||
- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090)
|
||||
- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082)
|
||||
- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067)
|
||||
- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064)
|
||||
|
||||
## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053)
|
||||
- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085)
|
||||
- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059)
|
||||
- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057)
|
||||
- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046)
|
||||
|
||||
## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040)
|
||||
- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036)
|
||||
- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031)
|
||||
|
||||
## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011)
|
||||
- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006)
|
||||
- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922)
|
||||
|
||||
## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932)
|
||||
- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926)
|
||||
- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894)
|
||||
- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993)
|
||||
- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986)
|
||||
- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981)
|
||||
- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965)
|
||||
- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944)
|
||||
- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941)
|
||||
- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938)
|
||||
- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903)
|
||||
- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893)
|
||||
- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891)
|
||||
- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884)
|
||||
- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881)
|
||||
- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840)
|
||||
- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824)
|
||||
- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795)
|
||||
- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995)
|
||||
- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982)
|
||||
- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960)
|
||||
- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839)
|
||||
- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990)
|
||||
- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988)
|
||||
- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984)
|
||||
- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979)
|
||||
- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972)
|
||||
- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971)
|
||||
- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967)
|
||||
- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964)
|
||||
- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959)
|
||||
- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956)
|
||||
- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954)
|
||||
- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953)
|
||||
- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933)
|
||||
- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920)
|
||||
- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909)
|
||||
- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896)
|
||||
- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889)
|
||||
- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831)
|
||||
- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934)
|
||||
|
||||
## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892)
|
||||
- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869)
|
||||
|
||||
## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866)
|
||||
|
||||
## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837)
|
||||
|
||||
## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809)
|
||||
- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807)
|
||||
- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823)
|
||||
|
||||
## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768)
|
||||
- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766)
|
||||
- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781)
|
||||
- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769)
|
||||
|
||||
## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770)
|
||||
- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760)
|
||||
- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754)
|
||||
- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752)
|
||||
- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748)
|
||||
- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744)
|
||||
- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742)
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736)
|
||||
- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721)
|
||||
- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719)
|
||||
- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716)
|
||||
- 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699)
|
||||
- TV Paint: initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693)
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678)
|
||||
- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675)
|
||||
- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674)
|
||||
|
|
@ -21,25 +267,33 @@
|
|||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740)
|
||||
- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722)
|
||||
- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682)
|
||||
- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717)
|
||||
- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Application manager [\#728](https://github.com/pypeclub/pype/pull/728)
|
||||
- Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706)
|
||||
- Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700)
|
||||
- 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697)
|
||||
|
||||
## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)
|
||||
|
||||
**Merged pull requests:**
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix\(SP\): getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
|
||||
# Changelog
|
||||
|
||||
|
|
|
|||
268
HISTORY.md
268
HISTORY.md
|
|
@ -1,3 +1,268 @@
|
|||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167)
|
||||
- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156)
|
||||
- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152)
|
||||
- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146)
|
||||
- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142)
|
||||
- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138)
|
||||
- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127)
|
||||
- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124)
|
||||
- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117)
|
||||
- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106)
|
||||
- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101)
|
||||
- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093)
|
||||
- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088)
|
||||
- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080)
|
||||
- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078)
|
||||
- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072)
|
||||
- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008)
|
||||
- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073)
|
||||
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174)
|
||||
- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166)
|
||||
- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163)
|
||||
- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125)
|
||||
- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090)
|
||||
- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082)
|
||||
- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067)
|
||||
- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064)
|
||||
|
||||
## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053)
|
||||
- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085)
|
||||
- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059)
|
||||
- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057)
|
||||
- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046)
|
||||
|
||||
## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040)
|
||||
- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036)
|
||||
- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031)
|
||||
|
||||
## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011)
|
||||
- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006)
|
||||
- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922)
|
||||
|
||||
## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932)
|
||||
- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926)
|
||||
- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894)
|
||||
- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993)
|
||||
- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986)
|
||||
- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981)
|
||||
- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965)
|
||||
- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944)
|
||||
- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941)
|
||||
- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938)
|
||||
- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903)
|
||||
- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893)
|
||||
- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891)
|
||||
- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884)
|
||||
- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881)
|
||||
- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840)
|
||||
- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824)
|
||||
- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795)
|
||||
- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995)
|
||||
- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982)
|
||||
- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960)
|
||||
- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839)
|
||||
- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990)
|
||||
- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988)
|
||||
- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984)
|
||||
- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979)
|
||||
- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972)
|
||||
- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971)
|
||||
- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967)
|
||||
- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964)
|
||||
- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959)
|
||||
- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956)
|
||||
- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954)
|
||||
- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953)
|
||||
- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933)
|
||||
- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920)
|
||||
- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909)
|
||||
- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896)
|
||||
- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889)
|
||||
- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831)
|
||||
- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934)
|
||||
|
||||
## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892)
|
||||
- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869)
|
||||
|
||||
## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866)
|
||||
|
||||
## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837)
|
||||
|
||||
## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809)
|
||||
- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807)
|
||||
- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823)
|
||||
|
||||
## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768)
|
||||
- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766)
|
||||
- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781)
|
||||
- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769)
|
||||
|
||||
## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770)
|
||||
- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760)
|
||||
- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754)
|
||||
- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752)
|
||||
- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748)
|
||||
- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744)
|
||||
- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742)
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736)
|
||||
- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721)
|
||||
- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719)
|
||||
- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716)
|
||||
- 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699)
|
||||
- Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678)
|
||||
- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675)
|
||||
- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674)
|
||||
- After Effects: base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667)
|
||||
- Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740)
|
||||
- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738)
|
||||
- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722)
|
||||
- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682)
|
||||
- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717)
|
||||
- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Application manager [\#728](https://github.com/pypeclub/pype/pull/728)
|
||||
- Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706)
|
||||
- Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700)
|
||||
- 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697)
|
||||
|
||||
## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
|
||||
# Changelog
|
||||
|
||||
## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15)
|
||||
|
|
@ -789,4 +1054,7 @@ A large cleanup release. Most of the change are under the hood.
|
|||
- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -166,14 +166,23 @@ sudo yum install qt5-qtbase-devel
|
|||
<details>
|
||||
<summary>Use pyenv to install Python version for OpenPype build</summary>
|
||||
|
||||
You will need **bzip2**, **readline** and **sqlite3** libraries.
|
||||
You will need **bzip2**, **readline**, **sqlite3** and other libraries.
|
||||
|
||||
**Ubuntu:**
|
||||
For more details about Python build environments see:
|
||||
|
||||
https://github.com/pyenv/pyenv/wiki#suggested-build-environment
|
||||
|
||||
**For Ubuntu:**
|
||||
```sh
|
||||
sudo apt install libbz2-dev libreadline-dev libsqlite3-dev
|
||||
sudo apt-get update; sudo apt-get install --no-install-recommends make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
|
||||
```
|
||||
|
||||
1) install **pyenv**
|
||||
**For Centos:**
|
||||
```sh
|
||||
yum install gcc zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel openssl-devel tk-devel libffi-devel
|
||||
```
|
||||
|
||||
**install pyenv**
|
||||
```sh
|
||||
curl https://pyenv.run | bash
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Open install dialog."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import os
|
||||
os.chdir(os.path.dirname(__file__)) # for override sys.path in Deadline
|
||||
|
||||
from Qt import QtWidgets # noqa
|
||||
from Qt.QtCore import Signal # noqa
|
||||
|
||||
from .install_dialog import InstallDialog
|
||||
from .bootstrap_repos import BootstrapRepos
|
||||
from .version import __version__ as version
|
||||
|
||||
|
|
@ -25,16 +21,21 @@ def get_result(res: int):
|
|||
|
||||
def open_dialog():
|
||||
"""Show Igniter dialog."""
|
||||
from Qt import QtWidgets
|
||||
from .install_dialog import InstallDialog
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
d = InstallDialog()
|
||||
d.finished.connect(get_result)
|
||||
d.open()
|
||||
|
||||
app.exec()
|
||||
|
||||
return RESULT
|
||||
|
||||
|
||||
__all__ = [
|
||||
"InstallDialog",
|
||||
"BootstrapRepos",
|
||||
"open_dialog",
|
||||
"version"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,10 @@ from zipfile import ZipFile, BadZipFile
|
|||
from appdirs import user_data_dir
|
||||
from speedcopy import copyfile
|
||||
|
||||
from .user_settings import OpenPypeSettingsRegistry
|
||||
from .user_settings import (
|
||||
OpenPypeSecureRegistry,
|
||||
OpenPypeSettingsRegistry
|
||||
)
|
||||
from .tools import get_openpype_path_from_db
|
||||
|
||||
|
||||
|
|
@ -239,6 +242,7 @@ class BootstrapRepos:
|
|||
self._app = "openpype"
|
||||
self._log = log.getLogger(str(__class__))
|
||||
self.data_dir = Path(user_data_dir(self._app, self._vendor))
|
||||
self.secure_registry = OpenPypeSecureRegistry("mongodb")
|
||||
self.registry = OpenPypeSettingsRegistry()
|
||||
self.zip_filter = [".pyc", "__pycache__"]
|
||||
self.openpype_filter = [
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from .tools import (
|
|||
validate_mongo_connection,
|
||||
get_openpype_path_from_db
|
||||
)
|
||||
from .user_settings import OpenPypeSettingsRegistry
|
||||
from .user_settings import OpenPypeSecureRegistry
|
||||
from .version import __version__
|
||||
|
||||
|
||||
|
|
@ -42,13 +42,13 @@ class InstallDialog(QtWidgets.QDialog):
|
|||
|
||||
def __init__(self, parent=None):
|
||||
super(InstallDialog, self).__init__(parent)
|
||||
self.registry = OpenPypeSettingsRegistry()
|
||||
self.secure_registry = OpenPypeSecureRegistry("mongodb")
|
||||
|
||||
self.mongo_url = ""
|
||||
try:
|
||||
self.mongo_url = (
|
||||
os.getenv("OPENPYPE_MONGO", "")
|
||||
or self.registry.get_secure_item("openPypeMongo")
|
||||
or self.secure_registry.get_item("openPypeMongo")
|
||||
)
|
||||
except ValueError:
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class InstallThread(QThread):
|
|||
if not os.getenv("OPENPYPE_MONGO"):
|
||||
# try to get it from settings registry
|
||||
try:
|
||||
self._mongo = bs.registry.get_secure_item(
|
||||
self._mongo = bs.secure_registry.get_item(
|
||||
"openPypeMongo")
|
||||
except ValueError:
|
||||
self.message.emit(
|
||||
|
|
@ -82,7 +82,7 @@ class InstallThread(QThread):
|
|||
self._mongo = os.getenv("OPENPYPE_MONGO")
|
||||
else:
|
||||
self.message.emit("Saving mongo connection string ...", False)
|
||||
bs.registry.set_secure_item("openPypeMongo", self._mongo)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ class InstallThread(QThread):
|
|||
f"!!! invalid mongo url {self._mongo}", True)
|
||||
self.finished.emit(InstallResult(-1))
|
||||
return
|
||||
bs.registry.set_secure_item("openPypeMongo", self._mongo)
|
||||
bs.secure_registry.set_item("openPypeMongo", self._mongo)
|
||||
os.environ["OPENPYPE_MONGO"] = self._mongo
|
||||
|
||||
self.message.emit(f"processing {self._path}", True)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,112 @@ except ImportError:
|
|||
|
||||
import platform
|
||||
|
||||
import appdirs
|
||||
import six
|
||||
import appdirs
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
|
||||
|
||||
class OpenPypeSecureRegistry:
|
||||
"""Store information using keyring.
|
||||
|
||||
Registry should be used for private data that should be available only for
|
||||
user.
|
||||
|
||||
All passed registry names will have added prefix `OpenPype/` to easier
|
||||
identify which data were created by OpenPype.
|
||||
|
||||
Args:
|
||||
name(str): Name of registry used as identifier for data.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
try:
|
||||
import keyring
|
||||
|
||||
except Exception:
|
||||
raise NotImplementedError(
|
||||
"Python module `keyring` is not available."
|
||||
)
|
||||
|
||||
# hack for cx_freeze and Windows keyring backend
|
||||
if platform.system().lower() == "windows":
|
||||
from keyring.backends import Windows
|
||||
|
||||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
# Force "OpenPype" prefix
|
||||
self._name = "/".join(("OpenPype", name))
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set sensitive item into system's keyring.
|
||||
|
||||
This uses `Keyring module`_ to save sensitive stuff into system's
|
||||
keyring.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
keyring.set_password(self._name, name, value)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_item(self, name, default=_PLACEHOLDER):
|
||||
"""Get value of sensitive item from system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
default (Any): Default value if item is not available.
|
||||
|
||||
Returns:
|
||||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist and default is not defined.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
value = keyring.get_password(self._name, name)
|
||||
if value:
|
||||
return value
|
||||
|
||||
if default is not _PLACEHOLDER:
|
||||
return default
|
||||
|
||||
# NOTE Should raise `KeyError`
|
||||
raise ValueError(
|
||||
"Item {}:{} does not exist in keyring.".format(self._name, name)
|
||||
)
|
||||
|
||||
def delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete value stored in system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item to be deleted.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
self.get_item.cache_clear()
|
||||
keyring.delete_password(self._name, name)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
|
|
@ -46,13 +150,6 @@ class ASettingRegistry():
|
|||
# type: (str) -> ASettingRegistry
|
||||
super(ASettingRegistry, self).__init__()
|
||||
|
||||
if six.PY3:
|
||||
import keyring
|
||||
# hack for cx_freeze and Windows keyring backend
|
||||
if platform.system() == "Windows":
|
||||
from keyring.backends import Windows
|
||||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
self._name = name
|
||||
self._items = {}
|
||||
|
||||
|
|
@ -127,78 +224,6 @@ class ASettingRegistry():
|
|||
del self._items[name]
|
||||
self._delete_item(name)
|
||||
|
||||
def set_secure_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set sensitive item into system's keyring.
|
||||
|
||||
This uses `Keyring module`_ to save sensitive stuff into system's
|
||||
keyring.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
keyring.set_password(self._name, name, value)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_secure_item(self, name):
|
||||
# type: (str) -> str
|
||||
"""Get value of sensitive item from system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
|
||||
Returns:
|
||||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
value = keyring.get_password(self._name, name)
|
||||
if not value:
|
||||
raise ValueError(
|
||||
"Item {}:{} does not exist in keyring.".format(
|
||||
self._name, name))
|
||||
return value
|
||||
|
||||
def delete_secure_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete value stored in system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item to be deleted.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
self.get_secure_item.cache_clear()
|
||||
keyring.delete_password(self._name, name)
|
||||
|
||||
|
||||
class IniSettingRegistry(ASettingRegistry):
|
||||
"""Class using :mod:`configparser`.
|
||||
|
|
@ -459,9 +484,10 @@ class OpenPypeSettingsRegistry(JSONSettingRegistry):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, name=None):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "openpype"
|
||||
if not name:
|
||||
name = "openpype_settings"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(OpenPypeSettingsRegistry, self).__init__(
|
||||
"openpype_settings", path)
|
||||
super(OpenPypeSettingsRegistry, self).__init__(name, path)
|
||||
|
|
|
|||
|
|
@ -6,12 +6,12 @@ class PrePython2Vendor(PreLaunchHook):
|
|||
"""Prepend python 2 dependencies for py2 hosts."""
|
||||
# WARNING This hook will probably be deprecated in OpenPype 3 - kept for test
|
||||
order = 10
|
||||
app_groups = ["hiero", "nuke", "nukex"]
|
||||
app_groups = ["hiero", "nuke", "nukex", "unreal"]
|
||||
|
||||
def execute(self):
|
||||
# Prepare vendor dir path
|
||||
self.log.info("adding global python 2 vendor")
|
||||
pype_root = os.getenv("OPENPYPE_ROOT")
|
||||
pype_root = os.getenv("OPENPYPE_REPOS_ROOT")
|
||||
python_2_vendor = os.path.join(
|
||||
pype_root,
|
||||
"openpype",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import subprocess
|
||||
from openpype.lib import PreLaunchHook
|
||||
|
||||
|
||||
|
|
@ -10,15 +11,32 @@ class LaunchWithWindowsShell(PreLaunchHook):
|
|||
instead.
|
||||
"""
|
||||
|
||||
order = 10
|
||||
app_groups = ["resolve", "nuke", "nukex", "hiero", "nukestudio"]
|
||||
# Should be as last hook becuase must change launch arguments to string
|
||||
order = 1000
|
||||
app_groups = ["nuke", "nukex", "hiero", "nukestudio"]
|
||||
platforms = ["windows"]
|
||||
|
||||
def execute(self):
|
||||
# Get comspec which is cmd.exe in most cases.
|
||||
comspec = os.environ.get("COMSPEC", "cmd.exe")
|
||||
# Add comspec to arguments list and add "/k"
|
||||
new_args = [comspec, "/c"]
|
||||
new_args.extend(self.launch_context.launch_args)
|
||||
new_args = [
|
||||
# Get comspec which is cmd.exe in most cases.
|
||||
os.environ.get("COMSPEC", "cmd.exe"),
|
||||
# NOTE change to "/k" if want to keep console opened
|
||||
"/c",
|
||||
# Convert arguments to command line arguments (as string)
|
||||
"\"{}\"".format(
|
||||
subprocess.list2cmdline(self.launch_context.launch_args)
|
||||
)
|
||||
]
|
||||
# Convert list to string
|
||||
# WARNING this only works if is used as string
|
||||
args_string = " ".join(new_args)
|
||||
self.log.info((
|
||||
"Modified launch arguments to be launched with shell \"{}\"."
|
||||
).format(args_string))
|
||||
|
||||
# Replace launch args with new one
|
||||
self.launch_context.launch_args = new_args
|
||||
self.launch_context.launch_args = args_string
|
||||
# Change `creationflags` to CREATE_NEW_CONSOLE
|
||||
self.launch_context.kwargs["creationflags"] = (
|
||||
subprocess.CREATE_NEW_CONSOLE
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import pyblish.api as pyblish
|
|||
import openpype.hosts.aftereffects
|
||||
|
||||
|
||||
log = logging.getLogger("pype.hosts.aftereffects")
|
||||
log = logging.getLogger("openpype.hosts.aftereffects")
|
||||
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.aftereffects.__file__))
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender):
|
|||
"Please recreate instance.")
|
||||
item_id = inst["members"][0]
|
||||
work_area_info = self.stub.get_work_area(int(item_id))
|
||||
|
||||
if not work_area_info:
|
||||
self.log.warning("Orphaned instance, deleting metadata")
|
||||
self.stub.remove_instance(int(item_id))
|
||||
continue
|
||||
|
||||
frameStart = work_area_info.workAreaStart
|
||||
|
||||
frameEnd = round(work_area_info.workAreaStart +
|
||||
|
|
|
|||
|
|
@ -21,3 +21,4 @@ class RemovePublishHighlight(openpype.api.Extractor):
|
|||
item = instance.data
|
||||
comp_name = item["comp_name"].replace(stub.PUBLISH_ICON, '')
|
||||
stub.rename_item(item["comp_id"], comp_name)
|
||||
instance.data["comp_name"] = comp_name
|
||||
|
|
|
|||
|
|
@ -51,8 +51,37 @@ def set_start_end_frames():
|
|||
"name": asset_name
|
||||
})
|
||||
|
||||
bpy.context.scene.frame_start = asset_doc["data"]["frameStart"]
|
||||
bpy.context.scene.frame_end = asset_doc["data"]["frameEnd"]
|
||||
scene = bpy.context.scene
|
||||
|
||||
# Default scene settings
|
||||
frameStart = scene.frame_start
|
||||
frameEnd = scene.frame_end
|
||||
fps = scene.render.fps
|
||||
resolution_x = scene.render.resolution_x
|
||||
resolution_y = scene.render.resolution_y
|
||||
|
||||
# Check if settings are set
|
||||
data = asset_doc.get("data")
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get("frameStart"):
|
||||
frameStart = data.get("frameStart")
|
||||
if data.get("frameEnd"):
|
||||
frameEnd = data.get("frameEnd")
|
||||
if data.get("fps"):
|
||||
fps = data.get("fps")
|
||||
if data.get("resolutionWidth"):
|
||||
resolution_x = data.get("resolutionWidth")
|
||||
if data.get("resolutionHeight"):
|
||||
resolution_y = data.get("resolutionHeight")
|
||||
|
||||
scene.frame_start = frameStart
|
||||
scene.frame_end = frameEnd
|
||||
scene.render.fps = fps
|
||||
scene.render.resolution_x = resolution_x
|
||||
scene.render.resolution_y = resolution_y
|
||||
|
||||
|
||||
def on_new(arg1, arg2):
|
||||
|
|
|
|||
|
|
@ -292,6 +292,9 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
animation_creator_name = "CreateAnimation"
|
||||
setdress_creator_name = "CreateSetDress"
|
||||
|
||||
def _remove_objects(self, objects):
|
||||
for obj in list(objects):
|
||||
if obj.type == 'ARMATURE':
|
||||
|
|
@ -368,7 +371,7 @@ class UnrealLayoutLoader(plugin.AssetLoader):
|
|||
location.get('z')
|
||||
)
|
||||
obj.rotation_euler = (
|
||||
rotation.get('x'),
|
||||
rotation.get('x') + math.pi / 2,
|
||||
-rotation.get('y'),
|
||||
-rotation.get('z')
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
from typing import List
|
||||
|
||||
import pyblish.api
|
||||
import openpype.hosts.blender.api.action
|
||||
|
||||
|
||||
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
||||
"""Validate that the current object is in Object Mode."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["model", "rig"]
|
||||
category = "geometry"
|
||||
label = "Object is in Object Mode"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
optional = True
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance) -> List:
|
||||
invalid = []
|
||||
for obj in [obj for obj in instance]:
|
||||
try:
|
||||
if obj.type == 'MESH' or obj.type == 'ARMATURE':
|
||||
# Check if the object is in object mode.
|
||||
if not obj.mode == 'OBJECT':
|
||||
invalid.append(obj)
|
||||
except Exception:
|
||||
continue
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise RuntimeError(
|
||||
f"Object found in instance is not in Object Mode: {invalid}")
|
||||
|
|
@ -62,7 +62,7 @@ def _prepare_publish_environments():
|
|||
env["AVALON_TASK"] = os.getenv("AVALON_TASK")
|
||||
env["AVALON_WORKDIR"] = os.getenv("AVALON_WORKDIR")
|
||||
env["AVALON_APP"] = f"hosts.{publish_host}"
|
||||
env["AVALON_APP_NAME"] = "celaction_local"
|
||||
env["AVALON_APP_NAME"] = "celaction/local"
|
||||
|
||||
env["PYBLISH_HOSTS"] = publish_host
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ def install():
|
|||
avalon.data["familiesStateDefault"] = False
|
||||
avalon.data["familiesStateToggled"] = family_states
|
||||
|
||||
log.info("pype.hosts.fusion installed")
|
||||
log.info("openpype.hosts.fusion installed")
|
||||
|
||||
pyblish.register_host("fusion")
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ class CollectFusionRenderMode(pyblish.api.InstancePlugin):
|
|||
available tool does not visualize which render mode is set for the
|
||||
current comp, please run the following line in the console (Py2)
|
||||
|
||||
comp.GetData("pype.rendermode")
|
||||
comp.GetData("openpype.rendermode")
|
||||
|
||||
This will return the name of the current render mode as seen above under
|
||||
Options.
|
||||
|
|
@ -34,7 +34,7 @@ class CollectFusionRenderMode(pyblish.api.InstancePlugin):
|
|||
raise RuntimeError("No comp previously collected, unable to "
|
||||
"retrieve Fusion version.")
|
||||
|
||||
rendermode = comp.GetData("pype.rendermode") or "local"
|
||||
rendermode = comp.GetData("openpype.rendermode") or "local"
|
||||
assert rendermode in options, "Must be supported render mode"
|
||||
|
||||
self.log.info("Render mode: {0}".format(rendermode))
|
||||
|
|
|
|||
|
|
@ -96,11 +96,11 @@ class SetRenderMode(QtWidgets.QWidget):
|
|||
return self._comp.GetAttrs("COMPS_Name")
|
||||
|
||||
def _get_comp_rendermode(self):
|
||||
return self._comp.GetData("pype.rendermode") or "local"
|
||||
return self._comp.GetData("openpype.rendermode") or "local"
|
||||
|
||||
def _set_comp_rendermode(self):
|
||||
rendermode = self.mode_options.currentText()
|
||||
self._comp.SetData("pype.rendermode", rendermode)
|
||||
self._comp.SetData("openpype.rendermode", rendermode)
|
||||
|
||||
self._comp.Print("Updated render mode to '%s'\n" % rendermode)
|
||||
self.hide()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import openpype.hosts.hiero.api as phiero
|
|||
avalon.api.install(phiero)
|
||||
|
||||
try:
|
||||
__import__("pype.hosts.hiero.api")
|
||||
__import__("openpype.hosts.hiero.api")
|
||||
__import__("pyblish")
|
||||
|
||||
except ImportError as e:
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class CreateLook(plugin.Creator):
|
|||
family = "look"
|
||||
icon = "paint-brush"
|
||||
defaults = ['Main']
|
||||
make_txt = True
|
||||
make_tx = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateLook, self).__init__(*args, **kwargs)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
import os
|
||||
import sys
|
||||
import nuke
|
||||
|
||||
from avalon import api as avalon
|
||||
from openpype.tools import workfiles
|
||||
from pyblish import api as pyblish
|
||||
from openpype.api import Logger
|
||||
import openpype.hosts.nuke
|
||||
import avalon.api
|
||||
import pyblish.api
|
||||
import openpype
|
||||
from . import lib, menu
|
||||
|
||||
|
||||
self = sys.modules[__name__]
|
||||
self.workfiles_launched = False
|
||||
log = Logger().get_logger(__name__)
|
||||
log = openpype.api.Logger().get_logger(__name__)
|
||||
|
||||
AVALON_CONFIG = os.getenv("AVALON_CONFIG", "pype")
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.nuke.__file__))
|
||||
|
|
@ -25,7 +19,7 @@ INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
|||
|
||||
# registering pyblish gui regarding settings in presets
|
||||
if os.getenv("PYBLISH_GUI", None):
|
||||
pyblish.register_gui(os.getenv("PYBLISH_GUI", None))
|
||||
pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None))
|
||||
|
||||
|
||||
def reload_config():
|
||||
|
|
@ -61,15 +55,16 @@ def install():
|
|||
'''
|
||||
|
||||
log.info("Registering Nuke plug-ins..")
|
||||
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)
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH)
|
||||
avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH)
|
||||
avalon.api.register_plugin_path(avalon.api.InventoryAction, INVENTORY_PATH)
|
||||
|
||||
# Register Avalon event for workfiles loading.
|
||||
avalon.on("workio.open_file", lib.check_inventory_versions)
|
||||
avalon.api.on("workio.open_file", lib.check_inventory_versions)
|
||||
|
||||
pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled)
|
||||
pyblish.api.register_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled)
|
||||
workfile_settings = lib.WorkfileSettings()
|
||||
# Disable all families except for the ones we explicitly want to see
|
||||
family_states = [
|
||||
|
|
@ -79,39 +74,27 @@ def install():
|
|||
"gizmo"
|
||||
]
|
||||
|
||||
avalon.data["familiesStateDefault"] = False
|
||||
avalon.data["familiesStateToggled"] = family_states
|
||||
|
||||
# Workfiles.
|
||||
launch_workfiles = os.environ.get("WORKFILES_STARTUP")
|
||||
|
||||
if launch_workfiles:
|
||||
nuke.addOnCreate(launch_workfiles_app, nodeClass="Root")
|
||||
avalon.api.data["familiesStateDefault"] = False
|
||||
avalon.api.data["familiesStateToggled"] = family_states
|
||||
|
||||
# Set context settings.
|
||||
nuke.addOnCreate(workfile_settings.set_context_settings, nodeClass="Root")
|
||||
# nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root")
|
||||
|
||||
nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root")
|
||||
nuke.addOnCreate(lib.open_last_workfile, nodeClass="Root")
|
||||
nuke.addOnCreate(lib.launch_workfiles_app, nodeClass="Root")
|
||||
menu.install()
|
||||
|
||||
|
||||
def launch_workfiles_app():
|
||||
'''Function letting start workfiles after start of host
|
||||
'''
|
||||
if not self.workfiles_launched:
|
||||
self.workfiles_launched = True
|
||||
workfiles.show(os.environ["AVALON_WORKDIR"])
|
||||
|
||||
|
||||
def uninstall():
|
||||
'''Uninstalling host's integration
|
||||
'''
|
||||
log.info("Deregistering Nuke plug-ins..")
|
||||
pyblish.deregister_plugin_path(PUBLISH_PATH)
|
||||
avalon.deregister_plugin_path(avalon.Loader, LOAD_PATH)
|
||||
avalon.deregister_plugin_path(avalon.Creator, CREATE_PATH)
|
||||
pyblish.api.deregister_plugin_path(PUBLISH_PATH)
|
||||
avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH)
|
||||
avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH)
|
||||
|
||||
pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled)
|
||||
pyblish.api.deregister_callback(
|
||||
"instanceToggled", on_pyblish_instance_toggled)
|
||||
|
||||
reload_config()
|
||||
menu.uninstall()
|
||||
|
|
@ -123,7 +106,7 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
|
|||
log.info("instance toggle: {}, old_value: {}, new_value:{} ".format(
|
||||
instance, old_value, new_value))
|
||||
|
||||
from avalon.nuke import (
|
||||
from avalon.api.nuke import (
|
||||
viewer_update_and_undo_stop,
|
||||
add_publish_knob
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@ import re
|
|||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
from avalon import api, io, lib
|
||||
from avalon.tools import workfiles
|
||||
import avalon.nuke
|
||||
from avalon.nuke import lib as anlib
|
||||
from avalon.nuke import (
|
||||
save_file, open_file
|
||||
)
|
||||
from openpype.api import (
|
||||
Logger,
|
||||
Anatomy,
|
||||
|
|
@ -13,6 +18,7 @@ from openpype.api import (
|
|||
get_anatomy_settings,
|
||||
get_hierarchy,
|
||||
get_asset,
|
||||
get_current_project_settings,
|
||||
config,
|
||||
ApplicationManager
|
||||
)
|
||||
|
|
@ -25,8 +31,10 @@ log = Logger().get_logger(__name__)
|
|||
|
||||
self = sys.modules[__name__]
|
||||
self._project = None
|
||||
self.workfiles_launched = False
|
||||
self._node_tab_name = "{}".format(os.getenv("AVALON_LABEL") or "Avalon")
|
||||
|
||||
|
||||
def get_node_imageio_setting(**kwarg):
|
||||
''' Get preset data for dataflow (fileType, compression, bitDepth)
|
||||
'''
|
||||
|
|
@ -1616,3 +1624,41 @@ def find_free_space_to_paste_nodes(
|
|||
xpos = min(group_xpos)
|
||||
ypos = max(group_ypos) + abs(offset)
|
||||
return xpos, ypos
|
||||
|
||||
|
||||
def launch_workfiles_app():
|
||||
'''Function letting start workfiles after start of host
|
||||
'''
|
||||
# get state from settings
|
||||
open_at_start = get_current_project_settings()["nuke"].get(
|
||||
"general", {}).get("open_workfile_at_start")
|
||||
|
||||
# return if none is defined
|
||||
if not open_at_start:
|
||||
return
|
||||
|
||||
if not self.workfiles_launched:
|
||||
self.workfiles_launched = True
|
||||
workfiles.show(os.environ["AVALON_WORKDIR"])
|
||||
|
||||
|
||||
def open_last_workfile():
|
||||
# get state from settings
|
||||
open_last_version = get_current_project_settings()["nuke"].get(
|
||||
"general", {}).get("create_initial_workfile")
|
||||
|
||||
log.info("Opening last workfile...")
|
||||
last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE")
|
||||
|
||||
if not os.path.exists(last_workfile_path):
|
||||
# return if none is defined
|
||||
if not open_last_version:
|
||||
return
|
||||
|
||||
save_file(last_workfile_path)
|
||||
else:
|
||||
# to avoid looping of the callback, remove it!
|
||||
nuke.removeOnCreate(open_last_workfile, nodeClass="Root")
|
||||
|
||||
# open workfile
|
||||
open_file(last_workfile_path)
|
||||
|
|
|
|||
|
|
@ -80,25 +80,31 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
# Add all nodes in group instances.
|
||||
if node.Class() == "Group":
|
||||
# check if it is write node in family
|
||||
if "write" in families:
|
||||
# only alter families for render family
|
||||
if "write" in families_ak:
|
||||
target = node["render"].value()
|
||||
if target == "Use existing frames":
|
||||
# Local rendering
|
||||
self.log.info("flagged for no render")
|
||||
families.append("render")
|
||||
families.append(family)
|
||||
elif target == "Local":
|
||||
# Local rendering
|
||||
self.log.info("flagged for local render")
|
||||
families.append("{}.local".format("render"))
|
||||
families.append("{}.local".format(family))
|
||||
elif target == "On farm":
|
||||
# Farm rendering
|
||||
self.log.info("flagged for farm render")
|
||||
instance.data["transfer"] = False
|
||||
families.append("{}.farm".format("render"))
|
||||
families.append("{}.farm".format(family))
|
||||
|
||||
# suffle family to `write` as it is main family
|
||||
# this will be changed later on in process
|
||||
if "render" in families:
|
||||
families.remove("render")
|
||||
family = "write"
|
||||
elif "prerender" in families:
|
||||
families.remove("prerender")
|
||||
family = "write"
|
||||
|
||||
node.begin()
|
||||
for i in nuke.allNodes():
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"families": [f.replace(".local", "").replace(".farm", "")
|
||||
for f in families if "write" not in f],
|
||||
"colorspace": node["colorspace"].value(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from openpype import lib
|
|||
from pyblish import api as pyblish
|
||||
import openpype.hosts.photoshop
|
||||
|
||||
log = logging.getLogger("pype.hosts.photoshop")
|
||||
log = logging.getLogger("openpype.hosts.photoshop")
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.photoshop.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ from .api.pipeline import (
|
|||
update_container,
|
||||
publish,
|
||||
launch_workfiles_app,
|
||||
maintained_selection
|
||||
maintained_selection,
|
||||
remove_instance,
|
||||
list_instances
|
||||
)
|
||||
|
||||
from .api.lib import (
|
||||
|
|
@ -73,6 +75,8 @@ __all__ = [
|
|||
"publish",
|
||||
"launch_workfiles_app",
|
||||
"maintained_selection",
|
||||
"remove_instance",
|
||||
"list_instances",
|
||||
|
||||
# utils
|
||||
"setup",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ from avalon.tools import (
|
|||
creator,
|
||||
loader,
|
||||
sceneinventory,
|
||||
libraryloader
|
||||
libraryloader,
|
||||
subsetmanager
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -64,8 +65,9 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
publish_btn = QtWidgets.QPushButton("Publish ...", self)
|
||||
load_btn = QtWidgets.QPushButton("Load ...", self)
|
||||
inventory_btn = QtWidgets.QPushButton("Inventory ...", self)
|
||||
subsetm_btn = QtWidgets.QPushButton("Subset Manager ...", self)
|
||||
libload_btn = QtWidgets.QPushButton("Library ...", self)
|
||||
# rename_btn = QtWidgets.QPushButton("Rename ...", self)
|
||||
# rename_btn = QtWidgets.QPushButton("Rename", self)
|
||||
# set_colorspace_btn = QtWidgets.QPushButton(
|
||||
# "Set colorspace from presets", self
|
||||
# )
|
||||
|
|
@ -81,6 +83,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
layout.addWidget(publish_btn)
|
||||
layout.addWidget(load_btn)
|
||||
layout.addWidget(inventory_btn)
|
||||
layout.addWidget(subsetm_btn)
|
||||
|
||||
layout.addWidget(Spacer(15, self))
|
||||
|
||||
|
|
@ -102,6 +105,7 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
publish_btn.clicked.connect(self.on_publish_clicked)
|
||||
load_btn.clicked.connect(self.on_load_clicked)
|
||||
inventory_btn.clicked.connect(self.on_inventory_clicked)
|
||||
subsetm_btn.clicked.connect(self.on_subsetm_clicked)
|
||||
libload_btn.clicked.connect(self.on_libload_clicked)
|
||||
# rename_btn.clicked.connect(self.on_rename_clicked)
|
||||
# set_colorspace_btn.clicked.connect(self.on_set_colorspace_clicked)
|
||||
|
|
@ -127,6 +131,10 @@ class OpenPypeMenu(QtWidgets.QWidget):
|
|||
print("Clicked Inventory")
|
||||
sceneinventory.show()
|
||||
|
||||
def on_subsetm_clicked(self):
|
||||
print("Clicked Subset Manager")
|
||||
subsetmanager.show()
|
||||
|
||||
def on_libload_clicked(self):
|
||||
print("Clicked Library")
|
||||
libraryloader.show()
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ def install():
|
|||
avalon.data["familiesStateDefault"] = False
|
||||
avalon.data["familiesStateToggled"] = family_states
|
||||
|
||||
log.info("pype.hosts.resolve installed")
|
||||
log.info("openpype.hosts.resolve installed")
|
||||
|
||||
pyblish.register_host("resolve")
|
||||
pyblish.register_plugin_path(PUBLISH_PATH)
|
||||
|
|
@ -258,3 +258,51 @@ def on_pyblish_instance_toggled(instance, old_value, new_value):
|
|||
# Whether instances should be passthrough based on new value
|
||||
timeline_item = instance.data["item"]
|
||||
set_publish_attribute(timeline_item, new_value)
|
||||
|
||||
|
||||
def remove_instance(instance):
|
||||
"""Remove instance marker from track item."""
|
||||
instance_id = instance.get("uuid")
|
||||
|
||||
selected_timeline_items = lib.get_current_timeline_items(
|
||||
filter=True, selecting_color=lib.publish_clip_color)
|
||||
|
||||
found_ti = None
|
||||
for timeline_item_data in selected_timeline_items:
|
||||
timeline_item = timeline_item_data["clip"]["item"]
|
||||
|
||||
# get openpype tag data
|
||||
tag_data = lib.get_timeline_item_pype_tag(timeline_item)
|
||||
_ti_id = tag_data.get("uuid")
|
||||
if _ti_id == instance_id:
|
||||
found_ti = timeline_item
|
||||
break
|
||||
|
||||
if found_ti is None:
|
||||
return
|
||||
|
||||
# removing instance by marker color
|
||||
print(f"Removing instance: {found_ti.GetName()}")
|
||||
found_ti.DeleteMarkersByColor(lib.pype_marker_color)
|
||||
|
||||
|
||||
def list_instances():
|
||||
"""List all created instances from current workfile."""
|
||||
listed_instances = []
|
||||
selected_timeline_items = lib.get_current_timeline_items(
|
||||
filter=True, selecting_color=lib.publish_clip_color)
|
||||
|
||||
for timeline_item_data in selected_timeline_items:
|
||||
timeline_item = timeline_item_data["clip"]["item"]
|
||||
ti_name = timeline_item.GetName().split(".")[0]
|
||||
|
||||
# get openpype tag data
|
||||
tag_data = lib.get_timeline_item_pype_tag(timeline_item)
|
||||
|
||||
if tag_data:
|
||||
asset = tag_data.get("asset")
|
||||
subset = tag_data.get("subset")
|
||||
tag_data["label"] = f"{ti_name} [{asset}-{subset}]"
|
||||
listed_instances.append(tag_data)
|
||||
|
||||
return listed_instances
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import re
|
||||
import uuid
|
||||
from avalon import api
|
||||
import openpype.api as pype
|
||||
from openpype.hosts import resolve
|
||||
|
|
@ -697,13 +698,13 @@ class PublishClip:
|
|||
Populating the tag data into internal variable self.tag_data
|
||||
"""
|
||||
# define vertical sync attributes
|
||||
master_layer = True
|
||||
hero_track = True
|
||||
self.review_layer = ""
|
||||
if self.vertical_sync:
|
||||
# check if track name is not in driving layer
|
||||
if self.track_name not in self.driving_layer:
|
||||
# if it is not then define vertical sync as None
|
||||
master_layer = False
|
||||
hero_track = False
|
||||
|
||||
# increasing steps by index of rename iteration
|
||||
self.count_steps *= self.rename_index
|
||||
|
|
@ -717,7 +718,7 @@ class PublishClip:
|
|||
self.tag_data[_k] = _v["value"]
|
||||
|
||||
# driving layer is set as positive match
|
||||
if master_layer or self.vertical_sync:
|
||||
if hero_track or self.vertical_sync:
|
||||
# mark review layer
|
||||
if self.review_track and (
|
||||
self.review_track not in self.review_track_default):
|
||||
|
|
@ -751,35 +752,39 @@ class PublishClip:
|
|||
hierarchy_formating_data
|
||||
)
|
||||
|
||||
tag_hierarchy_data.update({"masterLayer": True})
|
||||
if master_layer and self.vertical_sync:
|
||||
# tag_hierarchy_data.update({"masterLayer": True})
|
||||
tag_hierarchy_data.update({"heroTrack": True})
|
||||
if hero_track and self.vertical_sync:
|
||||
self.vertical_clip_match.update({
|
||||
(self.clip_in, self.clip_out): tag_hierarchy_data
|
||||
})
|
||||
|
||||
if not master_layer and self.vertical_sync:
|
||||
if not hero_track and self.vertical_sync:
|
||||
# driving layer is set as negative match
|
||||
for (_in, _out), master_data in self.vertical_clip_match.items():
|
||||
master_data.update({"masterLayer": False})
|
||||
for (_in, _out), hero_data in self.vertical_clip_match.items():
|
||||
hero_data.update({"heroTrack": False})
|
||||
if _in == self.clip_in and _out == self.clip_out:
|
||||
data_subset = master_data["subset"]
|
||||
# add track index in case duplicity of names in master data
|
||||
data_subset = hero_data["subset"]
|
||||
# add track index in case duplicity of names in hero data
|
||||
if self.subset in data_subset:
|
||||
master_data["subset"] = self.subset + str(
|
||||
hero_data["subset"] = self.subset + str(
|
||||
self.track_index)
|
||||
# in case track name and subset name is the same then add
|
||||
if self.subset_name == self.track_name:
|
||||
master_data["subset"] = self.subset
|
||||
hero_data["subset"] = self.subset
|
||||
# assing data to return hierarchy data to tag
|
||||
tag_hierarchy_data = master_data
|
||||
tag_hierarchy_data = hero_data
|
||||
|
||||
# add data to return data dict
|
||||
self.tag_data.update(tag_hierarchy_data)
|
||||
|
||||
if master_layer and self.review_layer:
|
||||
# add uuid to tag data
|
||||
self.tag_data["uuid"] = str(uuid.uuid4())
|
||||
|
||||
# add review track only to hero track
|
||||
if hero_track and self.review_layer:
|
||||
self.tag_data.update({"reviewTrack": self.review_layer})
|
||||
|
||||
|
||||
def _solve_tag_hierarchy_data(self, hierarchy_formating_data):
|
||||
""" Solve tag data from hierarchy data and templates. """
|
||||
# fill up clip name and hierarchy keys
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class ResolvePrelaunch(PreLaunchHook):
|
|||
self.launch_context.env["PRE_PYTHON_SCRIPT"] = pre_py_sc
|
||||
self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...")
|
||||
try:
|
||||
__import__("pype.hosts.resolve")
|
||||
__import__("openpype.hosts.resolve")
|
||||
__import__("pyblish")
|
||||
|
||||
except ImportError:
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class CreateShotClip(resolve.Creator):
|
|||
"vSyncTrack": {
|
||||
"value": gui_tracks, # noqa
|
||||
"type": "QComboBox",
|
||||
"label": "Master track",
|
||||
"label": "Hero track",
|
||||
"target": "ui",
|
||||
"toolTip": "Select driving track name which should be mastering all others", # noqa
|
||||
"order": 1}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ from openpype.hosts import resolve
|
|||
from pprint import pformat
|
||||
|
||||
|
||||
class CollectInstances(pyblish.api.ContextPlugin):
|
||||
class PrecollectInstances(pyblish.api.ContextPlugin):
|
||||
"""Collect all Track items selection."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.59
|
||||
label = "Collect Instances"
|
||||
label = "Precollect Instances"
|
||||
hosts = ["resolve"]
|
||||
|
||||
def process(self, context):
|
||||
|
|
@ -26,7 +26,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
data = dict()
|
||||
timeline_item = timeline_item_data["clip"]["item"]
|
||||
|
||||
# get openpype tag data
|
||||
# get pype tag data
|
||||
tag_data = resolve.get_timeline_item_pype_tag(timeline_item)
|
||||
self.log.debug(f"__ tag_data: {pformat(tag_data)}")
|
||||
|
||||
|
|
@ -102,10 +102,10 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
})
|
||||
|
||||
def create_shot_instance(self, context, timeline_item, **data):
|
||||
master_layer = data.get("masterLayer")
|
||||
hero_track = data.get("heroTrack")
|
||||
hierarchy_data = data.get("hierarchyData")
|
||||
|
||||
if not master_layer:
|
||||
if not hero_track:
|
||||
return
|
||||
|
||||
if not hierarchy_data:
|
||||
|
|
@ -9,10 +9,10 @@ from openpype.hosts.resolve.otio import davinci_export
|
|||
reload(davinci_export)
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file into context"""
|
||||
class PrecollectWorkfile(pyblish.api.ContextPlugin):
|
||||
"""Precollect the current working file into context"""
|
||||
|
||||
label = "Collect Workfile"
|
||||
label = "Precollect Workfile"
|
||||
order = pyblish.api.CollectorOrder - 0.6
|
||||
|
||||
def process(self, context):
|
||||
|
|
@ -21,8 +21,6 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
|
|||
subset = "workfile"
|
||||
project = resolve.get_current_project()
|
||||
fps = project.GetSetting("timelineFrameRate")
|
||||
|
||||
active_timeline = resolve.get_current_timeline()
|
||||
video_tracks = resolve.get_video_track_names()
|
||||
|
||||
# adding otio timeline to context
|
||||
|
|
@ -58,9 +58,8 @@ def _close_window(event):
|
|||
def _export_button(event):
|
||||
pm = resolve.GetProjectManager()
|
||||
project = pm.GetCurrentProject()
|
||||
fps = project.GetSetting("timelineFrameRate")
|
||||
timeline = project.GetCurrentTimeline()
|
||||
otio_timeline = otio_export.create_otio_timeline(timeline, fps)
|
||||
otio_timeline = otio_export.create_otio_timeline(project)
|
||||
otio_path = os.path.join(
|
||||
itm["exportfilebttn"].Text,
|
||||
timeline.GetName() + ".otio")
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from avalon.tvpaint.communication_server import register_localization_file
|
||||
from avalon.tvpaint import pipeline
|
||||
import avalon.api
|
||||
import pyblish.api
|
||||
from avalon.tvpaint import pipeline
|
||||
from avalon.tvpaint.communication_server import register_localization_file
|
||||
from .lib import set_context_settings
|
||||
|
||||
from openpype.hosts import tvpaint
|
||||
|
||||
log = logging.getLogger("openpype.hosts.tvpaint")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
HOST_DIR = os.path.dirname(os.path.abspath(tvpaint.__file__))
|
||||
PLUGINS_DIR = os.path.join(HOST_DIR, "plugins")
|
||||
|
|
@ -34,6 +35,18 @@ def on_instance_toggle(instance, old_value, new_value):
|
|||
pipeline._write_instances(current_instances)
|
||||
|
||||
|
||||
def initial_launch():
|
||||
# Setup project settings if its the template that's launched.
|
||||
# TODO also check for template creation when it's possible to define
|
||||
# templates
|
||||
last_workfile = os.environ.get("AVALON_LAST_WORKFILE")
|
||||
if not last_workfile or os.path.exists(last_workfile):
|
||||
return
|
||||
|
||||
log.info("Setting up project...")
|
||||
set_context_settings()
|
||||
|
||||
|
||||
def install():
|
||||
log.info("OpenPype - Installing TVPaint integration")
|
||||
localization_file = os.path.join(HOST_DIR, "resources", "avalon.loc")
|
||||
|
|
@ -49,6 +62,8 @@ def install():
|
|||
if on_instance_toggle not in registered_callbacks:
|
||||
pyblish.api.register_callback("instanceToggled", on_instance_toggle)
|
||||
|
||||
avalon.api.on("application.launched", initial_launch)
|
||||
|
||||
|
||||
def uninstall():
|
||||
log.info("OpenPype - Uninstalling TVPaint integration")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from PIL import Image
|
||||
|
||||
import avalon.io
|
||||
from avalon.tvpaint.lib import execute_george
|
||||
|
||||
|
||||
def composite_images(input_image_paths, output_filepath):
|
||||
"""Composite images in order from passed list.
|
||||
|
|
@ -18,3 +21,64 @@ def composite_images(input_image_paths, output_filepath):
|
|||
else:
|
||||
img_obj.alpha_composite(_img_obj)
|
||||
img_obj.save(output_filepath)
|
||||
|
||||
|
||||
def set_context_settings(asset_doc=None):
|
||||
"""Set workfile settings by asset document data.
|
||||
|
||||
Change fps, resolution and frame start/end.
|
||||
"""
|
||||
if asset_doc is None:
|
||||
# Use current session asset if not passed
|
||||
asset_doc = avalon.io.find_one({
|
||||
"type": "asset",
|
||||
"name": avalon.io.Session["AVALON_ASSET"]
|
||||
})
|
||||
|
||||
project_doc = avalon.io.find_one({"type": "project"})
|
||||
|
||||
framerate = asset_doc["data"].get("fps")
|
||||
if framerate is None:
|
||||
framerate = project_doc["data"].get("fps")
|
||||
|
||||
if framerate is not None:
|
||||
execute_george(
|
||||
"tv_framerate {} \"timestretch\"".format(framerate)
|
||||
)
|
||||
else:
|
||||
print("Framerate was not found!")
|
||||
|
||||
width_key = "resolutionWidth"
|
||||
height_key = "resolutionHeight"
|
||||
|
||||
width = asset_doc["data"].get(width_key)
|
||||
height = asset_doc["data"].get(height_key)
|
||||
if width is None or height is None:
|
||||
width = project_doc["data"].get(width_key)
|
||||
height = project_doc["data"].get(height_key)
|
||||
|
||||
if width is None or height is None:
|
||||
print("Resolution was not found!")
|
||||
else:
|
||||
execute_george("tv_resizepage {} {} 0".format(width, height))
|
||||
|
||||
frame_start = asset_doc["data"].get("frameStart")
|
||||
frame_end = asset_doc["data"].get("frameEnd")
|
||||
|
||||
if frame_start is None or frame_end is None:
|
||||
print("Frame range was not found!")
|
||||
return
|
||||
|
||||
handles = asset_doc["data"].get("handles") or 0
|
||||
handle_start = asset_doc["data"].get("handleStart")
|
||||
handle_end = asset_doc["data"].get("handleEnd")
|
||||
|
||||
if handle_start is None or handle_end is None:
|
||||
handle_start = handles
|
||||
handle_end = handles
|
||||
|
||||
frame_start -= int(handle_start)
|
||||
frame_end += int(handle_end)
|
||||
|
||||
execute_george("tv_markin {} set".format(frame_start - 1))
|
||||
execute_george("tv_markout {} set".format(frame_end - 1))
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
))
|
||||
|
||||
for instance_data in workfile_instances:
|
||||
instance_data["fps"] = context.data["fps"]
|
||||
instance_data["fps"] = context.data["sceneFps"]
|
||||
|
||||
# Store workfile instance data to instance data
|
||||
instance_data["originData"] = copy.deepcopy(instance_data)
|
||||
|
|
@ -32,6 +32,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
subset_name = instance_data["subset"]
|
||||
name = instance_data.get("name", subset_name)
|
||||
instance_data["name"] = name
|
||||
instance_data["label"] = "{} [{}-{}]".format(
|
||||
name,
|
||||
context.data["sceneMarkIn"] + 1,
|
||||
context.data["sceneMarkOut"] + 1
|
||||
)
|
||||
|
||||
active = instance_data.get("active", True)
|
||||
instance_data["active"] = active
|
||||
|
|
@ -73,8 +78,8 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
if instance is None:
|
||||
continue
|
||||
|
||||
instance.data["frameStart"] = context.data["frameStart"]
|
||||
instance.data["frameEnd"] = context.data["frameEnd"]
|
||||
instance.data["frameStart"] = context.data["sceneMarkIn"] + 1
|
||||
instance.data["frameEnd"] = context.data["sceneMarkOut"] + 1
|
||||
|
||||
self.log.debug("Created instance: {}\n{}".format(
|
||||
instance, json.dumps(instance.data, indent=4)
|
||||
|
|
|
|||
|
|
@ -122,36 +122,26 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
width = int(workfile_info_parts.pop(-1))
|
||||
workfile_path = " ".join(workfile_info_parts).replace("\"", "")
|
||||
|
||||
frame_start, frame_end = self.collect_clip_frames()
|
||||
# Marks return as "{frame - 1} {state} ", example "0 set".
|
||||
result = lib.execute_george("tv_markin")
|
||||
mark_in_frame, mark_in_state, _ = result.split(" ")
|
||||
|
||||
result = lib.execute_george("tv_markout")
|
||||
mark_out_frame, mark_out_state, _ = result.split(" ")
|
||||
|
||||
scene_data = {
|
||||
"currentFile": workfile_path,
|
||||
"sceneWidth": width,
|
||||
"sceneHeight": height,
|
||||
"pixelAspect": pixel_apsect,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"fps": frame_rate,
|
||||
"fieldOrder": field_order
|
||||
"scenePixelAspect": pixel_apsect,
|
||||
"sceneFps": frame_rate,
|
||||
"sceneFieldOrder": field_order,
|
||||
"sceneMarkIn": int(mark_in_frame),
|
||||
"sceneMarkInState": mark_in_state == "set",
|
||||
"sceneMarkOut": int(mark_out_frame),
|
||||
"sceneMarkOutState": mark_out_state == "set"
|
||||
}
|
||||
self.log.debug(
|
||||
"Scene data: {}".format(json.dumps(scene_data, indent=4))
|
||||
)
|
||||
context.data.update(scene_data)
|
||||
|
||||
def collect_clip_frames(self):
|
||||
clip_info_str = lib.execute_george("tv_clipinfo")
|
||||
self.log.debug("Clip info: {}".format(clip_info_str))
|
||||
clip_info_items = clip_info_str.split(" ")
|
||||
# Color index - not used
|
||||
clip_info_items.pop(-1)
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
mark_out = int(clip_info_items.pop(-1))
|
||||
frame_end = mark_out + 1
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
mark_in = int(clip_info_items.pop(-1))
|
||||
frame_start = mark_in + 1
|
||||
clip_info_items.pop(-1)
|
||||
|
||||
return frame_start, frame_end
|
||||
|
|
|
|||
64
openpype/hosts/tvpaint/plugins/publish/validate_marks.py
Normal file
64
openpype/hosts/tvpaint/plugins/publish/validate_marks.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
from avalon.tvpaint import lib
|
||||
|
||||
|
||||
class ValidateMarksRepair(pyblish.api.Action):
|
||||
"""Repair the marks."""
|
||||
|
||||
label = "Repair"
|
||||
icon = "wrench"
|
||||
on = "failed"
|
||||
|
||||
def process(self, context, plugin):
|
||||
expected_data = ValidateMarks.get_expected_data(context)
|
||||
|
||||
expected_data["markIn"] -= 1
|
||||
expected_data["markOut"] -= 1
|
||||
|
||||
lib.execute_george("tv_markin {} set".format(expected_data["markIn"]))
|
||||
lib.execute_george(
|
||||
"tv_markout {} set".format(expected_data["markOut"])
|
||||
)
|
||||
|
||||
|
||||
class ValidateMarks(pyblish.api.ContextPlugin):
|
||||
"""Validate mark in and out are enabled."""
|
||||
|
||||
label = "Validate Marks"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
optional = True
|
||||
actions = [ValidateMarksRepair]
|
||||
|
||||
@staticmethod
|
||||
def get_expected_data(context):
|
||||
return {
|
||||
"markIn": int(context.data["frameStart"]),
|
||||
"markInState": True,
|
||||
"markOut": int(context.data["frameEnd"]),
|
||||
"markOutState": True
|
||||
}
|
||||
|
||||
def process(self, context):
|
||||
current_data = {
|
||||
"markIn": context.data["sceneMarkIn"] + 1,
|
||||
"markInState": context.data["sceneMarkInState"],
|
||||
"markOut": context.data["sceneMarkOut"] + 1,
|
||||
"markOutState": context.data["sceneMarkOutState"]
|
||||
}
|
||||
expected_data = self.get_expected_data(context)
|
||||
invalid = {}
|
||||
for k in current_data.keys():
|
||||
if current_data[k] != expected_data[k]:
|
||||
invalid[k] = {
|
||||
"current": current_data[k],
|
||||
"expected": expected_data[k]
|
||||
}
|
||||
|
||||
if invalid:
|
||||
raise AssertionError(
|
||||
"Marks does not match database:\n{}".format(
|
||||
json.dumps(invalid, sort_keys=True, indent=4)
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateProjectSettings(pyblish.api.ContextPlugin):
|
||||
"""Validate project settings against database.
|
||||
"""
|
||||
|
||||
label = "Validate Project Settings"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
scene_data = {
|
||||
"fps": context.data.get("sceneFps"),
|
||||
"resolutionWidth": context.data.get("sceneWidth"),
|
||||
"resolutionHeight": context.data.get("sceneHeight"),
|
||||
"pixelAspect": context.data.get("scenePixelAspect")
|
||||
}
|
||||
invalid = {}
|
||||
for k in scene_data.keys():
|
||||
expected_value = context.data["assetEntity"]["data"][k]
|
||||
if scene_data[k] != expected_value:
|
||||
invalid[k] = {
|
||||
"current": scene_data[k], "expected": expected_value
|
||||
}
|
||||
|
||||
if invalid:
|
||||
raise AssertionError(
|
||||
"Project settings does not match database:\n{}".format(
|
||||
json.dumps(invalid, sort_keys=True, indent=4)
|
||||
)
|
||||
)
|
||||
0
openpype/hosts/unreal/__init__.py
Normal file
0
openpype/hosts/unreal/__init__.py
Normal file
|
|
@ -23,8 +23,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
|
|||
def execute(self):
|
||||
asset_name = self.data["asset_name"]
|
||||
task_name = self.data["task_name"]
|
||||
workdir = self.env["AVALON_WORKDIR"]
|
||||
engine_version = self.app_name.split("_")[-1]
|
||||
workdir = self.launch_context.env["AVALON_WORKDIR"]
|
||||
engine_version = self.app_name.split("_")[-1].replace("-", ".")
|
||||
unreal_project_name = f"{asset_name}_{task_name}"
|
||||
|
||||
# Unreal is sensitive about project names longer then 20 chars
|
||||
|
|
@ -81,8 +81,8 @@ class UnrealPrelaunchHook(PreLaunchHook):
|
|||
# Set "AVALON_UNREAL_PLUGIN" to current process environment for
|
||||
# execution of `create_unreal_project`
|
||||
env_key = "AVALON_UNREAL_PLUGIN"
|
||||
if self.env.get(env_key):
|
||||
os.environ[env_key] = self.env[env_key]
|
||||
if self.launch_context.env.get(env_key):
|
||||
os.environ[env_key] = self.launch_context.env[env_key]
|
||||
|
||||
unreal_lib.create_unreal_project(
|
||||
unreal_project_name,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import site
|
|||
# add Python version specific vendor folder
|
||||
site.addsitedir(
|
||||
os.path.join(
|
||||
os.getenv("OPENPYPE_ROOT", ""),
|
||||
os.getenv("OPENPYPE_REPOS_ROOT", ""),
|
||||
"vendor", "python", "python_{}".format(sys.version[0])))
|
||||
|
||||
from .terminal import Terminal
|
||||
|
|
@ -104,7 +104,8 @@ from .plugin_tools import (
|
|||
from .local_settings import (
|
||||
IniSettingRegistry,
|
||||
JSONSettingRegistry,
|
||||
PypeSettingsRegistry,
|
||||
OpenPypeSecureRegistry,
|
||||
OpenPypeSettingsRegistry,
|
||||
get_local_site_id,
|
||||
change_openpype_mongo_url
|
||||
)
|
||||
|
|
@ -217,7 +218,8 @@ __all__ = [
|
|||
|
||||
"IniSettingRegistry",
|
||||
"JSONSettingRegistry",
|
||||
"PypeSettingsRegistry",
|
||||
"OpenPypeSecureRegistry",
|
||||
"OpenPypeSettingsRegistry",
|
||||
"get_local_site_id",
|
||||
"change_openpype_mongo_url",
|
||||
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ class Anatomy:
|
|||
"""Returns value of root key from template."""
|
||||
root_templates = []
|
||||
for group in re.findall(self.root_key_regex, template):
|
||||
root_templates.append(group)
|
||||
root_templates.append("{" + group + "}")
|
||||
|
||||
if not root_templates:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@ from openpype.settings import (
|
|||
get_project_settings,
|
||||
get_environments
|
||||
)
|
||||
from openpype.settings.constants import (
|
||||
METADATA_KEYS,
|
||||
M_DYNAMIC_KEY_LABEL
|
||||
)
|
||||
from . import (
|
||||
PypeLogger,
|
||||
Anatomy
|
||||
|
|
@ -72,7 +76,7 @@ class ApplictionExecutableNotFound(Exception):
|
|||
for executable in application.executables:
|
||||
details += "\n- " + executable.executable_path
|
||||
|
||||
self.msg = msg.format(application.full_label, application.name)
|
||||
self.msg = msg.format(application.full_label, application.full_name)
|
||||
self.details = details
|
||||
|
||||
exc_mgs = str(self.msg)
|
||||
|
|
@ -123,7 +127,16 @@ class ApplicationGroup:
|
|||
self.host_name = host_name
|
||||
|
||||
variants = data.get("variants") or {}
|
||||
key_label_mapping = variants.pop(M_DYNAMIC_KEY_LABEL, {})
|
||||
for variant_name, variant_data in variants.items():
|
||||
if variant_name in METADATA_KEYS:
|
||||
continue
|
||||
|
||||
if "variant_label" not in variant_data:
|
||||
variant_label = key_label_mapping.get(variant_name)
|
||||
if variant_label:
|
||||
variant_data["variant_label"] = variant_label
|
||||
|
||||
variants[variant_name] = Application(
|
||||
variant_name, variant_data, self
|
||||
)
|
||||
|
|
@ -165,7 +178,7 @@ class Application:
|
|||
enabled = False
|
||||
if group.enabled:
|
||||
enabled = data.get("enabled", True)
|
||||
self.enabled = enabled
|
||||
self.enabled = enabled
|
||||
|
||||
self.label = data.get("variant_label") or name
|
||||
self.full_name = "/".join((group.name, name))
|
||||
|
|
@ -244,7 +257,7 @@ class Application:
|
|||
Returns:
|
||||
subprocess.Popen: Return executed process as Popen object.
|
||||
"""
|
||||
return self.manager.launch(self.name, *args, **kwargs)
|
||||
return self.manager.launch(self.full_name, *args, **kwargs)
|
||||
|
||||
|
||||
class ApplicationManager:
|
||||
|
|
@ -265,22 +278,31 @@ class ApplicationManager:
|
|||
self.tool_groups.clear()
|
||||
self.tools.clear()
|
||||
|
||||
settings = get_system_settings()
|
||||
settings = get_system_settings(
|
||||
clear_metadata=False, exclude_locals=False
|
||||
)
|
||||
|
||||
app_defs = settings["applications"]
|
||||
for group_name, variant_defs in app_defs.items():
|
||||
if group_name in METADATA_KEYS:
|
||||
continue
|
||||
|
||||
group = ApplicationGroup(group_name, variant_defs, self)
|
||||
self.app_groups[group_name] = group
|
||||
for app in group:
|
||||
# TODO This should be replaced with `full_name` in future
|
||||
self.applications[app.name] = app
|
||||
self.applications[app.full_name] = app
|
||||
|
||||
tools_definitions = settings["tools"]["tool_groups"]
|
||||
tool_label_mapping = tools_definitions.pop(M_DYNAMIC_KEY_LABEL, {})
|
||||
for tool_group_name, tool_group_data in tools_definitions.items():
|
||||
if not tool_group_name:
|
||||
if not tool_group_name or tool_group_name in METADATA_KEYS:
|
||||
continue
|
||||
|
||||
tool_group_label = (
|
||||
tool_label_mapping.get(tool_group_name) or tool_group_name
|
||||
)
|
||||
group = EnvironmentToolGroup(
|
||||
tool_group_name, tool_group_data, self
|
||||
tool_group_name, tool_group_label, tool_group_data, self
|
||||
)
|
||||
self.tool_groups[tool_group_name] = group
|
||||
for tool in group:
|
||||
|
|
@ -336,16 +358,24 @@ class EnvironmentToolGroup:
|
|||
manager (ApplicationManager): Manager that creates the group.
|
||||
"""
|
||||
|
||||
def __init__(self, name, data, manager):
|
||||
def __init__(self, name, label, data, manager):
|
||||
self.name = name
|
||||
self.label = label
|
||||
self._data = data
|
||||
self.manager = manager
|
||||
self._environment = data["environment"]
|
||||
|
||||
variants = data.get("variants") or {}
|
||||
label_by_key = variants.pop(M_DYNAMIC_KEY_LABEL, {})
|
||||
variants_by_name = {}
|
||||
for variant_name, variant_env in variants.items():
|
||||
tool = EnvironmentTool(variant_name, variant_env, self)
|
||||
if variant_name in METADATA_KEYS:
|
||||
continue
|
||||
|
||||
variant_label = label_by_key.get(variant_name) or variant_name
|
||||
tool = EnvironmentTool(
|
||||
variant_name, variant_label, variant_env, self
|
||||
)
|
||||
variants_by_name[variant_name] = tool
|
||||
self.variants = variants_by_name
|
||||
|
||||
|
|
@ -372,8 +402,10 @@ class EnvironmentTool:
|
|||
group (str): Name of group which wraps tool.
|
||||
"""
|
||||
|
||||
def __init__(self, name, environment, group):
|
||||
def __init__(self, name, label, environment, group):
|
||||
self.name = name
|
||||
self.variant_label = label
|
||||
self.label = " ".join((group.label, label))
|
||||
self.group = group
|
||||
self._environment = environment
|
||||
self.full_name = "/".join((group.name, name))
|
||||
|
|
@ -502,7 +534,7 @@ class LaunchHook:
|
|||
|
||||
@property
|
||||
def app_name(self):
|
||||
return getattr(self.application, "name", None)
|
||||
return getattr(self.application, "full_name", None)
|
||||
|
||||
def validate(self):
|
||||
"""Optional validation of launch hook on initialization.
|
||||
|
|
@ -804,10 +836,15 @@ class ApplicationLaunchContext:
|
|||
self.log.debug("All prelaunch hook executed. Starting new process.")
|
||||
|
||||
# Prepare subprocess args
|
||||
args = self.clear_launch_args(self.launch_args)
|
||||
self.log.debug(
|
||||
"Launching \"{}\" with args ({}): {}".format(
|
||||
self.app_name, len(args), args
|
||||
args_len_str = ""
|
||||
if isinstance(self.launch_args, str):
|
||||
args = self.launch_args
|
||||
else:
|
||||
args = self.clear_launch_args(self.launch_args)
|
||||
args_len_str = " ({})".format(len(args))
|
||||
self.log.info(
|
||||
"Launching \"{}\" with args{}: {}".format(
|
||||
self.app_name, args_len_str, args
|
||||
)
|
||||
)
|
||||
# Run process
|
||||
|
|
@ -853,7 +890,10 @@ class ApplicationLaunchContext:
|
|||
Return:
|
||||
list: Unpacked arguments.
|
||||
"""
|
||||
while True:
|
||||
if isinstance(args, str):
|
||||
return args
|
||||
all_cleared = False
|
||||
while not all_cleared:
|
||||
all_cleared = True
|
||||
new_args = []
|
||||
for arg in args:
|
||||
|
|
@ -865,8 +905,6 @@ class ApplicationLaunchContext:
|
|||
new_args.append(arg)
|
||||
args = new_args
|
||||
|
||||
if all_cleared:
|
||||
break
|
||||
return args
|
||||
|
||||
|
||||
|
|
@ -939,7 +977,7 @@ def get_app_environments_for_context(
|
|||
"project_name": project_name,
|
||||
"asset_name": asset_name,
|
||||
"task_name": task_name,
|
||||
"app_name": app_name,
|
||||
|
||||
"app": app,
|
||||
|
||||
"dbcon": dbcon,
|
||||
|
|
@ -1117,8 +1155,7 @@ def prepare_context_environments(data):
|
|||
"AVALON_ASSET": asset_doc["name"],
|
||||
"AVALON_TASK": task_name,
|
||||
"AVALON_APP": app.host_name,
|
||||
# TODO this hould be `app.full_name` in future PRs
|
||||
"AVALON_APP_NAME": app.name,
|
||||
"AVALON_APP_NAME": app.full_name,
|
||||
"AVALON_WORKDIR": workdir
|
||||
}
|
||||
log.debug(
|
||||
|
|
|
|||
|
|
@ -1123,6 +1123,7 @@ class BuildWorkfile:
|
|||
return output
|
||||
|
||||
|
||||
@with_avalon
|
||||
def get_creator_by_name(creator_name, case_sensitive=False):
|
||||
"""Find creator plugin by name.
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ log = Logger().get_logger(__name__)
|
|||
|
||||
def discover_host_vendor_module(module_name):
|
||||
host = os.environ["AVALON_APP"]
|
||||
pype_root = os.environ["OPENPYPE_ROOT"]
|
||||
pype_root = os.environ["OPENPYPE_REPOS_ROOT"]
|
||||
main_module = module_name.split(".")[0]
|
||||
module_path = os.path.join(
|
||||
pype_root, "hosts", host, "vendor", main_module)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from datetime import datetime
|
|||
from abc import ABCMeta, abstractmethod
|
||||
import json
|
||||
|
||||
# TODO Use pype igniter logic instead of using duplicated code
|
||||
# disable lru cache in Python 2
|
||||
try:
|
||||
from functools import lru_cache
|
||||
|
|
@ -25,11 +26,115 @@ except ImportError:
|
|||
|
||||
import platform
|
||||
|
||||
import appdirs
|
||||
import six
|
||||
import appdirs
|
||||
|
||||
from .import validate_mongo_connection
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
|
||||
|
||||
class OpenPypeSecureRegistry:
|
||||
"""Store information using keyring.
|
||||
|
||||
Registry should be used for private data that should be available only for
|
||||
user.
|
||||
|
||||
All passed registry names will have added prefix `OpenPype/` to easier
|
||||
identify which data were created by OpenPype.
|
||||
|
||||
Args:
|
||||
name(str): Name of registry used as identifier for data.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
try:
|
||||
import keyring
|
||||
|
||||
except Exception:
|
||||
raise NotImplementedError(
|
||||
"Python module `keyring` is not available."
|
||||
)
|
||||
|
||||
# hack for cx_freeze and Windows keyring backend
|
||||
if platform.system().lower() == "windows":
|
||||
from keyring.backends import Windows
|
||||
|
||||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
# Force "OpenPype" prefix
|
||||
self._name = "/".join(("OpenPype", name))
|
||||
|
||||
def set_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set sensitive item into system's keyring.
|
||||
|
||||
This uses `Keyring module`_ to save sensitive stuff into system's
|
||||
keyring.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
keyring.set_password(self._name, name, value)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_item(self, name, default=_PLACEHOLDER):
|
||||
"""Get value of sensitive item from system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
default (Any): Default value if item is not available.
|
||||
|
||||
Returns:
|
||||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist and default is not defined.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
value = keyring.get_password(self._name, name)
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
if default is not _PLACEHOLDER:
|
||||
return default
|
||||
|
||||
# NOTE Should raise `KeyError`
|
||||
raise ValueError(
|
||||
"Item {}:{} does not exist in keyring.".format(self._name, name)
|
||||
)
|
||||
|
||||
def delete_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete value stored in system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item to be deleted.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
import keyring
|
||||
|
||||
self.get_item.cache_clear()
|
||||
keyring.delete_password(self._name, name)
|
||||
|
||||
|
||||
@six.add_metaclass(ABCMeta)
|
||||
class ASettingRegistry():
|
||||
|
|
@ -48,13 +153,6 @@ class ASettingRegistry():
|
|||
# type: (str) -> ASettingRegistry
|
||||
super(ASettingRegistry, self).__init__()
|
||||
|
||||
if six.PY3:
|
||||
import keyring
|
||||
# hack for cx_freeze and Windows keyring backend
|
||||
if platform.system() == "Windows":
|
||||
from keyring.backends import Windows
|
||||
keyring.set_keyring(Windows.WinVaultKeyring())
|
||||
|
||||
self._name = name
|
||||
self._items = {}
|
||||
|
||||
|
|
@ -120,7 +218,7 @@ class ASettingRegistry():
|
|||
"""Delete item from settings.
|
||||
|
||||
Note:
|
||||
see :meth:`pype.lib.local_settings.ARegistrySettings.delete_item`
|
||||
see :meth:`openpype.lib.user_settings.ARegistrySettings.delete_item`
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
@ -129,78 +227,6 @@ class ASettingRegistry():
|
|||
del self._items[name]
|
||||
self._delete_item(name)
|
||||
|
||||
def set_secure_item(self, name, value):
|
||||
# type: (str, str) -> None
|
||||
"""Set sensitive item into system's keyring.
|
||||
|
||||
This uses `Keyring module`_ to save sensitive stuff into system's
|
||||
keyring.
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
value (str): Value of the item.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
keyring.set_password(self._name, name, value)
|
||||
|
||||
@lru_cache(maxsize=32)
|
||||
def get_secure_item(self, name):
|
||||
# type: (str) -> str
|
||||
"""Get value of sensitive item from system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item.
|
||||
|
||||
Returns:
|
||||
value (str): Value of the item.
|
||||
|
||||
Raises:
|
||||
ValueError: If item doesn't exist.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
value = keyring.get_password(self._name, name)
|
||||
if not value:
|
||||
raise ValueError(
|
||||
"Item {}:{} does not exist in keyring.".format(
|
||||
self._name, name))
|
||||
return value
|
||||
|
||||
def delete_secure_item(self, name):
|
||||
# type: (str) -> None
|
||||
"""Delete value stored in system's keyring.
|
||||
|
||||
See also `Keyring module`_
|
||||
|
||||
Args:
|
||||
name (str): Name of the item to be deleted.
|
||||
|
||||
.. _Keyring module:
|
||||
https://github.com/jaraco/keyring
|
||||
|
||||
"""
|
||||
if six.PY2:
|
||||
raise NotImplementedError(
|
||||
"Keyring not available on Python 2 hosts")
|
||||
import keyring
|
||||
self.get_secure_item.cache_clear()
|
||||
keyring.delete_password(self._name, name)
|
||||
|
||||
|
||||
class IniSettingRegistry(ASettingRegistry):
|
||||
"""Class using :mod:`configparser`.
|
||||
|
|
@ -218,7 +244,7 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
if not os.path.exists(self._registry_file):
|
||||
with open(self._registry_file, mode="w") as cfg:
|
||||
print("# Settings registry", cfg)
|
||||
print("# Generated by Pype {}".format(version), cfg)
|
||||
print("# Generated by OpenPype {}".format(version), cfg)
|
||||
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
print("# {}".format(now), cfg)
|
||||
|
||||
|
|
@ -352,7 +378,7 @@ class IniSettingRegistry(ASettingRegistry):
|
|||
"""Delete item from default section.
|
||||
|
||||
Note:
|
||||
See :meth:`~pype.lib.IniSettingsRegistry.delete_item_from_section`
|
||||
See :meth:`~openpype.lib.IniSettingsRegistry.delete_item_from_section`
|
||||
|
||||
"""
|
||||
self.delete_item_from_section("MAIN", name)
|
||||
|
|
@ -369,7 +395,7 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
now = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
|
||||
header = {
|
||||
"__metadata__": {
|
||||
"pype-version": os.getenv("OPENPYPE_VERSION", "N/A"),
|
||||
"openpype-version": os.getenv("OPENPYPE_VERSION", "N/A"),
|
||||
"generated": now
|
||||
},
|
||||
"registry": {}
|
||||
|
|
@ -387,7 +413,7 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
"""Get item value from registry json.
|
||||
|
||||
Note:
|
||||
See :meth:`pype.lib.JSONSettingRegistry.get_item`
|
||||
See :meth:`openpype.lib.JSONSettingRegistry.get_item`
|
||||
|
||||
"""
|
||||
with open(self._registry_file, mode="r") as cfg:
|
||||
|
|
@ -420,7 +446,7 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
"""Set item value to registry json.
|
||||
|
||||
Note:
|
||||
See :meth:`pype.lib.JSONSettingRegistry.set_item`
|
||||
See :meth:`openpype.lib.JSONSettingRegistry.set_item`
|
||||
|
||||
"""
|
||||
with open(self._registry_file, "r+") as cfg:
|
||||
|
|
@ -452,8 +478,8 @@ class JSONSettingRegistry(ASettingRegistry):
|
|||
json.dump(data, cfg, indent=4)
|
||||
|
||||
|
||||
class PypeSettingsRegistry(JSONSettingRegistry):
|
||||
"""Class handling Pype general settings registry.
|
||||
class OpenPypeSettingsRegistry(JSONSettingRegistry):
|
||||
"""Class handling OpenPype general settings registry.
|
||||
|
||||
Attributes:
|
||||
vendor (str): Name used for path construction.
|
||||
|
|
@ -461,21 +487,23 @@ class PypeSettingsRegistry(JSONSettingRegistry):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, name=None):
|
||||
self.vendor = "pypeclub"
|
||||
self.product = "pype"
|
||||
self.product = "openpype"
|
||||
if not name:
|
||||
name = "openpype_settings"
|
||||
path = appdirs.user_data_dir(self.product, self.vendor)
|
||||
super(PypeSettingsRegistry, self).__init__("pype_settings", path)
|
||||
super(OpenPypeSettingsRegistry, self).__init__(name, path)
|
||||
|
||||
|
||||
def _create_local_site_id(registry=None):
|
||||
"""Create a local site identifier."""
|
||||
from uuid import uuid4
|
||||
from coolname import generate_slug
|
||||
|
||||
if registry is None:
|
||||
registry = PypeSettingsRegistry()
|
||||
registry = OpenPypeSettingsRegistry()
|
||||
|
||||
new_id = str(uuid4())
|
||||
new_id = generate_slug(3)
|
||||
|
||||
print("Created local site id \"{}\"".format(new_id))
|
||||
|
||||
|
|
@ -489,7 +517,7 @@ def get_local_site_id():
|
|||
|
||||
Identifier is created if does not exists yet.
|
||||
"""
|
||||
registry = PypeSettingsRegistry()
|
||||
registry = OpenPypeSettingsRegistry()
|
||||
try:
|
||||
return registry.get_item("localId")
|
||||
except ValueError:
|
||||
|
|
@ -504,5 +532,9 @@ def change_openpype_mongo_url(new_mongo_url):
|
|||
"""
|
||||
|
||||
validate_mongo_connection(new_mongo_url)
|
||||
registry = PypeSettingsRegistry()
|
||||
registry.set_secure_item("pypeMongo", new_mongo_url)
|
||||
key = "openPypeMongo"
|
||||
registry = OpenPypeSecureRegistry("mongodb")
|
||||
existing_value = registry.get_item(key, None)
|
||||
if existing_value is not None:
|
||||
registry.delete_item(key)
|
||||
registry.set_item(key, new_mongo_url)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def get_pype_info():
|
|||
"version": get_pype_version(),
|
||||
"version_type": version_type,
|
||||
"executable": executable_args[-1],
|
||||
"pype_root": os.environ["OPENPYPE_ROOT"],
|
||||
"pype_root": os.environ["OPENPYPE_REPOS_ROOT"],
|
||||
"mongo_url": os.environ["OPENPYPE_MONGO"]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ from .log_viewer import LogViewModule
|
|||
from .muster import MusterModule
|
||||
from .deadline import DeadlineModule
|
||||
from .standalonepublish_action import StandAlonePublishAction
|
||||
from .sync_server import SyncServer
|
||||
from .sync_server import SyncServerModule
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -82,5 +82,5 @@ __all__ = (
|
|||
"DeadlineModule",
|
||||
"StandAlonePublishAction",
|
||||
|
||||
"SyncServer"
|
||||
"SyncServerModule"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import os
|
||||
import re
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import datetime
|
||||
import requests
|
||||
from .constants import (
|
||||
CLOCKIFY_ENDPOINT, ADMIN_PERMISSION_NAMES, CREDENTIALS_JSON_PATH
|
||||
CLOCKIFY_ENDPOINT,
|
||||
ADMIN_PERMISSION_NAMES
|
||||
)
|
||||
|
||||
from openpype.lib.local_settings import OpenPypeSecureRegistry
|
||||
|
||||
|
||||
def time_check(obj):
|
||||
if obj.request_counter < 10:
|
||||
|
|
@ -31,6 +34,8 @@ class ClockifyAPI:
|
|||
self.request_counter = 0
|
||||
self.request_time = time.time()
|
||||
|
||||
self.secure_registry = OpenPypeSecureRegistry("clockify")
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return {"X-Api-Key": self.api_key}
|
||||
|
|
@ -129,22 +134,10 @@ class ClockifyAPI:
|
|||
return False
|
||||
|
||||
def get_api_key(self):
|
||||
api_key = None
|
||||
try:
|
||||
file = open(CREDENTIALS_JSON_PATH, 'r')
|
||||
api_key = json.load(file).get('api_key', None)
|
||||
if api_key == '':
|
||||
api_key = None
|
||||
except Exception:
|
||||
file = open(CREDENTIALS_JSON_PATH, 'w')
|
||||
file.close()
|
||||
return api_key
|
||||
return self.secure_registry.get_item("api_key", None)
|
||||
|
||||
def save_api_key(self, api_key):
|
||||
data = {'api_key': api_key}
|
||||
file = open(CREDENTIALS_JSON_PATH, 'w')
|
||||
file.write(json.dumps(data))
|
||||
file.close()
|
||||
self.secure_registry.set_item("api_key", api_key)
|
||||
|
||||
def get_workspaces(self):
|
||||
action_url = 'workspaces/'
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import os
|
||||
import appdirs
|
||||
|
||||
|
||||
CLOCKIFY_FTRACK_SERVER_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "ftrack", "server"
|
||||
os.path.dirname(os.path.abspath(__file__)), "ftrack", "server"
|
||||
)
|
||||
CLOCKIFY_FTRACK_USER_PATH = os.path.join(
|
||||
os.path.dirname(__file__), "ftrack", "user"
|
||||
os.path.dirname(os.path.abspath(__file__)), "ftrack", "user"
|
||||
)
|
||||
CREDENTIALS_JSON_PATH = os.path.normpath(os.path.join(
|
||||
appdirs.user_data_dir("pype-app", "pype"),
|
||||
"clockify.json"
|
||||
))
|
||||
|
||||
ADMIN_PERMISSION_NAMES = ["WORKSPACE_OWN", "WORKSPACE_ADMIN"]
|
||||
CLOCKIFY_ENDPOINT = "https://api.clockify.me/api/"
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ class HarmonySubmitDeadline(
|
|||
def get_job_info(self):
|
||||
job_info = DeadlineJobInfo("Harmony")
|
||||
job_info.Name = self._instance.data["name"]
|
||||
job_info.Plugin = "HarmonyPype"
|
||||
job_info.Plugin = "HarmonyOpenPype"
|
||||
job_info.Frames = "{}-{}".format(
|
||||
self._instance.data["frameStartHandle"],
|
||||
self._instance.data["frameEndHandle"]
|
||||
|
|
@ -102,7 +102,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
|
||||
hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"]
|
||||
|
||||
families = ["render.farm", "prerender",
|
||||
families = ["render.farm", "prerender.farm",
|
||||
"renderlayer", "imagesequence", "vrayscene"]
|
||||
|
||||
aov_filter = {"maya": [r".+(?:\.|_)([Bb]eauty)(?:\.|_).*"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
import json
|
||||
|
||||
from openpype.api import ProjectSettings
|
||||
|
||||
from openpype.modules.ftrack.lib import ServerAction
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
||||
class PrepareProjectServer(ServerAction):
|
||||
"""Prepare project attributes in Anatomy."""
|
||||
|
||||
identifier = "prepare.project.server"
|
||||
label = "OpenPype Admin"
|
||||
variant = "- Prepare Project (Server)"
|
||||
description = "Set basic attributes on the project"
|
||||
|
||||
settings_key = "prepare_project"
|
||||
|
||||
role_list = ["Pypeclub", "Administrator", "Project Manager"]
|
||||
|
||||
# Key to store info about trigerring create folder structure
|
||||
item_splitter = {"type": "label", "value": "---"}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
"""Show only on project."""
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != "project"
|
||||
):
|
||||
return False
|
||||
|
||||
return self.valid_roles(session, entities, event)
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
if event['data'].get('values', {}):
|
||||
return
|
||||
|
||||
# Inform user that this may take a while
|
||||
self.show_message(event, "Preparing data... Please wait", True)
|
||||
self.log.debug("Preparing data which will be shown")
|
||||
|
||||
self.log.debug("Loading custom attributes")
|
||||
|
||||
project_entity = entities[0]
|
||||
project_name = project_entity["full_name"]
|
||||
|
||||
try:
|
||||
project_settings = ProjectSettings(project_name)
|
||||
except ValueError:
|
||||
return {
|
||||
"message": "Project is not synchronized yet",
|
||||
"success": False
|
||||
}
|
||||
|
||||
project_anatom_settings = project_settings["project_anatomy"]
|
||||
root_items = self.prepare_root_items(project_anatom_settings)
|
||||
|
||||
ca_items, multiselect_enumerators = (
|
||||
self.prepare_custom_attribute_items(project_anatom_settings)
|
||||
)
|
||||
|
||||
self.log.debug("Heavy items are ready. Preparing last items group.")
|
||||
|
||||
title = "Prepare Project"
|
||||
items = []
|
||||
|
||||
# Add root items
|
||||
items.extend(root_items)
|
||||
|
||||
items.append(self.item_splitter)
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Set basic Attributes:</h3>"
|
||||
})
|
||||
|
||||
items.extend(ca_items)
|
||||
|
||||
# This item will be last (before enumerators)
|
||||
# - sets value of auto synchronization
|
||||
auto_sync_name = "avalon_auto_sync"
|
||||
auto_sync_value = project_entity["custom_attributes"].get(
|
||||
CUST_ATTR_AUTO_SYNC, False
|
||||
)
|
||||
auto_sync_item = {
|
||||
"name": auto_sync_name,
|
||||
"type": "boolean",
|
||||
"value": auto_sync_value,
|
||||
"label": "AutoSync to Avalon"
|
||||
}
|
||||
# Add autosync attribute
|
||||
items.append(auto_sync_item)
|
||||
|
||||
# Add enumerator items at the end
|
||||
for item in multiselect_enumerators:
|
||||
items.append(item)
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"title": title
|
||||
}
|
||||
|
||||
def prepare_root_items(self, project_anatom_settings):
|
||||
self.log.debug("Root items preparation begins.")
|
||||
|
||||
root_items = []
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Check your Project root settings</h3>"
|
||||
})
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<p><i>NOTE: Roots are <b>crutial</b> for path filling"
|
||||
" (and creating folder structure).</i></p>"
|
||||
)
|
||||
})
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<p><i>WARNING: Do not change roots on running project,"
|
||||
" that <b>will cause workflow issues</b>.</i></p>"
|
||||
)
|
||||
})
|
||||
|
||||
empty_text = "Enter root path here..."
|
||||
|
||||
roots_entity = project_anatom_settings["roots"]
|
||||
for root_name, root_entity in roots_entity.items():
|
||||
root_items.append(self.item_splitter)
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "Root: \"{}\"".format(root_name)
|
||||
})
|
||||
for platform_name, value_entity in root_entity.items():
|
||||
root_items.append({
|
||||
"label": platform_name,
|
||||
"name": "__root__{}__{}".format(root_name, platform_name),
|
||||
"type": "text",
|
||||
"value": value_entity.value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(list(roots_entity.keys()))
|
||||
})
|
||||
|
||||
self.log.debug("Root items preparation ended.")
|
||||
return root_items
|
||||
|
||||
def _attributes_to_set(self, project_anatom_settings):
|
||||
attributes_to_set = {}
|
||||
|
||||
attribute_values_by_key = {}
|
||||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
key = attr["key"]
|
||||
if key.startswith("avalon_"):
|
||||
continue
|
||||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
for attr in cust_attrs:
|
||||
if attr["entity_type"].lower() != "show":
|
||||
continue
|
||||
key = attr["key"]
|
||||
if key.startswith("avalon_"):
|
||||
continue
|
||||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
# Sort by label
|
||||
attributes_to_set = dict(sorted(
|
||||
attributes_to_set.items(),
|
||||
key=lambda x: x[1]["label"]
|
||||
))
|
||||
return attributes_to_set
|
||||
|
||||
def prepare_custom_attribute_items(self, project_anatom_settings):
|
||||
items = []
|
||||
multiselect_enumerators = []
|
||||
attributes_to_set = self._attributes_to_set(project_anatom_settings)
|
||||
|
||||
self.log.debug("Preparing interface for keys: \"{}\"".format(
|
||||
str([key for key in attributes_to_set])
|
||||
))
|
||||
|
||||
for key, in_data in attributes_to_set.items():
|
||||
attr = in_data["object"]
|
||||
|
||||
# initial item definition
|
||||
item = {
|
||||
"name": key,
|
||||
"label": in_data["label"]
|
||||
}
|
||||
|
||||
# cust attr type - may have different visualization
|
||||
type_name = attr["type"]["name"].lower()
|
||||
easy_types = ["text", "boolean", "date", "number"]
|
||||
|
||||
easy_type = False
|
||||
if type_name in easy_types:
|
||||
easy_type = True
|
||||
|
||||
elif type_name == "enumerator":
|
||||
|
||||
attr_config = json.loads(attr["config"])
|
||||
attr_config_data = json.loads(attr_config["data"])
|
||||
|
||||
if attr_config["multiSelect"] is True:
|
||||
multiselect_enumerators.append(self.item_splitter)
|
||||
multiselect_enumerators.append({
|
||||
"type": "label",
|
||||
"value": in_data["label"]
|
||||
})
|
||||
|
||||
default = in_data["default"]
|
||||
names = []
|
||||
for option in sorted(
|
||||
attr_config_data, key=lambda x: x["menu"]
|
||||
):
|
||||
name = option["value"]
|
||||
new_name = "__{}__{}".format(key, name)
|
||||
names.append(new_name)
|
||||
item = {
|
||||
"name": new_name,
|
||||
"type": "boolean",
|
||||
"label": "- {}".format(option["menu"])
|
||||
}
|
||||
if default:
|
||||
if isinstance(default, (list, tuple)):
|
||||
if name in default:
|
||||
item["value"] = True
|
||||
else:
|
||||
if name == default:
|
||||
item["value"] = True
|
||||
|
||||
multiselect_enumerators.append(item)
|
||||
|
||||
multiselect_enumerators.append({
|
||||
"type": "hidden",
|
||||
"name": "__hidden__{}".format(key),
|
||||
"value": json.dumps(names)
|
||||
})
|
||||
else:
|
||||
easy_type = True
|
||||
item["data"] = attr_config_data
|
||||
|
||||
else:
|
||||
self.log.warning((
|
||||
"Custom attribute \"{}\" has type \"{}\"."
|
||||
" I don't know how to handle"
|
||||
).format(key, type_name))
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"!!! Can't handle Custom attritubte type \"{}\""
|
||||
" (key: \"{}\")"
|
||||
).format(type_name, key)
|
||||
})
|
||||
|
||||
if easy_type:
|
||||
item["type"] = type_name
|
||||
|
||||
# default value in interface
|
||||
default = in_data["default"]
|
||||
if default is not None:
|
||||
item["value"] = default
|
||||
|
||||
items.append(item)
|
||||
|
||||
return items, multiselect_enumerators
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
if not event['data'].get('values', {}):
|
||||
return
|
||||
|
||||
in_data = event['data']['values']
|
||||
|
||||
root_values = {}
|
||||
root_key = "__root__"
|
||||
for key in tuple(in_data.keys()):
|
||||
if key.startswith(root_key):
|
||||
_key = key[len(root_key):]
|
||||
root_values[_key] = in_data.pop(key)
|
||||
|
||||
root_names = in_data.pop("__rootnames__", None)
|
||||
root_data = {}
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
prefix = "{}__".format(root_name)
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
_key = key[len(prefix):]
|
||||
root_data[root_name][_key] = value
|
||||
|
||||
# Find hidden items for multiselect enumerators
|
||||
keys_to_process = []
|
||||
for key in in_data:
|
||||
if key.startswith("__hidden__"):
|
||||
keys_to_process.append(key)
|
||||
|
||||
self.log.debug("Preparing data for Multiselect Enumerators")
|
||||
enumerators = {}
|
||||
for key in keys_to_process:
|
||||
new_key = key.replace("__hidden__", "")
|
||||
enumerator_items = in_data.pop(key)
|
||||
enumerators[new_key] = json.loads(enumerator_items)
|
||||
|
||||
# find values set for multiselect enumerator
|
||||
for key, enumerator_items in enumerators.items():
|
||||
in_data[key] = []
|
||||
|
||||
name = "__{}__".format(key)
|
||||
|
||||
for item in enumerator_items:
|
||||
value = in_data.pop(item)
|
||||
if value is True:
|
||||
new_key = item.replace(name, "")
|
||||
in_data[key].append(new_key)
|
||||
|
||||
self.log.debug("Setting Custom Attribute values")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_settings = ProjectSettings(project_name)
|
||||
project_anatomy_settings = project_settings["project_anatomy"]
|
||||
project_anatomy_settings["roots"] = root_data
|
||||
|
||||
custom_attribute_values = {}
|
||||
attributes_entity = project_anatomy_settings["attributes"]
|
||||
for key, value in in_data.items():
|
||||
if key not in attributes_entity:
|
||||
custom_attribute_values[key] = value
|
||||
else:
|
||||
attributes_entity[key] = value
|
||||
|
||||
project_settings.save()
|
||||
|
||||
entity = entities[0]
|
||||
for key, value in custom_attribute_values.items():
|
||||
entity["custom_attributes"][key] = value
|
||||
self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register(session):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
PrepareProjectServer(session).register()
|
||||
|
|
@ -400,9 +400,9 @@ class CustomAttributes(BaseAction):
|
|||
|
||||
def tools_attribute(self, event):
|
||||
tools_data = []
|
||||
for tool_name in self.app_manager.tools.keys():
|
||||
for tool_name, tool in self.app_manager.tools.items():
|
||||
tools_data.append({
|
||||
tool_name: tool_name
|
||||
tool_name: tool.label
|
||||
})
|
||||
|
||||
# Make sure there is at least one item
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from openpype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.api import config, Anatomy
|
||||
from openpype.modules.ftrack.lib.avalon_sync import get_pype_attr
|
||||
from openpype.api import ProjectSettings
|
||||
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseAction,
|
||||
statics_icon
|
||||
)
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
||||
class PrepareProject(BaseAction):
|
||||
'''Edit meta data action.'''
|
||||
class PrepareProjectLocal(BaseAction):
|
||||
"""Prepare project attributes in Anatomy."""
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'prepare.project'
|
||||
#: Action label.
|
||||
label = 'Prepare Project'
|
||||
#: Action description.
|
||||
description = 'Set basic attributes on the project'
|
||||
#: roles that are allowed to register this action
|
||||
identifier = "prepare.project.local"
|
||||
label = "Prepare Project"
|
||||
description = "Set basic attributes on the project"
|
||||
icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg")
|
||||
|
||||
role_list = ["Pypeclub", "Administrator", "Project Manager"]
|
||||
|
||||
settings_key = "prepare_project"
|
||||
|
||||
# Key to store info about trigerring create folder structure
|
||||
create_project_structure_key = "create_folder_structure"
|
||||
item_splitter = {'type': 'label', 'value': '---'}
|
||||
item_splitter = {"type": "label", "value": "---"}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
"""Show only on project."""
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != "project"
|
||||
|
|
@ -44,27 +47,22 @@ class PrepareProject(BaseAction):
|
|||
|
||||
self.log.debug("Loading custom attributes")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_entity = entities[0]
|
||||
project_name = project_entity["full_name"]
|
||||
|
||||
project_defaults = (
|
||||
config.get_presets(project_name)
|
||||
.get("ftrack", {})
|
||||
.get("project_defaults", {})
|
||||
)
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
if not anatomy.roots:
|
||||
try:
|
||||
project_settings = ProjectSettings(project_name)
|
||||
except ValueError:
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"Have issues with loading Roots for project \"{}\"."
|
||||
).format(anatomy.project_name)
|
||||
"message": "Project is not synchronized yet",
|
||||
"success": False
|
||||
}
|
||||
|
||||
root_items = self.prepare_root_items(anatomy)
|
||||
project_anatom_settings = project_settings["project_anatomy"]
|
||||
root_items = self.prepare_root_items(project_anatom_settings)
|
||||
|
||||
ca_items, multiselect_enumerators = (
|
||||
self.prepare_custom_attribute_items(project_defaults)
|
||||
self.prepare_custom_attribute_items(project_anatom_settings)
|
||||
)
|
||||
|
||||
self.log.debug("Heavy items are ready. Preparing last items group.")
|
||||
|
|
@ -74,19 +72,6 @@ class PrepareProject(BaseAction):
|
|||
|
||||
# Add root items
|
||||
items.extend(root_items)
|
||||
items.append(self.item_splitter)
|
||||
|
||||
# Ask if want to trigger Action Create Folder Structure
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Want to create basic Folder Structure?</h3>"
|
||||
})
|
||||
items.append({
|
||||
"name": self.create_project_structure_key,
|
||||
"type": "boolean",
|
||||
"value": False,
|
||||
"label": "Check if Yes"
|
||||
})
|
||||
|
||||
items.append(self.item_splitter)
|
||||
items.append({
|
||||
|
|
@ -99,10 +84,13 @@ class PrepareProject(BaseAction):
|
|||
# This item will be last (before enumerators)
|
||||
# - sets value of auto synchronization
|
||||
auto_sync_name = "avalon_auto_sync"
|
||||
auto_sync_value = project_entity["custom_attributes"].get(
|
||||
CUST_ATTR_AUTO_SYNC, False
|
||||
)
|
||||
auto_sync_item = {
|
||||
"name": auto_sync_name,
|
||||
"type": "boolean",
|
||||
"value": project_defaults.get(auto_sync_name, False),
|
||||
"value": auto_sync_value,
|
||||
"label": "AutoSync to Avalon"
|
||||
}
|
||||
# Add autosync attribute
|
||||
|
|
@ -117,13 +105,10 @@ class PrepareProject(BaseAction):
|
|||
"title": title
|
||||
}
|
||||
|
||||
def prepare_root_items(self, anatomy):
|
||||
root_items = []
|
||||
def prepare_root_items(self, project_anatom_settings):
|
||||
self.log.debug("Root items preparation begins.")
|
||||
|
||||
root_names = anatomy.root_names()
|
||||
roots = anatomy.roots
|
||||
|
||||
root_items = []
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Check your Project root settings</h3>"
|
||||
|
|
@ -143,85 +128,40 @@ class PrepareProject(BaseAction):
|
|||
)
|
||||
})
|
||||
|
||||
default_roots = anatomy.roots
|
||||
while isinstance(default_roots, dict):
|
||||
key = tuple(default_roots.keys())[0]
|
||||
default_roots = default_roots[key]
|
||||
|
||||
empty_text = "Enter root path here..."
|
||||
|
||||
# Root names is None when anatomy templates contain "{root}"
|
||||
all_platforms = ["windows", "linux", "darwin"]
|
||||
if root_names is None:
|
||||
root_items.append(self.item_splitter)
|
||||
# find first possible key
|
||||
for platform in all_platforms:
|
||||
value = default_roots.raw_data.get(platform) or ""
|
||||
root_items.append({
|
||||
"label": platform,
|
||||
"name": "__root__{}".format(platform),
|
||||
"type": "text",
|
||||
"value": value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
return root_items
|
||||
|
||||
root_name_data = {}
|
||||
missing_roots = []
|
||||
for root_name in root_names:
|
||||
root_name_data[root_name] = {}
|
||||
if not isinstance(roots, dict):
|
||||
missing_roots.append(root_name)
|
||||
continue
|
||||
|
||||
root_item = roots.get(root_name)
|
||||
if not root_item:
|
||||
missing_roots.append(root_name)
|
||||
continue
|
||||
|
||||
for platform in all_platforms:
|
||||
root_name_data[root_name][platform] = (
|
||||
root_item.raw_data.get(platform) or ""
|
||||
)
|
||||
|
||||
if missing_roots:
|
||||
default_values = {}
|
||||
for platform in all_platforms:
|
||||
default_values[platform] = (
|
||||
default_roots.raw_data.get(platform) or ""
|
||||
)
|
||||
|
||||
for root_name in missing_roots:
|
||||
root_name_data[root_name] = default_values
|
||||
|
||||
root_names = list(root_name_data.keys())
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(root_names)
|
||||
})
|
||||
|
||||
for root_name, values in root_name_data.items():
|
||||
roots_entity = project_anatom_settings["roots"]
|
||||
for root_name, root_entity in roots_entity.items():
|
||||
root_items.append(self.item_splitter)
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "Root: \"{}\"".format(root_name)
|
||||
})
|
||||
for platform, value in values.items():
|
||||
for platform_name, value_entity in root_entity.items():
|
||||
root_items.append({
|
||||
"label": platform,
|
||||
"name": "__root__{}{}".format(root_name, platform),
|
||||
"label": platform_name,
|
||||
"name": "__root__{}__{}".format(root_name, platform_name),
|
||||
"type": "text",
|
||||
"value": value,
|
||||
"value": value_entity.value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(list(roots_entity.keys()))
|
||||
})
|
||||
|
||||
self.log.debug("Root items preparation ended.")
|
||||
return root_items
|
||||
|
||||
def _attributes_to_set(self, project_defaults):
|
||||
def _attributes_to_set(self, project_anatom_settings):
|
||||
attributes_to_set = {}
|
||||
|
||||
attribute_values_by_key = {}
|
||||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
|
|
@ -231,7 +171,7 @@ class PrepareProject(BaseAction):
|
|||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": project_defaults.get(key)
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
for attr in cust_attrs:
|
||||
|
|
@ -243,7 +183,7 @@ class PrepareProject(BaseAction):
|
|||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": project_defaults.get(key)
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
# Sort by label
|
||||
|
|
@ -253,10 +193,10 @@ class PrepareProject(BaseAction):
|
|||
))
|
||||
return attributes_to_set
|
||||
|
||||
def prepare_custom_attribute_items(self, project_defaults):
|
||||
def prepare_custom_attribute_items(self, project_anatom_settings):
|
||||
items = []
|
||||
multiselect_enumerators = []
|
||||
attributes_to_set = self._attributes_to_set(project_defaults)
|
||||
attributes_to_set = self._attributes_to_set(project_anatom_settings)
|
||||
|
||||
self.log.debug("Preparing interface for keys: \"{}\"".format(
|
||||
str([key for key in attributes_to_set])
|
||||
|
|
@ -363,24 +303,15 @@ class PrepareProject(BaseAction):
|
|||
|
||||
root_names = in_data.pop("__rootnames__", None)
|
||||
root_data = {}
|
||||
if root_names:
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
if key.startswith(root_name):
|
||||
_key = key[len(root_name):]
|
||||
root_data[root_name][_key] = value
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
prefix = "{}__".format(root_name)
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
else:
|
||||
for key, value in root_values.items():
|
||||
root_data[key] = value
|
||||
|
||||
# TODO implement creating of anatomy for new projects
|
||||
# project_name = entities[0]["full_name"]
|
||||
# anatomy = Anatomy(project_name)
|
||||
|
||||
# pop out info about creating project structure
|
||||
create_proj_struct = in_data.pop(self.create_project_structure_key)
|
||||
_key = key[len(prefix):]
|
||||
root_data[root_name][_key] = value
|
||||
|
||||
# Find hidden items for multiselect enumerators
|
||||
keys_to_process = []
|
||||
|
|
@ -407,54 +338,31 @@ class PrepareProject(BaseAction):
|
|||
new_key = item.replace(name, "")
|
||||
in_data[key].append(new_key)
|
||||
|
||||
self.log.debug("Setting Custom Attribute values:")
|
||||
entity = entities[0]
|
||||
self.log.debug("Setting Custom Attribute values")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_settings = ProjectSettings(project_name)
|
||||
project_anatomy_settings = project_settings["project_anatomy"]
|
||||
project_anatomy_settings["roots"] = root_data
|
||||
|
||||
custom_attribute_values = {}
|
||||
attributes_entity = project_anatomy_settings["attributes"]
|
||||
for key, value in in_data.items():
|
||||
if key not in attributes_entity:
|
||||
custom_attribute_values[key] = value
|
||||
else:
|
||||
attributes_entity[key] = value
|
||||
|
||||
project_settings.save()
|
||||
|
||||
entity = entities[0]
|
||||
for key, value in custom_attribute_values.items():
|
||||
entity["custom_attributes"][key] = value
|
||||
self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value))
|
||||
|
||||
session.commit()
|
||||
|
||||
# Create project structure
|
||||
self.create_project_specific_config(entities[0]["full_name"], in_data)
|
||||
|
||||
# Trigger Create Project Structure action
|
||||
if create_proj_struct is True:
|
||||
self.trigger_action("create.project.structure", event)
|
||||
|
||||
return True
|
||||
|
||||
def create_project_specific_config(self, project_name, json_data):
|
||||
self.log.debug("*** Creating project specifig configs ***")
|
||||
project_specific_path = project_overrides_dir_path(project_name)
|
||||
if not os.path.exists(project_specific_path):
|
||||
os.makedirs(project_specific_path)
|
||||
self.log.debug((
|
||||
"Project specific config folder for project \"{}\" created."
|
||||
).format(project_name))
|
||||
|
||||
# Presets ####################################
|
||||
self.log.debug("--- Processing Presets Begins: ---")
|
||||
|
||||
project_defaults_dir = os.path.normpath(os.path.join(
|
||||
project_specific_path, "presets", "ftrack"
|
||||
))
|
||||
project_defaults_path = os.path.normpath(os.path.join(
|
||||
project_defaults_dir, "project_defaults.json"
|
||||
))
|
||||
# Create folder if not exist
|
||||
if not os.path.exists(project_defaults_dir):
|
||||
self.log.debug("Creating Ftrack Presets folder: \"{}\"".format(
|
||||
project_defaults_dir
|
||||
))
|
||||
os.makedirs(project_defaults_dir)
|
||||
|
||||
with open(project_defaults_path, 'w') as file_stream:
|
||||
json.dump(json_data, file_stream, indent=4)
|
||||
|
||||
self.log.debug("*** Creating project specifig configs Finished ***")
|
||||
|
||||
|
||||
def register(session):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
PrepareProject(session).register()
|
||||
PrepareProjectLocal(session).register()
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class FtrackModule(
|
|||
ftrack_settings = settings[self.name]
|
||||
|
||||
self.enabled = ftrack_settings["enabled"]
|
||||
self.ftrack_url = ftrack_settings["ftrack_server"]
|
||||
self.ftrack_url = ftrack_settings["ftrack_server"].strip("/ ")
|
||||
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
server_event_handlers_paths = [
|
||||
|
|
@ -210,3 +210,7 @@ class FtrackModule(
|
|||
|
||||
def tray_exit(self):
|
||||
return self.tray_module.stop_action_server()
|
||||
|
||||
def set_credentials_to_env(self, username, api_key):
|
||||
os.environ["FTRACK_API_USER"] = username or ""
|
||||
os.environ["FTRACK_API_KEY"] = api_key or ""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import time
|
||||
import types
|
||||
import logging
|
||||
import traceback
|
||||
|
|
@ -10,7 +11,6 @@ from openpype.lib import (
|
|||
modules_from_path
|
||||
)
|
||||
|
||||
|
||||
log = PypeLogger.get_logger(__name__)
|
||||
|
||||
"""
|
||||
|
|
@ -120,6 +120,18 @@ class FtrackServer:
|
|||
if not session:
|
||||
session = ftrack_api.Session(auto_connect_event_hub=True)
|
||||
|
||||
# Wait until session has connected event hub
|
||||
if session._auto_connect_event_hub_thread:
|
||||
# Use timeout from session (since ftrack-api 2.1.0)
|
||||
timeout = getattr(session, "request_timeout", 60)
|
||||
started = time.time()
|
||||
while not session.event_hub.connected:
|
||||
if (time.time() - started) > timeout:
|
||||
raise RuntimeError((
|
||||
"Connection to Ftrack was not created in {} seconds"
|
||||
).format(timeout))
|
||||
time.sleep(0.1)
|
||||
|
||||
self.session = session
|
||||
if load_files:
|
||||
if not self.handler_paths:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import sys
|
|||
import logging
|
||||
import getpass
|
||||
import atexit
|
||||
import tempfile
|
||||
import threading
|
||||
import datetime
|
||||
import time
|
||||
import queue
|
||||
import appdirs
|
||||
import pymongo
|
||||
|
||||
import requests
|
||||
|
|
@ -165,7 +165,6 @@ class ProcessEventHub(SocketBaseEventHub):
|
|||
|
||||
def wait(self, duration=None):
|
||||
"""Overriden wait
|
||||
|
||||
Event are loaded from Mongo DB when queue is empty. Handled event is
|
||||
set as processed in Mongo DB.
|
||||
"""
|
||||
|
|
@ -252,7 +251,7 @@ class CustomEventHubSession(ftrack_api.session.Session):
|
|||
self, server_url=None, api_key=None, api_user=None, auto_populate=True,
|
||||
plugin_paths=None, cache=None, cache_key_maker=None,
|
||||
auto_connect_event_hub=False, schema_cache_path=None,
|
||||
plugin_arguments=None, **kwargs
|
||||
plugin_arguments=None, timeout=60, **kwargs
|
||||
):
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
|
@ -331,6 +330,7 @@ class CustomEventHubSession(ftrack_api.session.Session):
|
|||
self._request.auth = ftrack_api.session.SessionAuthentication(
|
||||
self._api_key, self._api_user
|
||||
)
|
||||
self.request_timeout = timeout
|
||||
|
||||
self.auto_populate = auto_populate
|
||||
|
||||
|
|
@ -368,8 +368,9 @@ class CustomEventHubSession(ftrack_api.session.Session):
|
|||
# rebuilding types)?
|
||||
if schema_cache_path is not False:
|
||||
if schema_cache_path is None:
|
||||
schema_cache_path = appdirs.user_cache_dir()
|
||||
schema_cache_path = os.environ.get(
|
||||
'FTRACK_API_SCHEMA_CACHE_PATH', tempfile.gettempdir()
|
||||
'FTRACK_API_SCHEMA_CACHE_PATH', schema_cache_path
|
||||
)
|
||||
|
||||
schema_cache_path = os.path.join(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class PrePython2Support(PreLaunchHook):
|
|||
Path to vendor modules is added to the beggining of PYTHONPATH.
|
||||
"""
|
||||
# There will be needed more granular filtering in future
|
||||
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio"]
|
||||
app_groups = ["maya", "nuke", "nukex", "hiero", "nukestudio", "unreal"]
|
||||
|
||||
def execute(self):
|
||||
# Prepare vendor dir path
|
||||
|
|
|
|||
|
|
@ -891,6 +891,33 @@ class SyncEntitiesFactory:
|
|||
|
||||
self.entities_dict[parent_id]["children"].remove(id)
|
||||
|
||||
def _query_custom_attributes(self, session, conf_ids, entity_ids):
|
||||
output = []
|
||||
# Prepare values to query
|
||||
attributes_joined = join_query_keys(conf_ids)
|
||||
attributes_len = len(conf_ids)
|
||||
chunk_size = int(5000 / attributes_len)
|
||||
for idx in range(0, len(entity_ids), chunk_size):
|
||||
entity_ids_joined = join_query_keys(
|
||||
entity_ids[idx:idx + chunk_size]
|
||||
)
|
||||
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": (
|
||||
"select value, entity_id from ContextCustomAttributeValue "
|
||||
"where entity_id in ({}) and configuration_id in ({})"
|
||||
).format(entity_ids_joined, attributes_joined)
|
||||
}]
|
||||
if hasattr(session, "call"):
|
||||
[result] = session.call(call_expr)
|
||||
else:
|
||||
[result] = session._call(call_expr)
|
||||
|
||||
for item in result["data"]:
|
||||
output.append(item)
|
||||
return output
|
||||
|
||||
def set_cutom_attributes(self):
|
||||
self.log.debug("* Preparing custom attributes")
|
||||
# Get custom attributes and values
|
||||
|
|
@ -1000,31 +1027,13 @@ class SyncEntitiesFactory:
|
|||
copy.deepcopy(prepared_avalon_attr_ca_id)
|
||||
)
|
||||
|
||||
# TODO query custom attributes by entity_id
|
||||
entity_ids_joined = ", ".join([
|
||||
"\"{}\"".format(id) for id in sync_ids
|
||||
])
|
||||
attributes_joined = ", ".join([
|
||||
"\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys()
|
||||
])
|
||||
|
||||
cust_attr_query = (
|
||||
"select value, configuration_id, entity_id"
|
||||
" from ContextCustomAttributeValue"
|
||||
" where entity_id in ({}) and configuration_id in ({})"
|
||||
items = self._query_custom_attributes(
|
||||
self.session,
|
||||
list(attribute_key_by_id.keys()),
|
||||
sync_ids
|
||||
)
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": cust_attr_query.format(
|
||||
entity_ids_joined, attributes_joined
|
||||
)
|
||||
}]
|
||||
if hasattr(self.session, "call"):
|
||||
[values] = self.session.call(call_expr)
|
||||
else:
|
||||
[values] = self.session._call(call_expr)
|
||||
|
||||
for item in values["data"]:
|
||||
for item in items:
|
||||
entity_id = item["entity_id"]
|
||||
attr_id = item["configuration_id"]
|
||||
key = attribute_key_by_id[attr_id]
|
||||
|
|
@ -1106,28 +1115,14 @@ class SyncEntitiesFactory:
|
|||
for key, val in prepare_dict_avalon.items():
|
||||
entity_dict["avalon_attrs"][key] = val
|
||||
|
||||
# Prepare values to query
|
||||
entity_ids_joined = ", ".join([
|
||||
"\"{}\"".format(id) for id in sync_ids
|
||||
])
|
||||
attributes_joined = ", ".join([
|
||||
"\"{}\"".format(attr_id) for attr_id in attribute_key_by_id.keys()
|
||||
])
|
||||
avalon_hier = []
|
||||
call_expr = [{
|
||||
"action": "query",
|
||||
"expression": (
|
||||
"select value, entity_id, configuration_id"
|
||||
" from ContextCustomAttributeValue"
|
||||
" where entity_id in ({}) and configuration_id in ({})"
|
||||
).format(entity_ids_joined, attributes_joined)
|
||||
}]
|
||||
if hasattr(self.session, "call"):
|
||||
[values] = self.session.call(call_expr)
|
||||
else:
|
||||
[values] = self.session._call(call_expr)
|
||||
items = self._query_custom_attributes(
|
||||
self.session,
|
||||
list(attribute_key_by_id.keys()),
|
||||
sync_ids
|
||||
)
|
||||
|
||||
for item in values["data"]:
|
||||
avalon_hier = []
|
||||
for item in items:
|
||||
value = item["value"]
|
||||
# WARNING It is not possible to propage enumerate hierachical
|
||||
# attributes with multiselection 100% right. Unseting all values
|
||||
|
|
@ -1256,19 +1251,21 @@ class SyncEntitiesFactory:
|
|||
if not msg or not items:
|
||||
continue
|
||||
self.report_items["warning"][msg] = items
|
||||
tasks = {}
|
||||
for task_type in task_types:
|
||||
task_type_name = task_type["name"]
|
||||
# Set short name to empty string
|
||||
# QUESTION Maybe better would be to lower and remove spaces
|
||||
# from task type name.
|
||||
tasks[task_type_name] = {
|
||||
"short_name": ""
|
||||
}
|
||||
|
||||
current_project_anatomy_data = get_anatomy_settings(
|
||||
project_name, exclude_locals=True
|
||||
)
|
||||
anatomy_tasks = current_project_anatomy_data["tasks"]
|
||||
tasks = {}
|
||||
default_type_data = {
|
||||
"short_name": ""
|
||||
}
|
||||
for task_type in task_types:
|
||||
task_type_name = task_type["name"]
|
||||
tasks[task_type_name] = copy.deepcopy(
|
||||
anatomy_tasks.get(task_type_name)
|
||||
or default_type_data
|
||||
)
|
||||
|
||||
project_config = {
|
||||
"tasks": tasks,
|
||||
|
|
|
|||
|
|
@ -1,23 +1,16 @@
|
|||
import os
|
||||
import json
|
||||
import ftrack_api
|
||||
import appdirs
|
||||
import getpass
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
CONFIG_PATH = os.path.normpath(appdirs.user_data_dir("pype-app", "pype"))
|
||||
CREDENTIALS_FILE_NAME = "ftrack_cred.json"
|
||||
CREDENTIALS_PATH = os.path.join(CONFIG_PATH, CREDENTIALS_FILE_NAME)
|
||||
CREDENTIALS_FOLDER = os.path.dirname(CREDENTIALS_PATH)
|
||||
from openpype.lib import OpenPypeSecureRegistry
|
||||
|
||||
if not os.path.isdir(CREDENTIALS_FOLDER):
|
||||
os.makedirs(CREDENTIALS_FOLDER)
|
||||
|
||||
USER_GETTER = None
|
||||
USERNAME_KEY = "username"
|
||||
API_KEY_KEY = "api_key"
|
||||
|
||||
|
||||
def get_ftrack_hostname(ftrack_server=None):
|
||||
|
|
@ -30,112 +23,73 @@ def get_ftrack_hostname(ftrack_server=None):
|
|||
return urlparse(ftrack_server).hostname
|
||||
|
||||
|
||||
def get_user():
|
||||
if USER_GETTER:
|
||||
return USER_GETTER()
|
||||
return getpass.getuser()
|
||||
def _get_ftrack_secure_key(hostname, key):
|
||||
"""Secure item key for entered hostname."""
|
||||
return "/".join(("ftrack", hostname, key))
|
||||
|
||||
|
||||
def get_credentials(ftrack_server=None, user=None):
|
||||
credentials = {}
|
||||
if not os.path.exists(CREDENTIALS_PATH):
|
||||
with open(CREDENTIALS_PATH, "w") as file:
|
||||
file.write(json.dumps(credentials))
|
||||
file.close()
|
||||
return credentials
|
||||
|
||||
with open(CREDENTIALS_PATH, "r") as file:
|
||||
content = file.read()
|
||||
|
||||
def get_credentials(ftrack_server=None):
|
||||
hostname = get_ftrack_hostname(ftrack_server)
|
||||
if not user:
|
||||
user = get_user()
|
||||
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
|
||||
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
|
||||
|
||||
content_json = json.loads(content or "{}")
|
||||
credentials = content_json.get(hostname, {}).get(user) or {}
|
||||
username_registry = OpenPypeSecureRegistry(username_name)
|
||||
api_key_registry = OpenPypeSecureRegistry(api_key_name)
|
||||
|
||||
return credentials
|
||||
|
||||
|
||||
def save_credentials(ft_user, ft_api_key, ftrack_server=None, user=None):
|
||||
hostname = get_ftrack_hostname(ftrack_server)
|
||||
if not user:
|
||||
user = get_user()
|
||||
|
||||
with open(CREDENTIALS_PATH, "r") as file:
|
||||
content = file.read()
|
||||
|
||||
content_json = json.loads(content or "{}")
|
||||
if hostname not in content_json:
|
||||
content_json[hostname] = {}
|
||||
|
||||
content_json[hostname][user] = {
|
||||
"username": ft_user,
|
||||
"api_key": ft_api_key
|
||||
return {
|
||||
USERNAME_KEY: username_registry.get_item(USERNAME_KEY, None),
|
||||
API_KEY_KEY: api_key_registry.get_item(API_KEY_KEY, None)
|
||||
}
|
||||
|
||||
# Deprecated keys
|
||||
if "username" in content_json:
|
||||
content_json.pop("username")
|
||||
if "apiKey" in content_json:
|
||||
content_json.pop("apiKey")
|
||||
|
||||
with open(CREDENTIALS_PATH, "w") as file:
|
||||
file.write(json.dumps(content_json, indent=4))
|
||||
|
||||
|
||||
def clear_credentials(ft_user=None, ftrack_server=None, user=None):
|
||||
if not ft_user:
|
||||
ft_user = os.environ.get("FTRACK_API_USER")
|
||||
|
||||
if not ft_user:
|
||||
return
|
||||
|
||||
def save_credentials(username, api_key, ftrack_server=None):
|
||||
hostname = get_ftrack_hostname(ftrack_server)
|
||||
if not user:
|
||||
user = get_user()
|
||||
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
|
||||
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
|
||||
|
||||
with open(CREDENTIALS_PATH, "r") as file:
|
||||
content = file.read()
|
||||
# Clear credentials
|
||||
clear_credentials(ftrack_server)
|
||||
|
||||
content_json = json.loads(content or "{}")
|
||||
if hostname not in content_json:
|
||||
content_json[hostname] = {}
|
||||
username_registry = OpenPypeSecureRegistry(username_name)
|
||||
api_key_registry = OpenPypeSecureRegistry(api_key_name)
|
||||
|
||||
content_json[hostname].pop(user, None)
|
||||
|
||||
with open(CREDENTIALS_PATH, "w") as file:
|
||||
file.write(json.dumps(content_json))
|
||||
username_registry.set_item(USERNAME_KEY, username)
|
||||
api_key_registry.set_item(API_KEY_KEY, api_key)
|
||||
|
||||
|
||||
def set_env(ft_user=None, ft_api_key=None):
|
||||
os.environ["FTRACK_API_USER"] = ft_user or ""
|
||||
os.environ["FTRACK_API_KEY"] = ft_api_key or ""
|
||||
def clear_credentials(ftrack_server=None):
|
||||
hostname = get_ftrack_hostname(ftrack_server)
|
||||
username_name = _get_ftrack_secure_key(hostname, USERNAME_KEY)
|
||||
api_key_name = _get_ftrack_secure_key(hostname, API_KEY_KEY)
|
||||
|
||||
username_registry = OpenPypeSecureRegistry(username_name)
|
||||
api_key_registry = OpenPypeSecureRegistry(api_key_name)
|
||||
|
||||
current_username = username_registry.get_item(USERNAME_KEY, None)
|
||||
current_api_key = api_key_registry.get_item(API_KEY_KEY, None)
|
||||
|
||||
if current_username is not None:
|
||||
username_registry.delete_item(USERNAME_KEY)
|
||||
|
||||
if current_api_key is not None:
|
||||
api_key_registry.delete_item(API_KEY_KEY)
|
||||
|
||||
|
||||
def get_env_credentials():
|
||||
return (
|
||||
os.environ.get("FTRACK_API_USER"),
|
||||
os.environ.get("FTRACK_API_KEY")
|
||||
)
|
||||
|
||||
|
||||
def check_credentials(ft_user, ft_api_key, ftrack_server=None):
|
||||
def check_credentials(username, api_key, ftrack_server=None):
|
||||
if not ftrack_server:
|
||||
ftrack_server = os.environ["FTRACK_SERVER"]
|
||||
|
||||
if not ft_user or not ft_api_key:
|
||||
if not username or not api_key:
|
||||
return False
|
||||
|
||||
try:
|
||||
session = ftrack_api.Session(
|
||||
server_url=ftrack_server,
|
||||
api_key=ft_api_key,
|
||||
api_user=ft_user
|
||||
api_key=api_key,
|
||||
api_user=username
|
||||
)
|
||||
session.close()
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import os
|
||||
from openpype.api import get_system_settings
|
||||
|
||||
|
||||
def get_ftrack_settings():
|
||||
return get_system_settings()["modules"]["ftrack"]
|
||||
|
||||
|
|
@ -10,7 +11,6 @@ def get_ftrack_url_from_settings():
|
|||
|
||||
|
||||
def get_ftrack_event_mongo_info():
|
||||
ftrack_settings = get_ftrack_settings()
|
||||
database_name = os.environ["OPENPYPE_DATABASE_NAME"]
|
||||
collection_name = "ftrack_events"
|
||||
return database_name, collection_name
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class FtrackTrayWrapper:
|
|||
self.bool_action_thread_running = False
|
||||
self.bool_timer_event = False
|
||||
|
||||
self.widget_login = login_dialog.CredentialsDialog()
|
||||
self.widget_login = login_dialog.CredentialsDialog(module)
|
||||
self.widget_login.login_changed.connect(self.on_login_change)
|
||||
self.widget_login.logout_signal.connect(self.on_logout)
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ class FtrackTrayWrapper:
|
|||
validation = credentials.check_credentials(ft_user, ft_api_key)
|
||||
if validation:
|
||||
self.widget_login.set_credentials(ft_user, ft_api_key)
|
||||
credentials.set_env(ft_user, ft_api_key)
|
||||
self.module.set_credentials_to_env(ft_user, ft_api_key)
|
||||
log.info("Connected to Ftrack successfully")
|
||||
self.on_login_change()
|
||||
|
||||
|
|
@ -337,7 +337,7 @@ class FtrackTrayWrapper:
|
|||
|
||||
def changed_user(self):
|
||||
self.stop_action_server()
|
||||
credentials.set_env()
|
||||
self.module.set_credentials_to_env(None, None)
|
||||
self.validate()
|
||||
|
||||
def start_timer_manager(self, data):
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ class CredentialsDialog(QtWidgets.QDialog):
|
|||
login_changed = QtCore.Signal()
|
||||
logout_signal = QtCore.Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, module, parent=None):
|
||||
super(CredentialsDialog, self).__init__(parent)
|
||||
|
||||
self.setWindowTitle("OpenPype - Ftrack Login")
|
||||
|
||||
self._module = module
|
||||
|
||||
self._login_server_thread = None
|
||||
self._is_logged = False
|
||||
self._in_advance_mode = False
|
||||
|
|
@ -268,7 +270,7 @@ class CredentialsDialog(QtWidgets.QDialog):
|
|||
verification = credentials.check_credentials(username, api_key)
|
||||
if verification:
|
||||
credentials.save_credentials(username, api_key, False)
|
||||
credentials.set_env(username, api_key)
|
||||
self._module.set_credentials_to_env(username, api_key)
|
||||
self.set_credentials(username, api_key)
|
||||
self.login_changed.emit()
|
||||
return verification
|
||||
|
|
|
|||
|
|
@ -40,8 +40,7 @@ class IdleManager(PypeModule, ITrayService):
|
|||
name = "idle_manager"
|
||||
|
||||
def initialize(self, module_settings):
|
||||
idle_man_settings = module_settings[self.name]
|
||||
self.enabled = idle_man_settings["enabled"]
|
||||
self.enabled = True
|
||||
|
||||
self.time_callbacks = collections.defaultdict(list)
|
||||
self.idle_thread = None
|
||||
|
|
@ -50,7 +49,8 @@ class IdleManager(PypeModule, ITrayService):
|
|||
return
|
||||
|
||||
def tray_start(self):
|
||||
self.start_thread()
|
||||
if self.time_callbacks:
|
||||
self.start_thread()
|
||||
|
||||
def tray_exit(self):
|
||||
self.stop_thread()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from openpype.modules.sync_server.sync_server import SyncServer
|
||||
from openpype.modules.sync_server.sync_server_module import SyncServerModule
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return SyncServer()
|
||||
return SyncServerModule()
|
||||
|
|
|
|||
0
openpype/modules/sync_server/providers/__init__.py
Normal file
0
openpype/modules/sync_server/providers/__init__.py
Normal file
|
|
@ -1,16 +1,23 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
import abc
|
||||
import six
|
||||
from openpype.api import Logger
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class AbstractProvider(metaclass=ABCMeta):
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AbstractProvider:
|
||||
|
||||
def __init__(self, site_name, tree=None, presets=None):
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.site_name = site_name
|
||||
|
||||
self.presets = presets
|
||||
|
||||
@abstractmethod
|
||||
super(AbstractProvider, self).__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns True if provider is activated, eg. has working credentials.
|
||||
|
|
@ -18,36 +25,54 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
(boolean)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def upload_file(self, source_path, target_path, overwrite=True):
|
||||
@abc.abstractmethod
|
||||
def upload_file(self, source_path, path,
|
||||
server, collection, file, representation, site,
|
||||
overwrite=False):
|
||||
"""
|
||||
Copy file from 'source_path' to 'target_path' on provider.
|
||||
Use 'overwrite' boolean to rewrite existing file on provider
|
||||
|
||||
Args:
|
||||
source_path (string): absolute path on local system
|
||||
target_path (string): absolute path on provider (GDrive etc.)
|
||||
overwrite (boolean): True if overwite existing
|
||||
source_path (string):
|
||||
path (string): absolute path with or without name of the file
|
||||
overwrite (boolean): replace existing file
|
||||
|
||||
arguments for saving progress:
|
||||
server (SyncServer): server instance to call update_db on
|
||||
collection (str): name of collection
|
||||
file (dict): info about uploaded file (matches structure from db)
|
||||
representation (dict): complete repre containing 'file'
|
||||
site (str): site name
|
||||
Returns:
|
||||
(string) file_id of created file, raises exception
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def download_file(self, source_path, local_path, overwrite=True):
|
||||
@abc.abstractmethod
|
||||
def download_file(self, source_path, local_path,
|
||||
server, collection, file, representation, site,
|
||||
overwrite=False):
|
||||
"""
|
||||
Download file from provider into local system
|
||||
|
||||
Args:
|
||||
source_path (string): absolute path on provider
|
||||
local_path (string): absolute path on local
|
||||
overwrite (bool): default set to True
|
||||
local_path (string): absolute path with or without name of the file
|
||||
overwrite (boolean): replace existing file
|
||||
|
||||
arguments for saving progress:
|
||||
server (SyncServer): server instance to call update_db on
|
||||
collection (str): name of collection
|
||||
file (dict): info about uploaded file (matches structure from db)
|
||||
representation (dict): complete repre containing 'file'
|
||||
site (str): site name
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def delete_file(self, path):
|
||||
"""
|
||||
Deletes file from 'path'. Expects path to specific file.
|
||||
|
|
@ -60,7 +85,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def list_folder(self, folder_path):
|
||||
"""
|
||||
List all files and subfolders of particular path non-recursively.
|
||||
|
|
@ -72,7 +97,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def create_folder(self, folder_path):
|
||||
"""
|
||||
Create all nonexistent folders and subfolders in 'path'.
|
||||
|
|
@ -85,7 +110,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def get_tree(self):
|
||||
"""
|
||||
Creates folder structure for providers which do not provide
|
||||
|
|
@ -94,16 +119,50 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
@abc.abstractmethod
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Replaces root placeholders with appropriate real value from
|
||||
'root_configs' (from Settings or Local Settings) or Anatomy
|
||||
(mainly for 'studio' site)
|
||||
Returns root values for path resolving
|
||||
|
||||
Args:
|
||||
path(string): path with '{root[work]}/...'
|
||||
root_config(dict): from Settings or Local Settings
|
||||
anatomy (Anatomy): prepared anatomy object for project
|
||||
Takes value from Anatomy which takes values from Settings
|
||||
overridden by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
pass
|
||||
|
||||
def resolve_path(self, path, root_config=None, anatomy=None):
|
||||
"""
|
||||
Replaces all root placeholders with proper values
|
||||
|
||||
Args:
|
||||
path(string): root[work]/folder...
|
||||
root_config (dict): {'work': "c:/..."...}
|
||||
anatomy (Anatomy): object of Anatomy
|
||||
Returns:
|
||||
(string): proper url
|
||||
"""
|
||||
if not root_config:
|
||||
root_config = self.get_roots_config(anatomy)
|
||||
|
||||
if root_config and not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
|
||||
try:
|
||||
if not root_config:
|
||||
raise KeyError
|
||||
|
||||
path = path.format(**root_config)
|
||||
except KeyError:
|
||||
try:
|
||||
path = anatomy.fill_root(path)
|
||||
except KeyError:
|
||||
msg = "Error in resolving local root from anatomy"
|
||||
log.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
return path
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from openpype.api import get_system_settings
|
|||
from ..utils import time_function
|
||||
import time
|
||||
|
||||
|
||||
SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive.readonly'] # for write|delete
|
||||
|
|
@ -45,9 +46,10 @@ class GDriveHandler(AbstractProvider):
|
|||
MY_DRIVE_STR = 'My Drive' # name of root folder of regular Google drive
|
||||
CHUNK_SIZE = 2097152 # must be divisible by 256!
|
||||
|
||||
def __init__(self, site_name, tree=None, presets=None):
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.project_name = project_name
|
||||
self.site_name = site_name
|
||||
|
||||
self.presets = presets
|
||||
|
|
@ -65,137 +67,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self._tree = tree
|
||||
self.active = True
|
||||
|
||||
def _get_gd_service(self):
|
||||
"""
|
||||
Authorize client with 'credentials.json', uses service account.
|
||||
Service account needs to have target folder shared with.
|
||||
Produces service that communicates with GDrive API.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
creds = service_account.Credentials.from_service_account_file(
|
||||
self.presets["credentials_url"],
|
||||
scopes=SCOPES)
|
||||
service = build('drive', 'v3',
|
||||
credentials=creds, cache_discovery=False)
|
||||
return service
|
||||
|
||||
def _prepare_root_info(self):
|
||||
"""
|
||||
Prepare info about roots and theirs folder ids from 'presets'.
|
||||
Configuration might be for single or multiroot projects.
|
||||
Regular My Drive and Shared drives are implemented, their root
|
||||
folder ids need to be queried in slightly different way.
|
||||
|
||||
Returns:
|
||||
(dicts) of dicts where root folders are keys
|
||||
"""
|
||||
roots = {}
|
||||
for path in self.get_roots_config().values():
|
||||
if self.MY_DRIVE_STR in path:
|
||||
roots[self.MY_DRIVE_STR] = self.service.files()\
|
||||
.get(fileId='root').execute()
|
||||
else:
|
||||
shared_drives = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
response = self.service.drives().list(
|
||||
pageSize=100,
|
||||
pageToken=page_token).execute()
|
||||
shared_drives.extend(response.get('drives', []))
|
||||
page_token = response.get('nextPageToken', None)
|
||||
if page_token is None:
|
||||
break
|
||||
|
||||
folders = path.split('/')
|
||||
if len(folders) < 2:
|
||||
raise ValueError("Wrong root folder definition {}".
|
||||
format(path))
|
||||
|
||||
for shared_drive in shared_drives:
|
||||
if folders[1] in shared_drive["name"]:
|
||||
roots[shared_drive["name"]] = {
|
||||
"name": shared_drive["name"],
|
||||
"id": shared_drive["id"]}
|
||||
if self.MY_DRIVE_STR not in roots: # add My Drive always
|
||||
roots[self.MY_DRIVE_STR] = self.service.files() \
|
||||
.get(fileId='root').execute()
|
||||
|
||||
return roots
|
||||
|
||||
@time_function
|
||||
def _build_tree(self, folders):
|
||||
"""
|
||||
Create in-memory structure resolving paths to folder id as
|
||||
recursive querying might be slower.
|
||||
Initialized in the time of class initialization.
|
||||
Maybe should be persisted
|
||||
Tree is structure of path to id:
|
||||
'/ROOT': {'id': '1234567'}
|
||||
'/ROOT/PROJECT_FOLDER': {'id':'222222'}
|
||||
'/ROOT/PROJECT_FOLDER/Assets': {'id': '3434545'}
|
||||
Args:
|
||||
folders (list): list of dictionaries with folder metadata
|
||||
Returns:
|
||||
(dictionary) path as a key, folder id as a value
|
||||
"""
|
||||
log.debug("build_tree len {}".format(len(folders)))
|
||||
root_ids = []
|
||||
default_root_id = None
|
||||
tree = {}
|
||||
ending_by = {}
|
||||
for root_name, root in self.root.items(): # might be multiple roots
|
||||
if root["id"] not in root_ids:
|
||||
tree["/" + root_name] = {"id": root["id"]}
|
||||
ending_by[root["id"]] = "/" + root_name
|
||||
root_ids.append(root["id"])
|
||||
|
||||
if self.MY_DRIVE_STR == root_name:
|
||||
default_root_id = root["id"]
|
||||
|
||||
no_parents_yet = {}
|
||||
while folders:
|
||||
folder = folders.pop(0)
|
||||
parents = folder.get("parents", [])
|
||||
# weird cases, shared folders, etc, parent under root
|
||||
if not parents:
|
||||
parent = default_root_id
|
||||
else:
|
||||
parent = parents[0]
|
||||
|
||||
if folder["id"] in root_ids: # do not process root
|
||||
continue
|
||||
|
||||
if parent in ending_by:
|
||||
path_key = ending_by[parent] + "/" + folder["name"]
|
||||
ending_by[folder["id"]] = path_key
|
||||
tree[path_key] = {"id": folder["id"]}
|
||||
else:
|
||||
no_parents_yet.setdefault(parent, []).append((folder["id"],
|
||||
folder["name"]))
|
||||
loop_cnt = 0
|
||||
# break if looped more then X times - safety against infinite loop
|
||||
while no_parents_yet and loop_cnt < 20:
|
||||
|
||||
keys = list(no_parents_yet.keys())
|
||||
for parent in keys:
|
||||
if parent in ending_by.keys():
|
||||
subfolders = no_parents_yet.pop(parent)
|
||||
for folder_id, folder_name in subfolders:
|
||||
path_key = ending_by[parent] + "/" + folder_name
|
||||
ending_by[folder_id] = path_key
|
||||
tree[path_key] = {"id": folder_id}
|
||||
loop_cnt += 1
|
||||
|
||||
if len(no_parents_yet) > 0:
|
||||
log.debug("Some folders path are not resolved {}".
|
||||
format(no_parents_yet))
|
||||
log.debug("Remove deleted folders from trash.")
|
||||
|
||||
return tree
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns True if provider is activated, eg. has working credentials.
|
||||
|
|
@ -204,6 +75,21 @@ class GDriveHandler(AbstractProvider):
|
|||
"""
|
||||
return self.active
|
||||
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Returns root values for path resolving
|
||||
|
||||
Use only Settings as GDrive cannot be modified by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
# GDrive roots cannot be locally overridden
|
||||
return self.presets['root']
|
||||
|
||||
def get_tree(self):
|
||||
"""
|
||||
Building of the folder tree could be potentially expensive,
|
||||
|
|
@ -217,26 +103,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self._tree = self._build_tree(self.list_folders())
|
||||
return self._tree
|
||||
|
||||
def get_roots_config(self):
|
||||
"""
|
||||
Returns value from presets of roots. It calculates with multi
|
||||
roots. Config should be simple key value, or dictionary.
|
||||
|
||||
Examples:
|
||||
"root": "/My Drive"
|
||||
OR
|
||||
"root": {"root_ONE": "value", "root_TWO":"value}
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
roots = self.presets["root"]
|
||||
if isinstance(roots, str):
|
||||
roots = {"root": roots}
|
||||
return roots
|
||||
|
||||
def create_folder(self, path):
|
||||
"""
|
||||
Create all nonexistent folders and subfolders in 'path'.
|
||||
|
|
@ -510,20 +376,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self.service.files().delete(fileId=file["id"],
|
||||
supportsAllDrives=True).execute()
|
||||
|
||||
def _get_folder_metadata(self, path):
|
||||
"""
|
||||
Get info about folder with 'path'
|
||||
Args:
|
||||
path (string):
|
||||
|
||||
Returns:
|
||||
(dictionary) with metadata or raises ValueError
|
||||
"""
|
||||
try:
|
||||
return self.get_tree()[path]
|
||||
except Exception:
|
||||
raise ValueError("Uknown folder id {}".format(id))
|
||||
|
||||
def list_folder(self, folder_path):
|
||||
"""
|
||||
List all files and subfolders of particular path non-recursively.
|
||||
|
|
@ -678,15 +530,151 @@ class GDriveHandler(AbstractProvider):
|
|||
return
|
||||
return provider_presets
|
||||
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
if not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
def _get_gd_service(self):
|
||||
"""
|
||||
Authorize client with 'credentials.json', uses service account.
|
||||
Service account needs to have target folder shared with.
|
||||
Produces service that communicates with GDrive API.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
creds = service_account.Credentials.from_service_account_file(
|
||||
self.presets["credentials_url"],
|
||||
scopes=SCOPES)
|
||||
service = build('drive', 'v3',
|
||||
credentials=creds, cache_discovery=False)
|
||||
return service
|
||||
|
||||
def _prepare_root_info(self):
|
||||
"""
|
||||
Prepare info about roots and theirs folder ids from 'presets'.
|
||||
Configuration might be for single or multiroot projects.
|
||||
Regular My Drive and Shared drives are implemented, their root
|
||||
folder ids need to be queried in slightly different way.
|
||||
|
||||
Returns:
|
||||
(dicts) of dicts where root folders are keys
|
||||
"""
|
||||
roots = {}
|
||||
config_roots = self.get_roots_config()
|
||||
for path in config_roots.values():
|
||||
if self.MY_DRIVE_STR in path:
|
||||
roots[self.MY_DRIVE_STR] = self.service.files()\
|
||||
.get(fileId='root').execute()
|
||||
else:
|
||||
shared_drives = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
response = self.service.drives().list(
|
||||
pageSize=100,
|
||||
pageToken=page_token).execute()
|
||||
shared_drives.extend(response.get('drives', []))
|
||||
page_token = response.get('nextPageToken', None)
|
||||
if page_token is None:
|
||||
break
|
||||
|
||||
folders = path.split('/')
|
||||
if len(folders) < 2:
|
||||
raise ValueError("Wrong root folder definition {}".
|
||||
format(path))
|
||||
|
||||
for shared_drive in shared_drives:
|
||||
if folders[1] in shared_drive["name"]:
|
||||
roots[shared_drive["name"]] = {
|
||||
"name": shared_drive["name"],
|
||||
"id": shared_drive["id"]}
|
||||
if self.MY_DRIVE_STR not in roots: # add My Drive always
|
||||
roots[self.MY_DRIVE_STR] = self.service.files() \
|
||||
.get(fileId='root').execute()
|
||||
|
||||
return roots
|
||||
|
||||
@time_function
|
||||
def _build_tree(self, folders):
|
||||
"""
|
||||
Create in-memory structure resolving paths to folder id as
|
||||
recursive querying might be slower.
|
||||
Initialized in the time of class initialization.
|
||||
Maybe should be persisted
|
||||
Tree is structure of path to id:
|
||||
'/ROOT': {'id': '1234567'}
|
||||
'/ROOT/PROJECT_FOLDER': {'id':'222222'}
|
||||
'/ROOT/PROJECT_FOLDER/Assets': {'id': '3434545'}
|
||||
Args:
|
||||
folders (list): list of dictionaries with folder metadata
|
||||
Returns:
|
||||
(dictionary) path as a key, folder id as a value
|
||||
"""
|
||||
log.debug("build_tree len {}".format(len(folders)))
|
||||
root_ids = []
|
||||
default_root_id = None
|
||||
tree = {}
|
||||
ending_by = {}
|
||||
for root_name, root in self.root.items(): # might be multiple roots
|
||||
if root["id"] not in root_ids:
|
||||
tree["/" + root_name] = {"id": root["id"]}
|
||||
ending_by[root["id"]] = "/" + root_name
|
||||
root_ids.append(root["id"])
|
||||
|
||||
if self.MY_DRIVE_STR == root_name:
|
||||
default_root_id = root["id"]
|
||||
|
||||
no_parents_yet = {}
|
||||
while folders:
|
||||
folder = folders.pop(0)
|
||||
parents = folder.get("parents", [])
|
||||
# weird cases, shared folders, etc, parent under root
|
||||
if not parents:
|
||||
parent = default_root_id
|
||||
else:
|
||||
parent = parents[0]
|
||||
|
||||
if folder["id"] in root_ids: # do not process root
|
||||
continue
|
||||
|
||||
if parent in ending_by:
|
||||
path_key = ending_by[parent] + "/" + folder["name"]
|
||||
ending_by[folder["id"]] = path_key
|
||||
tree[path_key] = {"id": folder["id"]}
|
||||
else:
|
||||
no_parents_yet.setdefault(parent, []).append((folder["id"],
|
||||
folder["name"]))
|
||||
loop_cnt = 0
|
||||
# break if looped more then X times - safety against infinite loop
|
||||
while no_parents_yet and loop_cnt < 20:
|
||||
|
||||
keys = list(no_parents_yet.keys())
|
||||
for parent in keys:
|
||||
if parent in ending_by.keys():
|
||||
subfolders = no_parents_yet.pop(parent)
|
||||
for folder_id, folder_name in subfolders:
|
||||
path_key = ending_by[parent] + "/" + folder_name
|
||||
ending_by[folder_id] = path_key
|
||||
tree[path_key] = {"id": folder_id}
|
||||
loop_cnt += 1
|
||||
|
||||
if len(no_parents_yet) > 0:
|
||||
log.debug("Some folders path are not resolved {}".
|
||||
format(no_parents_yet))
|
||||
log.debug("Remove deleted folders from trash.")
|
||||
|
||||
return tree
|
||||
|
||||
def _get_folder_metadata(self, path):
|
||||
"""
|
||||
Get info about folder with 'path'
|
||||
Args:
|
||||
path (string):
|
||||
|
||||
Returns:
|
||||
(dictionary) with metadata or raises ValueError
|
||||
"""
|
||||
try:
|
||||
return path.format(**root_config)
|
||||
except KeyError:
|
||||
msg = "Error in resolving remote root, unknown key"
|
||||
log.error(msg)
|
||||
return self.get_tree()[path]
|
||||
except Exception:
|
||||
raise ValueError("Uknown folder id {}".format(id))
|
||||
|
||||
def _handle_q(self, q, trashed=False):
|
||||
""" API list call contain trashed and hidden files/folder by default.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from enum import Enum
|
||||
from .gdrive import GDriveHandler
|
||||
from .local_drive import LocalDriveHandler
|
||||
|
||||
|
|
@ -25,7 +24,8 @@ class ProviderFactory:
|
|||
"""
|
||||
self.providers[provider] = (creator, batch_limit)
|
||||
|
||||
def get_provider(self, provider, site_name, tree=None, presets=None):
|
||||
def get_provider(self, provider, project_name, site_name,
|
||||
tree=None, presets=None):
|
||||
"""
|
||||
Returns new instance of provider client for specific site.
|
||||
One provider could have multiple sites.
|
||||
|
|
@ -37,6 +37,7 @@ class ProviderFactory:
|
|||
provider (string): 'gdrive','S3'
|
||||
site_name (string): descriptor of site, different service accounts
|
||||
must have different site name
|
||||
project_name (string): different projects could have diff. sites
|
||||
tree (dictionary): - folder paths to folder id structure
|
||||
presets (dictionary): config for provider and site (eg.
|
||||
"credentials_url"..)
|
||||
|
|
@ -44,7 +45,8 @@ class ProviderFactory:
|
|||
(implementation of AbstractProvider)
|
||||
"""
|
||||
creator_info = self._get_creator_info(provider)
|
||||
site = creator_info[0](site_name, tree, presets) # call init
|
||||
# call init
|
||||
site = creator_info[0](project_name, site_name, tree, presets)
|
||||
|
||||
return site
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import shutil
|
|||
import threading
|
||||
import time
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.api import Logger, Anatomy
|
||||
from .abstract_provider import AbstractProvider
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
|
@ -12,6 +12,14 @@ log = Logger().get_logger("SyncServer")
|
|||
|
||||
class LocalDriveHandler(AbstractProvider):
|
||||
""" Handles required operations on mounted disks with OS """
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.project_name = project_name
|
||||
self.site_name = site_name
|
||||
|
||||
self.active = self.is_active()
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
|
|
@ -82,27 +90,37 @@ class LocalDriveHandler(AbstractProvider):
|
|||
os.makedirs(folder_path, exist_ok=True)
|
||||
return folder_path
|
||||
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Returns root values for path resolving
|
||||
|
||||
Takes value from Anatomy which takes values from Settings
|
||||
overridden by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(self.project_name,
|
||||
self._normalize_site_name(self.site_name))
|
||||
|
||||
return {'root': anatomy.roots}
|
||||
|
||||
def get_tree(self):
|
||||
return
|
||||
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
if root_config and not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
def get_configurable_items_for_site(self):
|
||||
"""
|
||||
Returns list of items that should be configurable by User
|
||||
|
||||
try:
|
||||
if not root_config:
|
||||
raise KeyError
|
||||
|
||||
path = path.format(**root_config)
|
||||
except KeyError:
|
||||
try:
|
||||
path = anatomy.fill_root(path)
|
||||
except KeyError:
|
||||
msg = "Error in resolving local root from anatomy"
|
||||
log.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
return path
|
||||
Returns:
|
||||
(list of dict)
|
||||
[{key:"root", label:"root", value:"valueFromSettings"}]
|
||||
"""
|
||||
pass
|
||||
|
||||
def _copy(self, source_path, target_path):
|
||||
print("copying {}->{}".format(source_path, target_path))
|
||||
|
|
@ -133,3 +151,9 @@ class LocalDriveHandler(AbstractProvider):
|
|||
)
|
||||
target_file_size = os.path.getsize(target_path)
|
||||
time.sleep(0.5)
|
||||
|
||||
def _normalize_site_name(self, site_name):
|
||||
"""Transform user id to 'local' for Local settings"""
|
||||
if site_name != 'studio':
|
||||
return 'local'
|
||||
return site_name
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
1193
openpype/modules/sync_server/sync_server_module.py
Normal file
1193
openpype/modules/sync_server/sync_server_module.py
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
52
openpype/modules/sync_server/tray/lib.py
Normal file
52
openpype/modules/sync_server/tray/lib.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from Qt import QtCore
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
STATUS = {
|
||||
0: 'In Progress',
|
||||
1: 'Queued',
|
||||
2: 'Failed',
|
||||
3: 'Paused',
|
||||
4: 'Synced OK',
|
||||
-1: 'Not available'
|
||||
}
|
||||
|
||||
DUMMY_PROJECT = "No project configured"
|
||||
|
||||
ProviderRole = QtCore.Qt.UserRole + 2
|
||||
ProgressRole = QtCore.Qt.UserRole + 4
|
||||
DateRole = QtCore.Qt.UserRole + 6
|
||||
FailedRole = QtCore.Qt.UserRole + 8
|
||||
|
||||
|
||||
def pretty_size(value, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(value) < 1024.0:
|
||||
return "%3.1f%s%s" % (value, unit, suffix)
|
||||
value /= 1024.0
|
||||
return "%.1f%s%s" % (value, 'Yi', suffix)
|
||||
|
||||
|
||||
def convert_progress(value):
|
||||
try:
|
||||
progress = float(value)
|
||||
except (ValueError, TypeError):
|
||||
progress = 0.0
|
||||
|
||||
return progress
|
||||
|
||||
|
||||
def translate_provider_for_icon(sync_server, project, site):
|
||||
"""
|
||||
Get provider for 'site'
|
||||
|
||||
This is used for getting icon, 'studio' should have different icon
|
||||
then local sites, even the provider 'local_drive' is same
|
||||
|
||||
"""
|
||||
if site == sync_server.DEFAULT_SITE:
|
||||
return sync_server.DEFAULT_SITE
|
||||
return sync_server.get_provider_for_site(project, site)
|
||||
1124
openpype/modules/sync_server/tray/models.py
Normal file
1124
openpype/modules/sync_server/tray/models.py
Normal file
File diff suppressed because it is too large
Load diff
820
openpype/modules/sync_server/tray/widgets.py
Normal file
820
openpype/modules/sync_server/tray/widgets.py
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from Qt.QtCore import Qt
|
||||
|
||||
from openpype.tools.settings import (
|
||||
ProjectListWidget,
|
||||
style
|
||||
)
|
||||
|
||||
from openpype.api import get_local_site_id
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
|
||||
from openpype.modules.sync_server.tray.models import (
|
||||
SyncRepresentationSummaryModel,
|
||||
SyncRepresentationDetailModel
|
||||
)
|
||||
|
||||
from openpype.modules.sync_server.tray import lib
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class SyncProjectListWidget(ProjectListWidget):
|
||||
"""
|
||||
Lists all projects that are synchronized to choose from
|
||||
"""
|
||||
|
||||
def __init__(self, sync_server, parent):
|
||||
super(SyncProjectListWidget, self).__init__(parent)
|
||||
self.sync_server = sync_server
|
||||
self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.project_list.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
self.project_name = None
|
||||
self.local_site = None
|
||||
self.icons = {}
|
||||
|
||||
def validate_context_change(self):
|
||||
return True
|
||||
|
||||
def refresh(self):
|
||||
model = self.project_list.model()
|
||||
model.clear()
|
||||
|
||||
project_name = None
|
||||
for project_name in self.sync_server.sync_project_settings.\
|
||||
keys():
|
||||
if self.sync_server.is_paused() or \
|
||||
self.sync_server.is_project_paused(project_name):
|
||||
icon = self._get_icon("paused")
|
||||
else:
|
||||
icon = self._get_icon("synced")
|
||||
|
||||
model.appendRow(QtGui.QStandardItem(icon, project_name))
|
||||
|
||||
if len(self.sync_server.sync_project_settings.keys()) == 0:
|
||||
model.appendRow(QtGui.QStandardItem(lib.DUMMY_PROJECT))
|
||||
|
||||
self.current_project = self.project_list.currentIndex().data(
|
||||
QtCore.Qt.DisplayRole
|
||||
)
|
||||
if not self.current_project:
|
||||
self.current_project = self.project_list.model().item(0). \
|
||||
data(QtCore.Qt.DisplayRole)
|
||||
|
||||
if project_name:
|
||||
self.local_site = self.sync_server.get_active_site(project_name)
|
||||
|
||||
def _get_icon(self, status):
|
||||
if not self.icons.get(status):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, status)
|
||||
icon = QtGui.QIcon(pix_url)
|
||||
self.icons[status] = icon
|
||||
else:
|
||||
icon = self.icons[status]
|
||||
return icon
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
point_index = self.project_list.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
else:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.local_site == get_local_site_id():
|
||||
action = QtWidgets.QAction("Clear local project")
|
||||
actions_mapping[action] = self._clear_project
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
if to_run:
|
||||
to_run()
|
||||
|
||||
def _pause(self):
|
||||
if self.project_name:
|
||||
self.sync_server.pause_project(self.project_name)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
def _unpause(self):
|
||||
if self.project_name:
|
||||
self.sync_server.unpause_project(self.project_name)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
def _clear_project(self):
|
||||
if self.project_name:
|
||||
self.sync_server.clear_project(self.project_name, self.local_site)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
|
||||
class SyncRepresentationWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Summary dialog with list of representations that matches current
|
||||
settings 'local_site' and 'remote_site'.
|
||||
"""
|
||||
active_changed = QtCore.Signal() # active index changed
|
||||
message_generated = QtCore.Signal(str)
|
||||
|
||||
default_widths = (
|
||||
("asset", 220),
|
||||
("subset", 190),
|
||||
("version", 55),
|
||||
("representation", 95),
|
||||
("local_site", 170),
|
||||
("remote_site", 170),
|
||||
("files_count", 50),
|
||||
("files_size", 60),
|
||||
("priority", 50),
|
||||
("state", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, project=None, parent=None):
|
||||
super(SyncRepresentationWidget, self).__init__(parent)
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
self._selected_id = None # keep last selected _id
|
||||
self.representation_id = None
|
||||
self.site_name = None # to pause/unpause representation
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representations..")
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationSummaryModel(sync_server, headers, project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(
|
||||
-1, Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.table_view.doubleClicked.connect(self._double_clicked)
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
def _selection_changed(self, _new_selection):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = \
|
||||
self.table_view.model().data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
Sets selection to 'self._selected_id' if exists.
|
||||
|
||||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
self.selection_model.setCurrentIndex(index, mode)
|
||||
else:
|
||||
self._selected_id = None
|
||||
|
||||
def _double_clicked(self, index):
|
||||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
detail_window = SyncServerDetailWindow(
|
||||
self.sync_server, _id, self.table_view.model().project)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
"""
|
||||
Shows menu with loader actions on Right-click.
|
||||
"""
|
||||
point_index = self.table_view.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.representation_id = self.item._id
|
||||
log.debug("menu representation _id:: {}".
|
||||
format(self.representation_id))
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
local_site = self.item.local_site
|
||||
local_progress = self.item.local_progress
|
||||
remote_site = self.item.remote_site
|
||||
remote_progress = self.item.remote_progress
|
||||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
if 'studio' in site:
|
||||
txt = " studio version"
|
||||
else:
|
||||
txt = " local version"
|
||||
action = QtWidgets.QAction("Open in explorer" + txt)
|
||||
if progress == 1.0:
|
||||
actions_mapping[action] = self._open_in_explorer
|
||||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
# progress smaller then 1.0 --> in progress or queued
|
||||
if local_progress < 1.0:
|
||||
self.site_name = local_site
|
||||
else:
|
||||
self.site_name = remote_site
|
||||
|
||||
if self.item.state in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[3]:
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
menu.addAction(action)
|
||||
|
||||
# if self.item.state == lib.STATUS[1]:
|
||||
# action = QtWidgets.QAction("Open error detail")
|
||||
# actions_mapping[action] = self._show_detail
|
||||
# menu.addAction(action)
|
||||
|
||||
if remote_progress == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync Active site")
|
||||
actions_mapping[action] = self._reset_local_site
|
||||
menu.addAction(action)
|
||||
|
||||
if local_progress == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync Remote site")
|
||||
actions_mapping[action] = self._reset_remote_site
|
||||
menu.addAction(action)
|
||||
|
||||
if local_site != self.sync_server.DEFAULT_SITE:
|
||||
action = QtWidgets.QAction("Completely remove from local")
|
||||
actions_mapping[action] = self._remove_site
|
||||
menu.addAction(action)
|
||||
else:
|
||||
action = QtWidgets.QAction("Mark for sync to local")
|
||||
actions_mapping[action] = self._add_site
|
||||
menu.addAction(action)
|
||||
|
||||
if not actions_mapping:
|
||||
action = QtWidgets.QAction("< No action >")
|
||||
actions_mapping[action] = None
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
to_run_kwargs = actions_kwargs_mapping.get(result, {})
|
||||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
self.table_view.model().refresh()
|
||||
|
||||
def _pause(self):
|
||||
self.sync_server.pause_representation(self.table_view.model().project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
self.message_generated.emit("Paused {}".format(self.representation_id))
|
||||
|
||||
def _unpause(self):
|
||||
self.sync_server.unpause_representation(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
self.message_generated.emit("Unpaused {}".format(
|
||||
self.representation_id))
|
||||
|
||||
# temporary here for testing, will be removed TODO
|
||||
def _add_site(self):
|
||||
log.info(self.representation_id)
|
||||
project_name = self.table_view.model().project
|
||||
local_site_name = get_local_site_id()
|
||||
try:
|
||||
self.sync_server.add_site(
|
||||
project_name,
|
||||
self.representation_id,
|
||||
local_site_name
|
||||
)
|
||||
self.message_generated.emit(
|
||||
"Site {} added for {}".format(local_site_name,
|
||||
self.representation_id))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
|
||||
def _remove_site(self):
|
||||
"""
|
||||
Removes site record AND files.
|
||||
|
||||
This is ONLY for representations stored on local site, which
|
||||
cannot be same as SyncServer.DEFAULT_SITE.
|
||||
|
||||
This could only happen when artist work on local machine, not
|
||||
connected to studio mounted drives.
|
||||
"""
|
||||
log.info("Removing {}".format(self.representation_id))
|
||||
try:
|
||||
local_site = get_local_site_id()
|
||||
self.sync_server.remove_site(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
local_site,
|
||||
True)
|
||||
self.message_generated.emit("Site {} removed".format(local_site))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'local')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'remote')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.table_view.model().project
|
||||
fpath = self.sync_server.get_local_file_path(project,
|
||||
site,
|
||||
fpath)
|
||||
|
||||
fpath = os.path.normpath(os.path.dirname(fpath))
|
||||
if os.path.isdir(fpath):
|
||||
if 'win' in sys.platform: # windows
|
||||
subprocess.Popen('explorer "%s"' % fpath)
|
||||
elif sys.platform == 'darwin': # macOS
|
||||
subprocess.Popen(['open', fpath])
|
||||
else: # linux
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', fpath])
|
||||
except OSError:
|
||||
raise OSError('unsupported xdg-open call??')
|
||||
|
||||
def _save_scrollbar(self):
|
||||
self._scrollbar_pos = self.table_view.verticalScrollBar().value()
|
||||
|
||||
def _set_scrollbar(self):
|
||||
if self._scrollbar_pos:
|
||||
self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)
|
||||
|
||||
|
||||
class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Widget to display list of synchronizable files for single repre.
|
||||
|
||||
Args:
|
||||
_id (str): representation _id
|
||||
project (str): name of project with repre
|
||||
parent (QDialog): SyncServerDetailWindow
|
||||
"""
|
||||
active_changed = QtCore.Signal() # active index changed
|
||||
|
||||
default_widths = (
|
||||
("file", 290),
|
||||
("local_site", 185),
|
||||
("remote_site", 185),
|
||||
("size", 60),
|
||||
("priority", 25),
|
||||
("state", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, _id=None, project=None, parent=None):
|
||||
super(SyncRepresentationDetailWidget, self).__init__(parent)
|
||||
|
||||
log.debug("Representation_id:{}".format(_id))
|
||||
self.representation_id = _id
|
||||
self.item = None # set to item that mouse was clicked over
|
||||
self.project = project
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
self._selected_id = None
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representation..")
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationDetailModel(sync_server, headers, _id,
|
||||
project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QtWidgets.QTableView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(-1,
|
||||
Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
def _selection_changed(self):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
Sets selection to 'self._selected_id' if exists.
|
||||
|
||||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
self.selection_model.setCurrentIndex(index, mode)
|
||||
else:
|
||||
self._selected_id = None
|
||||
|
||||
def _show_detail(self):
|
||||
"""
|
||||
Shows windows with error message for failed sync of a file.
|
||||
"""
|
||||
dt = max(self.item.created_dt, self.item.sync_dt)
|
||||
detail_window = SyncRepresentationErrorWindow(self.item._id,
|
||||
self.project,
|
||||
dt,
|
||||
self.item.tries,
|
||||
self.item.error)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
"""
|
||||
Shows menu with loader actions on Right-click.
|
||||
"""
|
||||
point_index = self.table_view.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
local_site = self.item.local_site
|
||||
local_progress = self.item.local_progress
|
||||
remote_site = self.item.remote_site
|
||||
remote_progress = self.item.remote_progress
|
||||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
if 'studio' in site:
|
||||
txt = " studio version"
|
||||
else:
|
||||
txt = " local version"
|
||||
action = QtWidgets.QAction("Open in explorer" + txt)
|
||||
if progress == 1:
|
||||
actions_mapping[action] = self._open_in_explorer
|
||||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[2]:
|
||||
action = QtWidgets.QAction("Open error detail")
|
||||
actions_mapping[action] = self._show_detail
|
||||
menu.addAction(action)
|
||||
|
||||
if float(remote_progress) == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync active site")
|
||||
actions_mapping[action] = self._reset_local_site
|
||||
menu.addAction(action)
|
||||
|
||||
if float(local_progress) == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync remote site")
|
||||
actions_mapping[action] = self._reset_remote_site
|
||||
menu.addAction(action)
|
||||
|
||||
if not actions_mapping:
|
||||
action = QtWidgets.QAction("< No action >")
|
||||
actions_mapping[action] = None
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
to_run_kwargs = actions_kwargs_mapping.get(result, {})
|
||||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'local',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'remote',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.project
|
||||
fpath = self.sync_server.get_local_file_path(project, site, fpath)
|
||||
|
||||
fpath = os.path.normpath(os.path.dirname(fpath))
|
||||
if os.path.isdir(fpath):
|
||||
if 'win' in sys.platform: # windows
|
||||
subprocess.Popen('explorer "%s"' % fpath)
|
||||
elif sys.platform == 'darwin': # macOS
|
||||
subprocess.Popen(['open', fpath])
|
||||
else: # linux
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', fpath])
|
||||
except OSError:
|
||||
raise OSError('unsupported xdg-open call??')
|
||||
|
||||
def _save_scrollbar(self):
|
||||
self._scrollbar_pos = self.table_view.verticalScrollBar().value()
|
||||
|
||||
def _set_scrollbar(self):
|
||||
if self._scrollbar_pos:
|
||||
self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)
|
||||
|
||||
|
||||
class SyncRepresentationErrorWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Dialog to show when sync error happened, prints error message
|
||||
"""
|
||||
|
||||
def __init__(self, _id, dt, tries, msg, parent=None):
|
||||
super(SyncRepresentationErrorWidget, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
||||
txts = []
|
||||
txts.append("{}: {}".format("Last update date", pretty_timestamp(dt)))
|
||||
txts.append("{}: {}".format("Retries", str(tries)))
|
||||
txts.append("{}: {}".format("Error message", msg))
|
||||
|
||||
text_area = QtWidgets.QPlainTextEdit("\n\n".join(txts))
|
||||
text_area.setReadOnly(True)
|
||||
layout.addWidget(text_area)
|
||||
|
||||
|
||||
class ImageDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""
|
||||
Prints icon of site and progress of synchronization
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ImageDelegate, self).__init__(parent)
|
||||
self.icons = {}
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
super(ImageDelegate, self).paint(painter, option, index)
|
||||
option = QtWidgets.QStyleOptionViewItem(option)
|
||||
option.showDecorationSelected = True
|
||||
|
||||
provider = index.data(lib.ProviderRole)
|
||||
value = index.data(lib.ProgressRole)
|
||||
date_value = index.data(lib.DateRole)
|
||||
is_failed = index.data(lib.FailedRole)
|
||||
|
||||
if not self.icons.get(provider):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"providers", "resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
pixmap = QtGui.QPixmap(pix_url)
|
||||
self.icons[provider] = pixmap
|
||||
else:
|
||||
pixmap = self.icons[provider]
|
||||
|
||||
padding = 10
|
||||
point = QtCore.QPoint(option.rect.x() + padding,
|
||||
option.rect.y() +
|
||||
(option.rect.height() - pixmap.height()) / 2)
|
||||
painter.drawPixmap(point, pixmap)
|
||||
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
|
||||
text_rect = option.rect.translated(10, 0)
|
||||
painter.drawText(text_rect,
|
||||
QtCore.Qt.AlignCenter,
|
||||
date_value)
|
||||
|
||||
if is_failed:
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))
|
||||
|
||||
|
||||
class SyncServerDetailWindow(QtWidgets.QDialog):
|
||||
def __init__(self, sync_server, _id, project, parent=None):
|
||||
log.debug(
|
||||
"!!! SyncServerDetailWindow _id:: {}".format(_id))
|
||||
super(SyncServerDetailWindow, self).__init__(parent)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
self.resize(1000, 400)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = SyncRepresentationDetailWidget(sync_server, _id, project,
|
||||
parent=self)
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.message = QtWidgets.QLabel()
|
||||
self.message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(self.message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Detail")
|
||||
|
||||
|
||||
class SyncRepresentationErrorWindow(QtWidgets.QDialog):
|
||||
def __init__(self, _id, project, dt, tries, msg, parent=None):
|
||||
super(SyncRepresentationErrorWindow, self).__init__(parent)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
self.resize(900, 150)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
|
||||
container = SyncRepresentationErrorWidget(_id, dt, tries, msg,
|
||||
parent=self)
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Error Detail")
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
import time
|
||||
from openpype.api import Logger
|
||||
from openpype.api import Logger
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class SyncStatus:
|
||||
DO_NOTHING = 0
|
||||
DO_UPLOAD = 1
|
||||
DO_DOWNLOAD = 2
|
||||
|
||||
|
||||
def time_function(method):
|
||||
""" Decorator to print how much time function took.
|
||||
For debugging.
|
||||
|
|
|
|||
|
|
@ -45,11 +45,13 @@ class TimersManager(PypeModule, ITrayService, IIdleManager, IWebServerRoutes):
|
|||
timers_settings = modules_settings[self.name]
|
||||
|
||||
self.enabled = timers_settings["enabled"]
|
||||
auto_stop = timers_settings["auto_stop"]
|
||||
# When timer will stop if idle manager is running (minutes)
|
||||
full_time = int(timers_settings["full_time"] * 60)
|
||||
# How many minutes before the timer is stopped will popup the message
|
||||
message_time = int(timers_settings["message_time"] * 60)
|
||||
|
||||
self.auto_stop = auto_stop
|
||||
self.time_show_message = full_time - message_time
|
||||
self.time_stop_timer = full_time
|
||||
|
||||
|
|
@ -160,6 +162,9 @@ class TimersManager(PypeModule, ITrayService, IIdleManager, IWebServerRoutes):
|
|||
def callbacks_by_idle_time(self):
|
||||
"""Implementation of IIdleManager interface."""
|
||||
# Time when message is shown
|
||||
if not self.auto_stop:
|
||||
return {}
|
||||
|
||||
callbacks = collections.defaultdict(list)
|
||||
callbacks[self.time_show_message].append(lambda: self.time_callback(0))
|
||||
|
||||
|
|
|
|||
33
openpype/plugins/load/add_site.py
Normal file
33
openpype/plugins/load/add_site.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from avalon import api
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
class AddSyncSite(api.Loader):
|
||||
"""Add sync site to representation"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Add Sync Site"
|
||||
order = 2 # lower means better
|
||||
icon = "download"
|
||||
color = "#999999"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
self.log.info("Adding {} to representation: {}".format(
|
||||
data["site_name"], data["_id"]))
|
||||
self.add_site_to_representation(data["project_name"],
|
||||
data["_id"],
|
||||
data["site_name"])
|
||||
self.log.debug("Site added.")
|
||||
|
||||
@staticmethod
|
||||
def add_site_to_representation(project_name, representation_id, site_name):
|
||||
"""Adds new site to representation_id, resets if exists"""
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
sync_server.add_site(project_name, representation_id, site_name,
|
||||
force=True)
|
||||
|
||||
def filepath_from_context(self, context):
|
||||
"""No real file loading"""
|
||||
return ""
|
||||
|
|
@ -15,11 +15,12 @@ from openpype.api import Anatomy
|
|||
|
||||
|
||||
class DeleteOldVersions(api.Loader):
|
||||
|
||||
"""Deletes specific number of old version"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Delete Old Versions"
|
||||
order = 35
|
||||
icon = "trash"
|
||||
color = "#d8d8d8"
|
||||
|
||||
|
|
@ -421,8 +422,9 @@ class DeleteOldVersions(api.Loader):
|
|||
|
||||
|
||||
class CalculateOldVersions(DeleteOldVersions):
|
||||
|
||||
"""Calculate file size of old versions"""
|
||||
label = "Calculate Old Versions"
|
||||
order = 30
|
||||
|
||||
options = [
|
||||
qargparse.Integer(
|
||||
|
|
|
|||
33
openpype/plugins/load/remove_site.py
Normal file
33
openpype/plugins/load/remove_site.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from avalon import api
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
class RemoveSyncSite(api.Loader):
|
||||
"""Remove sync site and its files on representation"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Remove Sync Site"
|
||||
order = 4
|
||||
icon = "download"
|
||||
color = "#999999"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
self.log.info("Removing {} on representation: {}".format(
|
||||
data["site_name"], data["_id"]))
|
||||
self.remove_site_on_representation(data["project_name"],
|
||||
data["_id"],
|
||||
data["site_name"])
|
||||
self.log.debug("Site added.")
|
||||
|
||||
@staticmethod
|
||||
def remove_site_on_representation(project_name, representation_id,
|
||||
site_name):
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
sync_server.remove_site(project_name, representation_id,
|
||||
site_name, True)
|
||||
|
||||
def filepath_from_context(self, context):
|
||||
"""No real file loading"""
|
||||
return ""
|
||||
|
|
@ -40,7 +40,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
|
|||
continue
|
||||
|
||||
# exclude if not masterLayer True
|
||||
if not instance.data.get("masterLayer"):
|
||||
if not instance.data.get("heroTrack"):
|
||||
continue
|
||||
|
||||
# get asset build data if any available
|
||||
|
|
@ -50,7 +50,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin):
|
|||
|
||||
# suppose that all instances are Shots
|
||||
shot_data['entity_type'] = 'Shot'
|
||||
shot_data['tasks'] = instance.data.get("tasks") or []
|
||||
shot_data['tasks'] = instance.data.get("tasks") or {}
|
||||
shot_data["comments"] = instance.data.get("comments", [])
|
||||
|
||||
shot_data['custom_attributes'] = {
|
||||
|
|
|
|||
|
|
@ -976,6 +976,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
local_site = local_site_id
|
||||
|
||||
remote_site = sync_server_presets["config"].get("remote_site")
|
||||
if remote_site == local_site:
|
||||
remote_site = None
|
||||
|
||||
if remote_site == 'local':
|
||||
remote_site = local_site_id
|
||||
|
||||
|
|
|
|||
|
|
@ -54,11 +54,11 @@ def __main__():
|
|||
print("Got Pype location from environment: {}".format(
|
||||
os.environ.get('OPENPYPE_SETUP_PATH')))
|
||||
|
||||
pype_command = "pype.ps1"
|
||||
pype_command = "openpype.ps1"
|
||||
if platform.system().lower() == "linux":
|
||||
pype_command = "pype"
|
||||
elif platform.system().lower() == "windows":
|
||||
pype_command = "pype.bat"
|
||||
pype_command = "openpype.bat"
|
||||
|
||||
if kwargs.pype:
|
||||
pype_root = kwargs.pype
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@
|
|||
"resolutionHeight": 1080,
|
||||
"pixelAspect": 1.0,
|
||||
"applications": [
|
||||
"maya_2020",
|
||||
"nuke_12-2",
|
||||
"nukex_12-2",
|
||||
"hiero_12-2",
|
||||
"resolve_16",
|
||||
"houdini_18-5",
|
||||
"blender_2-90",
|
||||
"harmony_20",
|
||||
"photoshop_2021",
|
||||
"aftereffects_2021",
|
||||
"unreal_4-24"
|
||||
"maya/2020",
|
||||
"nuke/12-2",
|
||||
"nukex/12-2",
|
||||
"hiero/12-2",
|
||||
"resolve/16",
|
||||
"houdini/18-5",
|
||||
"blender/2-91",
|
||||
"harmony/20",
|
||||
"photoshop/2021",
|
||||
"aftereffects/2021",
|
||||
"unreal/4-24"
|
||||
],
|
||||
"tools_env": []
|
||||
}
|
||||
|
|
@ -7,6 +7,14 @@
|
|||
"not ready"
|
||||
]
|
||||
},
|
||||
"prepare_project": {
|
||||
"enabled": true,
|
||||
"role_list": [
|
||||
"Pypeclub",
|
||||
"Administrator",
|
||||
"Project manager"
|
||||
]
|
||||
},
|
||||
"sync_hier_entity_attributes": {
|
||||
"enabled": true,
|
||||
"interest_entity_types": [
|
||||
|
|
@ -195,7 +203,7 @@
|
|||
"publish": {
|
||||
"IntegrateFtrackNote": {
|
||||
"enabled": true,
|
||||
"note_with_intent_template": "",
|
||||
"note_with_intent_template": "{intent}: {comment}",
|
||||
"note_labels": []
|
||||
},
|
||||
"ValidateFtrackAttributes": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"ExtractJpegEXR": {
|
||||
"enabled": true,
|
||||
"ffmpeg_args": {
|
||||
"input": [],
|
||||
"input": [
|
||||
"-gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
"priority": 50,
|
||||
"primary_pool": "",
|
||||
"secondary_pool": "",
|
||||
"chunk_size": 0
|
||||
"chunk_size": 1000000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,13 @@
|
|||
"yetiRig": "ma"
|
||||
},
|
||||
"create": {
|
||||
"CreateLook": {
|
||||
"enabled": true,
|
||||
"make_tx": true,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateAnimation": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -38,12 +45,6 @@
|
|||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateLook": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
"Main"
|
||||
]
|
||||
},
|
||||
"CreateMayaScene": {
|
||||
"enabled": true,
|
||||
"defaults": [
|
||||
|
|
@ -313,8 +314,8 @@
|
|||
"rendererName": "vp2Renderer"
|
||||
},
|
||||
"Resolution": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"percent": 1.0,
|
||||
"mode": "Custom"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"load": "ctrl+alt+l",
|
||||
"manage": "ctrl+alt+m",
|
||||
"build_workfile": "ctrl+alt+b"
|
||||
}
|
||||
},
|
||||
"open_workfile_at_start": false,
|
||||
"create_initial_workfile": true
|
||||
},
|
||||
"create": {
|
||||
"CreateWriteRender": {
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
"ExtractThumbnailSP": {
|
||||
"ffmpeg_args": {
|
||||
"input": [
|
||||
"gamma 2.2"
|
||||
"-gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue