Merge branch 'develop' into feature/3.0_tvpaint_asset_name_validation

This commit is contained in:
iLLiCiTiT 2021-04-14 10:31:29 +02:00
commit 4eade6fc3c
54 changed files with 5128 additions and 3732 deletions

View file

@ -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

View file

@ -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)*

View file

@ -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():

View file

@ -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(),
}

View file

@ -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["sceneFrameStart"],
context.data["sceneFrameEnd"]
)
active = instance_data.get("active", True)
instance_data["active"] = active

View file

@ -141,11 +141,11 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
"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,
"sceneFrameStart": frame_start,
"sceneFrameEnd": frame_end,
"sceneFps": frame_rate,
"sceneFieldOrder": field_order
}
self.log.debug(
"Scene data: {}".format(json.dumps(scene_data, indent=4))

View file

@ -0,0 +1,36 @@
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 = {
"frameStart": context.data.get("sceneFrameStart"),
"frameEnd": context.data.get("sceneFrameEnd"),
"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)
)
)

View file

@ -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"
)

View file

@ -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)(?:\.|_).*"],

View file

@ -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()

View file

@ -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()

View file

@ -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()

View 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

View file

@ -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.

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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)

File diff suppressed because it is too large Load diff

View 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")

View file

@ -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.

View 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 ""

View file

@ -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(

View 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 ""

View file

@ -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

View file

@ -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": {

View file

@ -6,7 +6,9 @@
"ExtractJpegEXR": {
"enabled": true,
"ffmpeg_args": {
"input": [],
"input": [
"-gamma 2.2"
],
"output": []
}
},

View file

@ -313,8 +313,8 @@
"rendererName": "vp2Renderer"
},
"Resolution": {
"width": 1080,
"height": 1920,
"width": 1920,
"height": 1080,
"percent": 1.0,
"mode": "Custom"
},

View file

@ -116,7 +116,7 @@
"ExtractThumbnailSP": {
"ffmpeg_args": {
"input": [
"gamma 2.2"
"-gamma 2.2"
],
"output": []
}

View file

@ -0,0 +1,10 @@
{
"publish": {
"ValidateMissingLayers": {
"enabled": true,
"optional": true,
"active": true
}
},
"filters": {}
}

View file

@ -6,7 +6,7 @@
"host_name": "maya",
"environment": {
"PYTHONPATH": [
"{OPENPYPE_ROOT}/pype/hosts/maya/startup",
"{OPENPYPE_ROOT}/openpype/hosts/maya/startup",
"{OPENPYPE_ROOT}/repos/avalon-core/setup/maya",
"{OPENPYPE_ROOT}/repos/maya-look-assigner",
"{PYTHONPATH}"
@ -715,7 +715,7 @@
"{OPENPYPE_ROOT}/repos/avalon-core/setup/blender",
"{PYTHONPATH}"
],
"CREATE_NEW_CONSOLE": "yes"
"QT_PREFERRED_BINDING": "PySide2"
},
"variants": {
"2-83": {

View file

@ -82,6 +82,10 @@
"type": "schema",
"name": "schema_project_harmony"
},
{
"type": "schema",
"name": "schema_project_tvpaint"
},
{
"type": "schema",
"name": "schema_project_celaction"

View file

@ -36,6 +36,25 @@
}
]
},
{
"type": "dict",
"key": "prepare_project",
"label": "Prepare Project",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "list",
"key": "role_list",
"label": "Roles",
"object_type": "text"
}
]
},
{
"type": "dict",
"key": "sync_hier_entity_attributes",

View file

@ -0,0 +1,32 @@
{
"type": "dict",
"collapsible": true,
"key": "tvpaint",
"label": "TVPaint",
"is_file": true,
"children": [
{
"type": "dict",
"collapsible": true,
"key": "publish",
"label": "Publish plugins",
"is_file": true,
"children": [
{
"type": "schema_template",
"name": "template_publish_plugin",
"template_data": [
{
"key": "ValidateMissingLayers",
"label": "ValidateMissingLayers"
}
]
}
]
},
{
"type": "schema",
"name": "schema_publish_gui_filter"
}
]
}

View file

@ -1,4 +1,5 @@
import os
from openpype import resources
def load_stylesheet():
@ -9,4 +10,4 @@ def load_stylesheet():
def app_icon_path():
return os.path.join(os.path.dirname(__file__), "openpype_icon.png")
return resources.pype_icon_filepath()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""Package declaring Pype version."""
__version__ = "3.0.0-beta"
__version__ = "3.0.0-beta2"

View file

@ -1,6 +1,6 @@
[tool.poetry]
name = "OpenPype"
version = "3.0.0-alpha1"
version = "3.0.0-beta2"
description = "Multi-platform open-source pipeline built around the Avalon platform, expanding it with extra features and integrations."
authors = ["OpenPype Team <info@openpype.io>"]
license = "MIT License"

@ -1 +1 @@
Subproject commit bbba8765c431ee124590e4f12d2e56db4d62eacd
Subproject commit 911bd8999ab0030d0f7412dde6fd545c1a73b62d

View file

@ -0,0 +1,226 @@
---
id: artist_hosts_blender
title: Blender
sidebar_label: Blender
---
## OpenPype global tools
- [Set Context](artist_tools.md#set-context)
- [Work Files](artist_tools.md#workfiles)
- [Create](artist_tools.md#creator)
- [Load](artist_tools.md#loader)
- [Manage (Inventory)](artist_tools.md#inventory)
- [Publish](artist_tools.md#publisher)
- [Library Loader](artist_tools.md#library-loader)
## Working with OpenPype in Blender
OpenPype is here to ease you the burden of working on project with lots of
collaborators, worrying about naming, setting stuff, browsing through endless
directories, loading and exporting and so on. To achieve that, OpenPype is using
concept of being _"data driven"_. This means that what happens when publishing
is influenced by data in scene. This can by slightly confusing so let's get to
it with few examples.
## Setting scene data
Blender settings concerning framerate, resolution and frame range are handled
by OpenPype. If set correctly in Ftrack, Blender will automatically set the
values for you.
## Publishing models
### Intro
Publishing models in Blender is pretty straightforward. Create your model as you
need. You might need to adhere to specifications of your studio that can be different
between studios and projects but by default your geometry does not need any
other convention.
![Model example](assets/blender-model_example.jpg)
### Creating instance
Now create **Model instance** from it to let OpenPype know what in the scene you want to
publish. Go **OpenPype → Create... → Model**.
![Model create instance](assets/blender-model_create_instance.jpg)
`Asset` field is a name of asset you are working on - it should be already filled
with correct name as you've started Blender or switched context to specific asset. You
can edit that field to change it to different asset (but that one must already exists).
`Subset` field is a name you can decide on. It should describe what kind of data you
have in the model. For example, you can name it `Proxy` to indicate that this is
low resolution stuff. See [Subset](artist_concepts#subset).
<!-- :::note LOD support
By changing subset name you can take advantage of _LOD support_ in OpenPype. Your
asset can contain various resolution defined by different subsets. You can then
switch between them very easy using [Inventory (Manage)](artist_tools#inventory).
There LODs are conveniently grouped so they don't clutter Inventory view.
Name your subset like `main_LOD1`. Important part is that `_LOD1`. You can have as many LODs as you need.
::: -->
Read-only field just under it show final subset name, adding subset field to
name of the group you have selected.
`Use selection` checkbox will use whatever you have selected in Outliner to be
wrapped in Model instance. This is usually what you want. Click on **Create** button.
You'll notice then after you've created new Model instance, there is a new
collection in Outliner called after your asset and subset, in our case it is
`character1_modelDefault`. The assets selected when creating the Model instance
are linked in the new collection.
And that's it, you have your first model ready to publish.
Now save your scene (if you didn't do it already). You will notice that path
in Save dialog is already set to place where scenes related to modeling task on
your asset should reside. As in our case we are working on asset called
**character1** and on task **modeling**, path relative to your project directory will be
`project_XY/assets/character1/work/modeling`. The default name for the file will
be `project_XY_asset_task_version`, so in our case
`simonetest_character1_modeling_v001.blend`. Let's save it.
![Model create instance](assets/blender-save_modelling_file.jpg)
### Publishing models
Now let's publish it. Go **OpenPype → Publish...**. You will be presented with following window:
![Model publish](assets/blender-model_pre_publish.jpg)
Note that content of this window can differs by your pipeline configuration.
For more detail see [Publisher](artist_tools#publisher).
Items in left column are instances you will be publishing. You can disable them
by clicking on square next to them. White filled square indicate they are ready for
publishing, red means something went wrong either during collection phase
or publishing phase. Empty one with gray text is disabled.
See that in this case we are publishing from the scene file
`simonetest_character1_modeling_v001.blend` the Blender model named
`character1_modelDefault`.
Right column lists all tasks that are run during collection, validation,
extraction and integration phase. White items are optional and you can disable
them by clicking on them.
Lets do dry-run on publishing to see if we pass all validators. Click on flask
icon at the bottom. Validators are run. Ideally you will end up with everything
green in validator section.
### Fixing problems
For the sake of demonstration, I intentionally kept the model in Edit Mode, to
trigger the validator designed to check just this.
![Failed Model Validator](assets/blender-model_publish_error.jpg)
You can see our model is now marked red in left column and in right we have
red box next to `Mesh is in Object Mode` validator.
You can click on arrow next to it to see more details:
![Failed Model Validator details](assets/blender-model_error_details.jpg)
From there you can see in **Records** entry that there is problem with the
object `Suzanne`. Some validators have option to fix problem for you or just
select objects that cause trouble. This is the case with our failed validator.
In main overview you can notice little A in a circle next to validator
name. Right click on it and you can see menu item `select invalid`. This
will select offending object in Blender.
Fix is easy. Without closing Publisher window we just turn back the Object Mode.
Then we need to reset it to make it notice changes we've made. Click on arrow
circle button at the bottom and it will reset the Publisher to initial state. Run
validators again (flask icon) to see if everything is ok.
It should OK be now. Write some comment if you want and click play icon button
when ready.
Publish process will now take its course. Depending on data you are publishing
it can take a while. You should end up with everything green and message
**Finished successfully ...** You can now close publisher window.
To check for yourself that model is published, open
[Asset Loader](artist_tools#loader) - **OpenPype → Load...**.
There you should see your model, named `modelDefault`.
### Loading models
You can load model with [Loader](artist_tools.md#loader). Go **OpenPype → Load...**,
select your rig, right click on it and click **Link model (blend)**.
## Creating Rigs
Creating and publishing rigs with OpenPype follows similar workflow as with
other data types. Create your rig and mark parts of your hierarchy in sets to
help OpenPype validators and extractors to check it and publish it.
### Preparing rig for publish
When creating rigs in Blender, it is important to keep a specific structure for
the bones and the geometry. Let's first create a model and its rig. For
demonstration, I'll create a simple model for a robotic arm made of simple boxes.
![Blender - Simple model for rigging](assets/blender-rig_model_setup.jpg)
I have now created the armature `RIG_RobotArm`. While the naming is not important,
you can just adhere to your naming conventions, the hierarchy is. Once the models
are skinned to the armature, the geometry must be organized in a separate Collection.
In this case, I have the armature in the main Collection, and the geometry in
the `Geometry` Collection.
![Blender - Rig Hierarchy Example](assets/blender-rig_hierarchy_example.jpg)
When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype.
Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**.
![Blender - Rig Hierarchy Example](assets/blender-rig_create.jpg)
A new collection named after the selected Asset and Subset should have been created.
In our case, it is `character1_rigDefault`. All the selected armature and models
have been linked in this new collection. You should end up with something like
this:
![Blender - Rig Hierarchy Example](assets/blender-rig_hierarchy_before_publish.jpg)
### Publishing rigs
Publishing rig is done in same way as publishing everything else. Save your scene
and go **OpenPype → Publish**. For more detail see [Publisher](artist_tools#publisher).
### Loading rigs
You can load rig with [Loader](artist_tools.md#loader). Go **OpenPype → Load...**,
select your rig, right click on it and click **Link rig (blend)**.
## Layouts in Blender
A layout is a set of elements that populate a scene. OpenPype allows to version
and manage those sets.
### Publishing a layout
Working with Layout is easy. Just load your assets into scene with
[Loader](artist_tools.md#loader) (**OpenPype → Load...**). Populate your scene as
you wish, translate each piece to fit your need. When ready, select all imported
stuff and go **OpenPype → Create...** and select **Layout**. When selecting rigs,
you need to select only the armature, the geometry will automatically be included.
This will create set containing your selection and marking it for publishing.
Now you can publish is with **OpenPype → Publish**.
### Loading layouts
You can load a Layout using [Loader](artist_tools.md#loader)
(**OpenPype → Load...**). Select your layout, right click on it and
select **Link Layout (blend)**. This will populate your scene with all those
models you've put into layout.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -19,6 +19,7 @@ module.exports = {
"artist_hosts_nukestudio",
"artist_hosts_nuke",
"artist_hosts_maya",
"artist_hosts_blender",
"artist_hosts_harmony",
"artist_hosts_aftereffects",
"artist_hosts_photoshop",