diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index bc08868dc1..fc0705998f 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -10,6 +10,7 @@ on:
branches: [main]
paths:
- 'website/**'
+ workflow_dispatch:
jobs:
check-build:
diff --git a/.github/workflows/nightly_merge.yml b/.github/workflows/nightly_merge.yml
index 676b00a351..1d36c89cc7 100644
--- a/.github/workflows/nightly_merge.yml
+++ b/.github/workflows/nightly_merge.yml
@@ -1,4 +1,4 @@
-name: Nightly Merge
+name: Dev -> Main
on:
schedule:
@@ -14,10 +14,16 @@ jobs:
- name: 🚛 Checkout Code
uses: actions/checkout@v2
- - name: Merge development -> main
- uses: devmasx/merge-branch@v1.3.1
+ - name: 🔨 Merge develop to main
+ uses: everlytic/branch-merge@1.1.0
with:
- type: now
- from_branch: develop
- target_branch: main
- github_token: ${{ secrets.TOKEN }}
\ No newline at end of file
+ github_token: ${{ secrets.ADMIN_TOKEN }}
+ source_ref: 'develop'
+ target_branch: 'main'
+ commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
+
+ - name: Invoke pre-release workflow
+ uses: benc-uk/workflow-dispatch@v1
+ with:
+ workflow: Nightly Prerelease
+ token: ${{ secrets.ADMIN_TOKEN }}
\ No newline at end of file
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 0e6c7b5da9..d0853e74d6 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -1,8 +1,7 @@
name: Nightly Prerelease
on:
- push:
- branches: [main]
+ workflow_dispatch:
jobs:
@@ -43,14 +42,16 @@ jobs:
id: generate-full-changelog
uses: heinrichreimer/github-changelog-generator-action@v2.2
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- breakingLabel: '#### 💥 Breaking'
- enhancementLabel: '#### 🚀 Enhancements'
- bugsLabel: '#### 🐛 Bug fixes'
- deprecatedLabel: '#### ⚠️ Deprecations'
+ token: ${{ secrets.ADMIN_TOKEN }}
+ breakingLabel: '**💥 Breaking**'
+ enhancementLabel: '**🚀 Enhancements**'
+ bugsLabel: '**🐛 Bug fixes**'
+ deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
issues: false
issuesWoLabels: false
+ sinceTag: "3.0.0"
+ maxIssues: 100
pullRequests: true
prWoLabels: false
author: false
@@ -80,14 +81,20 @@ jobs:
git commit -m "[Automated] Bump version"
tag_name="CI/${{ steps.version.outputs.next_tag }}"
git tag -a $tag_name -m "nightly build"
- git push
- git push origin $tag_name
+
+ - name: Push to protected main branch
+ uses: CasperWA/push-protected@v2
+ with:
+ token: ${{ secrets.ADMIN_TOKEN }}
+ branch: main
+ tags: true
+ unprotect_reviews: true
- name: 🔨 Merge main back to develop
uses: everlytic/branch-merge@1.1.0
if: steps.version_type.outputs.type != 'skip'
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
+ github_token: ${{ secrets.ADMIN_TOKEN }}
source_ref: 'main'
target_branch: 'develop'
commit_message_template: '[Automated] Merged {source_ref} into {target_branch}'
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 782c9c8dda..37e1cb4b15 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,13 +1,14 @@
name: Stable Release
on:
- push:
- tags:
- - '*[0-9].*[0-9].*[0-9]*'
+ release:
+ types:
+ - prereleased
jobs:
create_release:
runs-on: ubuntu-latest
+ if: github.actor != 'pypebot'
steps:
- name: 🚛 Checkout Code
@@ -22,37 +23,31 @@ jobs:
- name: Install Python requirements
run: pip install gitpython semver
- - name: Set env
- run: |
- echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
-
- git config user.email ${{ secrets.CI_EMAIL }}
- git config user.name ${{ secrets.CI_USER }}
- git fetch
- git checkout -b main origin/main
- git tag -d ${GITHUB_REF#refs/*/}
- git push origin --delete ${GITHUB_REF#refs/*/}
- echo PREVIOUS_VERSION=`git describe --tags --match="[0-9]*" --abbrev=0` >> $GITHUB_ENV
-
- name: 💉 Inject new version into files
id: version
- if: steps.version_type.outputs.type != 'skip'
run: |
- python ./tools/ci_tools.py --version ${{ env.RELEASE_VERSION }}
+ echo ::set-output name=current_version::${GITHUB_REF#refs/*/}
+ RESULT=$(python ./tools/ci_tools.py --finalize ${GITHUB_REF#refs/*/})
+ LASTRELEASE=$(python ./tools/ci_tools.py --lastversion release)
+
+ echo ::set-output name=last_release::$LASTRELEASE
+ echo ::set-output name=release_tag::$RESULT
- name: "✏️ Generate full changelog"
- if: steps.version_type.outputs.type != 'skip'
+ if: steps.version.outputs.release_tag != 'skip'
id: generate-full-changelog
uses: heinrichreimer/github-changelog-generator-action@v2.2
with:
- token: ${{ secrets.GITHUB_TOKEN }}
- breakingLabel: '#### 💥 Breaking'
- enhancementLabel: '#### 🚀 Enhancements'
- bugsLabel: '#### 🐛 Bug fixes'
- deprecatedLabel: '#### ⚠️ Deprecations'
+ token: ${{ secrets.ADMIN_TOKEN }}
+ breakingLabel: '**💥 Breaking**'
+ enhancementLabel: '**🚀 Enhancements**'
+ bugsLabel: '**🐛 Bug fixes**'
+ deprecatedLabel: '**⚠️ Deprecations**'
addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
issues: false
issuesWoLabels: false
+ sinceTag: "3.0.0"
+ maxIssues: 100
pullRequests: true
prWoLabels: false
author: false
@@ -60,40 +55,78 @@ jobs:
compareLink: true
stripGeneratorNotice: true
verbose: true
- futureRelease: ${{ env.RELEASE_VERSION }}
+ futureRelease: ${{ steps.version.outputs.release_tag }}
excludeTagsRegex: "CI/.+"
releaseBranch: "main"
- - name: "🖨️ Print changelog to console"
- run: echo ${{ steps.generate-last-changelog.outputs.changelog }}
-
- name: 💾 Commit and Tag
id: git_commit
- if: steps.version_type.outputs.type != 'skip'
+ if: steps.version.outputs.release_tag != 'skip'
run: |
+ git config user.email ${{ secrets.CI_EMAIL }}
+ git config user.name ${{ secrets.CI_USER }}
git add .
git commit -m "[Automated] Release"
- tag_name="${{ env.RELEASE_VERSION }}"
- git push
- git tag -fa $tag_name -m "stable release"
- git push origin $tag_name
+ tag_name="${{ steps.version.outputs.release_tag }}"
+ git tag -a $tag_name -m "stable release"
- - name: "🚀 Github Release"
- uses: docker://antonyurchenko/git-release:latest
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DRAFT_RELEASE: "false"
- PRE_RELEASE: "false"
- CHANGELOG_FILE: "CHANGELOG.md"
- ALLOW_EMPTY_CHANGELOG: "false"
- ALLOW_TAG_PREFIX: "true"
-
-
- - name: 🔨 Merge main back to develop
- uses: everlytic/branch-merge@1.1.0
- if: steps.version_type.outputs.type != 'skip'
+ - name: 🔏 Push to protected main branch
+ if: steps.version.outputs.release_tag != 'skip'
+ uses: CasperWA/push-protected@v2
with:
- github_token: ${{ secrets.GITHUB_TOKEN }}
+ token: ${{ secrets.ADMIN_TOKEN }}
+ branch: main
+ tags: true
+ unprotect_reviews: true
+
+ - name: "✏️ Generate last changelog"
+ if: steps.version.outputs.release_tag != 'skip'
+ id: generate-last-changelog
+ uses: heinrichreimer/github-changelog-generator-action@v2.2
+ with:
+ token: ${{ secrets.ADMIN_TOKEN }}
+ breakingLabel: '**💥 Breaking**'
+ enhancementLabel: '**🚀 Enhancements**'
+ bugsLabel: '**🐛 Bug fixes**'
+ deprecatedLabel: '**⚠️ Deprecations**'
+ addSections: '{"documentation":{"prefix":"### 📖 Documentation","labels":["documentation"]},"tests":{"prefix":"### ✅ Testing","labels":["tests"]}}'
+ issues: false
+ issuesWoLabels: false
+ sinceTag: ${{ steps.version.outputs.last_release }}
+ maxIssues: 100
+ pullRequests: true
+ prWoLabels: false
+ author: false
+ unreleased: true
+ compareLink: true
+ stripGeneratorNotice: true
+ verbose: true
+ futureRelease: ${{ steps.version.outputs.release_tag }}
+ excludeTagsRegex: "CI/.+"
+ releaseBranch: "main"
+ stripHeaders: true
+ base: 'none'
+
+
+ - name: 🚀 Github Release
+ if: steps.version.outputs.release_tag != 'skip'
+ uses: ncipollo/release-action@v1
+ with:
+ body: ${{ steps.generate-last-changelog.outputs.changelog }}
+ tag: ${{ steps.version.outputs.release_tag }}
+ token: ${{ secrets.ADMIN_TOKEN }}
+
+ - name: ☠ Delete Pre-release
+ if: steps.version.outputs.release_tag != 'skip'
+ uses: cb80/delrel@latest
+ with:
+ tag: "${{ steps.version.outputs.current_version }}"
+
+ - name: 🔁 Merge main back to develop
+ if: steps.version.outputs.release_tag != 'skip'
+ uses: everlytic/branch-merge@1.1.0
+ with:
+ github_token: ${{ secrets.ADMIN_TOKEN }}
source_ref: 'main'
target_branch: 'develop'
commit_message_template: '[Automated] Merged release {source_ref} into {target_branch}'
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 754e3698e2..221a2f2241 100644
--- a/.gitignore
+++ b/.gitignore
@@ -97,4 +97,5 @@ website/.docusaurus
# Poetry
########
-.poetry/
\ No newline at end of file
+.poetry/
+.python-version
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 032f876aa3..96b90cd53e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,125 @@
# Changelog
+## [3.2.0-nightly.4](https://github.com/pypeclub/OpenPype/tree/HEAD)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.4...HEAD)
+
+**🚀 Enhancements**
+
+- Application executables with environment variables [\#1757](https://github.com/pypeclub/OpenPype/pull/1757)
+- Settings Hosts enum [\#1739](https://github.com/pypeclub/OpenPype/pull/1739)
+- Validate containers settings [\#1736](https://github.com/pypeclub/OpenPype/pull/1736)
+- PS - added loader from sequence [\#1726](https://github.com/pypeclub/OpenPype/pull/1726)
+- Autoupdate launcher [\#1725](https://github.com/pypeclub/OpenPype/pull/1725)
+- Subset template and TVPaint subset template docs [\#1717](https://github.com/pypeclub/OpenPype/pull/1717)
+- Toggle Ftrack upload in StandalonePublisher [\#1708](https://github.com/pypeclub/OpenPype/pull/1708)
+- Overscan color extract review [\#1701](https://github.com/pypeclub/OpenPype/pull/1701)
+- Nuke: Prerender Frame Range by default [\#1699](https://github.com/pypeclub/OpenPype/pull/1699)
+- Smoother edges of color triangle [\#1695](https://github.com/pypeclub/OpenPype/pull/1695)
+
+**🐛 Bug fixes**
+
+- Backend acre module commit update [\#1745](https://github.com/pypeclub/OpenPype/pull/1745)
+- hiero: precollect instances failing when audio selected [\#1743](https://github.com/pypeclub/OpenPype/pull/1743)
+- Hiero: creator instance error [\#1742](https://github.com/pypeclub/OpenPype/pull/1742)
+- Nuke: fixing render creator for no selection format failing [\#1741](https://github.com/pypeclub/OpenPype/pull/1741)
+- Local settings UI crash on missing defaults [\#1737](https://github.com/pypeclub/OpenPype/pull/1737)
+- TVPaint white background on thumbnail [\#1735](https://github.com/pypeclub/OpenPype/pull/1735)
+- Ftrack missing custom attribute message [\#1734](https://github.com/pypeclub/OpenPype/pull/1734)
+- Launcher project changes [\#1733](https://github.com/pypeclub/OpenPype/pull/1733)
+- Ftrack sync status [\#1732](https://github.com/pypeclub/OpenPype/pull/1732)
+- TVPaint use layer name for default variant [\#1724](https://github.com/pypeclub/OpenPype/pull/1724)
+- Default subset template for TVPaint review and workfile families [\#1716](https://github.com/pypeclub/OpenPype/pull/1716)
+- Maya: Extract review hotfix [\#1714](https://github.com/pypeclub/OpenPype/pull/1714)
+- Settings: Imageio improving granularity [\#1711](https://github.com/pypeclub/OpenPype/pull/1711)
+- Application without executables [\#1679](https://github.com/pypeclub/OpenPype/pull/1679)
+
+**Merged pull requests:**
+
+- TVPaint ftrack family [\#1755](https://github.com/pypeclub/OpenPype/pull/1755)
+
+## [2.18.4](https://github.com/pypeclub/OpenPype/tree/2.18.4) (2021-06-24)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/2.18.3...2.18.4)
+
+**Merged pull requests:**
+
+- celaction fixes [\#1754](https://github.com/pypeclub/OpenPype/pull/1754)
+- celaciton: audio subset changed data structure [\#1750](https://github.com/pypeclub/OpenPype/pull/1750)
+
+## [2.18.3](https://github.com/pypeclub/OpenPype/tree/2.18.3) (2021-06-23)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.2.0-nightly.2...2.18.3)
+
+**🐛 Bug fixes**
+
+- Tools names forwards compatibility [\#1727](https://github.com/pypeclub/OpenPype/pull/1727)
+
+**Merged pull requests:**
+
+- global: removing obsolete ftrack validator plugin [\#1710](https://github.com/pypeclub/OpenPype/pull/1710)
+
+## [2.18.2](https://github.com/pypeclub/OpenPype/tree/2.18.2) (2021-06-16)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.1.0...2.18.2)
+
+**🚀 Enhancements**
+
+- StandalonePublisher: adding exception for adding `delete` tag to repre [\#1650](https://github.com/pypeclub/OpenPype/pull/1650)
+
+**🐛 Bug fixes**
+
+- Maya: Extract review hotfix - 2.x backport [\#1713](https://github.com/pypeclub/OpenPype/pull/1713)
+- StandalonePublisher: instance data attribute `keepSequence` [\#1668](https://github.com/pypeclub/OpenPype/pull/1668)
+
+**Merged pull requests:**
+
+- 1698 Nuke: Prerender Frame Range by default [\#1709](https://github.com/pypeclub/OpenPype/pull/1709)
+
+## [3.1.0](https://github.com/pypeclub/OpenPype/tree/3.1.0) (2021-06-15)
+
+[Full Changelog](https://github.com/pypeclub/OpenPype/compare/CI/3.1.0-nightly.4...3.1.0)
+
+**🚀 Enhancements**
+
+- Log Viewer with OpenPype style [\#1703](https://github.com/pypeclub/OpenPype/pull/1703)
+- Scrolling in OpenPype info widget [\#1702](https://github.com/pypeclub/OpenPype/pull/1702)
+- OpenPype style in modules [\#1694](https://github.com/pypeclub/OpenPype/pull/1694)
+- Sort applications and tools alphabetically in Settings UI [\#1689](https://github.com/pypeclub/OpenPype/pull/1689)
+- \#683 - Validate Frame Range in Standalone Publisher [\#1683](https://github.com/pypeclub/OpenPype/pull/1683)
+- Hiero: old container versions identify with red color [\#1682](https://github.com/pypeclub/OpenPype/pull/1682)
+- Project Manger: Default name column width [\#1669](https://github.com/pypeclub/OpenPype/pull/1669)
+- Remove outline in stylesheet [\#1667](https://github.com/pypeclub/OpenPype/pull/1667)
+- TVPaint: Creator take layer name as default value for subset variant [\#1663](https://github.com/pypeclub/OpenPype/pull/1663)
+- TVPaint custom subset template [\#1662](https://github.com/pypeclub/OpenPype/pull/1662)
+- Editorial: conform assets validator [\#1659](https://github.com/pypeclub/OpenPype/pull/1659)
+- Feature Slack integration [\#1657](https://github.com/pypeclub/OpenPype/pull/1657)
+- Nuke - Publish simplification [\#1653](https://github.com/pypeclub/OpenPype/pull/1653)
+- \#1333 - added tooltip hints to Pyblish buttons [\#1649](https://github.com/pypeclub/OpenPype/pull/1649)
+
+**🐛 Bug fixes**
+
+- Nuke: broken publishing rendered frames [\#1707](https://github.com/pypeclub/OpenPype/pull/1707)
+- Standalone publisher Thumbnail export args [\#1705](https://github.com/pypeclub/OpenPype/pull/1705)
+- Bad zip can break OpenPype start [\#1691](https://github.com/pypeclub/OpenPype/pull/1691)
+- Hiero: published whole edit mov [\#1687](https://github.com/pypeclub/OpenPype/pull/1687)
+- Ftrack subprocess handle of stdout/stderr [\#1675](https://github.com/pypeclub/OpenPype/pull/1675)
+- Settings list race condifiton and mutable dict list conversion [\#1671](https://github.com/pypeclub/OpenPype/pull/1671)
+- Mac launch arguments fix [\#1660](https://github.com/pypeclub/OpenPype/pull/1660)
+- Fix missing dbm python module [\#1652](https://github.com/pypeclub/OpenPype/pull/1652)
+- Transparent branches in view on Mac [\#1648](https://github.com/pypeclub/OpenPype/pull/1648)
+- Add asset on task item [\#1646](https://github.com/pypeclub/OpenPype/pull/1646)
+- Project manager save and queue [\#1645](https://github.com/pypeclub/OpenPype/pull/1645)
+- New project anatomy values [\#1644](https://github.com/pypeclub/OpenPype/pull/1644)
+
+**Merged pull requests:**
+
+- update dependencies [\#1697](https://github.com/pypeclub/OpenPype/pull/1697)
+- Bump normalize-url from 4.5.0 to 4.5.1 in /website [\#1686](https://github.com/pypeclub/OpenPype/pull/1686)
+- Use poetry to build / publish OpenPype wheel [\#1636](https://github.com/pypeclub/OpenPype/pull/1636)
+
+# Changelog
+
## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)
@@ -1565,10 +1685,9 @@ 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)*
-\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
+
+
diff --git a/HISTORY.md b/HISTORY.md
index ae4492bd7a..032f876aa3 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -3,7 +3,7 @@
## [3.0.0](https://github.com/pypeclub/openpype/tree/3.0.0)
-[Full Changelog](https://github.com/pypeclub/openpype/compare/CI/3.0.0-rc.6...3.0.0)
+[Full Changelog](https://github.com/pypeclub/openpype/compare/2.18.1...3.0.0)
### Configuration
- Studio Settings GUI: no more json configuration files.
diff --git a/README.md b/README.md
index 566e226538..6b4495c9b6 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
OpenPype
====
-[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml)  
+[](https://github.com/pypeclub/pype/actions/workflows/documentation.yml) 
diff --git a/igniter/bootstrap_repos.py b/igniter/bootstrap_repos.py
index 7c4f8b4b69..6eaea27116 100644
--- a/igniter/bootstrap_repos.py
+++ b/igniter/bootstrap_repos.py
@@ -972,8 +972,12 @@ class BootstrapRepos:
"openpype/version.py") as version_file:
zip_version = {}
exec(version_file.read(), zip_version)
- version_check = OpenPypeVersion(
- version=zip_version["__version__"])
+ try:
+ version_check = OpenPypeVersion(
+ version=zip_version["__version__"])
+ except ValueError as e:
+ self._print(str(e), True)
+ return False
version_main = version_check.get_main_version() # noqa: E501
detected_main = detected_version.get_main_version() # noqa: E501
diff --git a/igniter/version.py b/igniter/version.py
index 3c627aaa1a..56d58f7f60 100644
--- a/igniter/version.py
+++ b/igniter/version.py
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
"""Definition of Igniter version."""
-__version__ = "1.0.0"
+__version__ = "1.0.1"
diff --git a/openpype/cli.py b/openpype/cli.py
index 9f4561b82e..48951c7287 100644
--- a/openpype/cli.py
+++ b/openpype/cli.py
@@ -115,7 +115,9 @@ def extractenvironments(output_json_path, project, asset, task, app):
@main.command()
@click.argument("paths", nargs=-1)
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
-def publish(debug, paths):
+@click.option("-t", "--targets", help="Targets module", default=None,
+ multiple=True)
+def publish(debug, paths, targets):
"""Start CLI publishing.
Publish collects json from paths provided as an argument.
@@ -123,7 +125,7 @@ def publish(debug, paths):
"""
if debug:
os.environ['OPENPYPE_DEBUG'] = '3'
- PypeCommands.publish(list(paths))
+ PypeCommands.publish(list(paths), targets)
@main.command()
diff --git a/openpype/hooks/pre_mac_launch.py b/openpype/hooks/pre_mac_launch.py
index 3f07ae07db..f85557a4f0 100644
--- a/openpype/hooks/pre_mac_launch.py
+++ b/openpype/hooks/pre_mac_launch.py
@@ -31,4 +31,4 @@ class LaunchWithTerminal(PreLaunchHook):
if len(self.launch_context.launch_args) > 1:
self.launch_context.launch_args.insert(1, "--args")
# Prepend open arguments
- self.launch_context.launch_args.insert(0, ["open", "-a"])
+ self.launch_context.launch_args.insert(0, ["open", "-na"])
diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py
index d8a235be77..876fae5da9 100644
--- a/openpype/hosts/hiero/api/lib.py
+++ b/openpype/hosts/hiero/api/lib.py
@@ -190,7 +190,7 @@ def get_track_items(
if not item.isEnabled():
continue
if track_item_name:
- if item.name() in track_item_name:
+ if track_item_name in item.name():
return item
# make sure only track items with correct track names are added
if track_name and track_name in track.name():
@@ -949,6 +949,54 @@ def sync_clip_name_to_data_asset(track_items_list):
print("asset was changed in clip: {}".format(ti_name))
+def check_inventory_versions():
+ """
+ Actual version color idetifier of Loaded containers
+
+ Check all track items and filter only
+ Loader nodes for its version. It will get all versions from database
+ and check if the node is having actual version. If not then it will color
+ it to red.
+ """
+ from . import parse_container
+ from avalon import io
+
+ # presets
+ clip_color_last = "green"
+ clip_color = "red"
+
+ # get all track items from current timeline
+ for track_item in get_track_items():
+ container = parse_container(track_item)
+
+ if container:
+ # get representation from io
+ representation = io.find_one({
+ "type": "representation",
+ "_id": io.ObjectId(container["representation"])
+ })
+
+ # Get start frame from version data
+ version = io.find_one({
+ "type": "version",
+ "_id": representation["parent"]
+ })
+
+ # get all versions in list
+ versions = io.find({
+ "type": "version",
+ "parent": version["parent"]
+ }).distinct('name')
+
+ max_version = max(versions)
+
+ # set clip colour
+ if version.get("name") == max_version:
+ track_item.source().binItem().setColor(clip_color_last)
+ else:
+ track_item.source().binItem().setColor(clip_color)
+
+
def selection_changed_timeline(event):
"""Callback on timeline to check if asset in data is the same as clip name.
@@ -958,9 +1006,15 @@ def selection_changed_timeline(event):
timeline_editor = event.sender
selection = timeline_editor.selection()
+ selection = [ti for ti in selection
+ if isinstance(ti, hiero.core.TrackItem)]
+
# run checking function
sync_clip_name_to_data_asset(selection)
+ # also mark old versions of loaded containers
+ check_inventory_versions()
+
def before_project_save(event):
track_items = get_track_items(
@@ -972,3 +1026,6 @@ def before_project_save(event):
# run checking function
sync_clip_name_to_data_asset(track_items)
+
+ # also mark old versions of loaded containers
+ check_inventory_versions()
diff --git a/openpype/hosts/hiero/plugins/create/create_shot_clip.py b/openpype/hosts/hiero/plugins/create/create_shot_clip.py
index 25be9f090b..0c5bf93a3f 100644
--- a/openpype/hosts/hiero/plugins/create/create_shot_clip.py
+++ b/openpype/hosts/hiero/plugins/create/create_shot_clip.py
@@ -1,3 +1,4 @@
+from copy import deepcopy
import openpype.hosts.hiero.api as phiero
# from openpype.hosts.hiero.api import plugin, lib
# reload(lib)
@@ -206,20 +207,24 @@ class CreateShotClip(phiero.Creator):
presets = None
def process(self):
+ # Creator copy of object attributes that are modified during `process`
+ presets = deepcopy(self.presets)
+ gui_inputs = deepcopy(self.gui_inputs)
+
# get key pares from presets and match it on ui inputs
- for k, v in self.gui_inputs.items():
+ for k, v in gui_inputs.items():
if v["type"] in ("dict", "section"):
# nested dictionary (only one level allowed
# for sections and dict)
for _k, _v in v["value"].items():
- if self.presets.get(_k):
- self.gui_inputs[k][
- "value"][_k]["value"] = self.presets[_k]
- if self.presets.get(k):
- self.gui_inputs[k]["value"] = self.presets[k]
+ if presets.get(_k):
+ gui_inputs[k][
+ "value"][_k]["value"] = presets[_k]
+ if presets.get(k):
+ gui_inputs[k]["value"] = presets[k]
# open widget for plugins inputs
- widget = self.widget(self.gui_name, self.gui_info, self.gui_inputs)
+ widget = self.widget(self.gui_name, self.gui_info, gui_inputs)
widget.exec_()
if len(self.selected) < 1:
diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py
index 92e0a70d15..4984849aa7 100644
--- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py
+++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py
@@ -41,16 +41,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
# process all sellected timeline track items
for track_item in selected_timeline_items:
-
data = {}
clip_name = track_item.name()
source_clip = track_item.source()
-
- # get clips subtracks and anotations
- annotations = self.clip_annotations(source_clip)
- subtracks = self.clip_subtrack(track_item)
- self.log.debug("Annotations: {}".format(annotations))
- self.log.debug(">> Subtracks: {}".format(subtracks))
+ self.log.debug("clip_name: {}".format(clip_name))
# get openpype tag data
tag_data = phiero.get_track_item_pype_data(track_item)
@@ -62,6 +56,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
if tag_data.get("id") != "pyblish.avalon.instance":
continue
+ # get clips subtracks and anotations
+ annotations = self.clip_annotations(source_clip)
+ subtracks = self.clip_subtrack(track_item)
+ self.log.debug("Annotations: {}".format(annotations))
+ self.log.debug(">> Subtracks: {}".format(subtracks))
+
# solve handles length
tag_data["handleStart"] = min(
tag_data["handleStart"], int(track_item.handleInLength()))
@@ -128,7 +128,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin):
"_ instance.data: {}".format(pformat(instance.data)))
if not with_audio:
- return
+ continue
# create audio subset instance
self.create_audio_instance(context, **data)
diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py
index fa1ce7f9a9..57e3f478f1 100644
--- a/openpype/hosts/maya/plugins/publish/extract_playblast.py
+++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py
@@ -72,7 +72,7 @@ class ExtractPlayblast(openpype.api.Extractor):
# Isolate view is requested by having objects in the set besides a
# camera.
- if preset.pop("isolate_view", False) or instance.data.get("isolate"):
+ if preset.pop("isolate_view", False) and instance.data.get("isolate"):
preset["isolate"] = instance.data["setMembers"]
# Show/Hide image planes on request.
diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py
index 5a91888781..aa8adc3986 100644
--- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py
+++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py
@@ -75,7 +75,7 @@ class ExtractThumbnail(openpype.api.Extractor):
# Isolate view is requested by having objects in the set besides a
# camera.
- if preset.pop("isolate_view", False) or instance.data.get("isolate"):
+ if preset.pop("isolate_view", False) and instance.data.get("isolate"):
preset["isolate"] = instance.data["setMembers"]
with maintained_time():
diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py
index 3c41574dbf..d7f3fdc6ba 100644
--- a/openpype/hosts/nuke/api/lib.py
+++ b/openpype/hosts/nuke/api/lib.py
@@ -286,7 +286,8 @@ def add_button_write_to_read(node):
node.addKnob(knob)
-def create_write_node(name, data, input=None, prenodes=None, review=True):
+def create_write_node(name, data, input=None, prenodes=None,
+ review=True, linked_knobs=None):
''' Creating write node which is group node
Arguments:
@@ -298,18 +299,21 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
review (bool): adding review knob
Example:
- prenodes = [(
- "NameNode", # string
- "NodeClass", # string
- ( # OrderDict: knob and values pairs
- ("knobName", "knobValue"),
- ("knobName", "knobValue")
- ),
- ( # list outputs
- "firstPostNodeName",
- "secondPostNodeName"
- )
- )
+ prenodes = [
+ {
+ "nodeName": {
+ "class": "" # string
+ "knobs": [
+ ("knobName": value),
+ ...
+ ],
+ "dependent": [
+ following_node_01,
+ ...
+ ]
+ }
+ },
+ ...
]
Return:
@@ -385,35 +389,42 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
prev_node.hideControlPanel()
# creating pre-write nodes `prenodes`
if prenodes:
- for name, klass, properties, set_output_to in prenodes:
+ for node in prenodes:
+ # get attributes
+ name = node["name"]
+ klass = node["class"]
+ knobs = node["knobs"]
+ dependent = node["dependent"]
+
# create node
now_node = nuke.createNode(klass, "name {}".format(name))
now_node.hideControlPanel()
# add data to knob
- for k, v in properties:
+ for _knob in knobs:
+ knob, value = _knob
try:
- now_node[k].value()
+ now_node[knob].value()
except NameError:
log.warning(
"knob `{}` does not exist on node `{}`".format(
- k, now_node["name"].value()
+ knob, now_node["name"].value()
))
continue
- if k and v:
- now_node[k].setValue(str(v))
+ if knob and value:
+ now_node[knob].setValue(value)
# connect to previous node
- if set_output_to:
- if isinstance(set_output_to, (tuple or list)):
- for i, node_name in enumerate(set_output_to):
+ if dependent:
+ if isinstance(dependent, (tuple or list)):
+ for i, node_name in enumerate(dependent):
input_node = nuke.createNode(
"Input", "name {}".format(node_name))
input_node.hideControlPanel()
now_node.setInput(1, input_node)
- elif isinstance(set_output_to, str):
+ elif isinstance(dependent, str):
input_node = nuke.createNode(
"Input", "name {}".format(node_name))
input_node.hideControlPanel()
@@ -455,12 +466,16 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
GN.addKnob(nuke.Text_Knob('', 'Rendering'))
# Add linked knobs.
- linked_knob_names = [
- "_grp-start_",
- "use_limit", "first", "last",
- "_grp-end_",
- "Render"
- ]
+ linked_knob_names = []
+
+ # add input linked knobs and create group only if any input
+ if linked_knobs:
+ linked_knob_names.append("_grp-start_")
+ linked_knob_names.extend(linked_knobs)
+ linked_knob_names.append("_grp-end_")
+
+ linked_knob_names.append("Render")
+
for name in linked_knob_names:
if "_grp-start_" in name:
knob = nuke.Tab_Knob(
@@ -471,13 +486,20 @@ def create_write_node(name, data, input=None, prenodes=None, review=True):
"rnd_attr_end", "Rendering attributes", nuke.TABENDGROUP)
GN.addKnob(knob)
else:
- link = nuke.Link_Knob("")
- link.makeLink(write_node.name(), name)
- link.setName(name)
- if "Render" in name:
- link.setLabel("Render Local")
- link.setFlag(0x1000)
- GN.addKnob(link)
+ if "___" in name:
+ # add devider
+ GN.addKnob(nuke.Text_Knob(""))
+ else:
+ # add linked knob by name
+ link = nuke.Link_Knob("")
+ link.makeLink(write_node.name(), name)
+ link.setName(name)
+
+ # make render
+ if "Render" in name:
+ link.setLabel("Render Local")
+ link.setFlag(0x1000)
+ GN.addKnob(link)
# adding write to read button
add_button_write_to_read(GN)
diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py
index 6e1a2ddd96..1b925014ad 100644
--- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py
+++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py
@@ -103,7 +103,8 @@ class CreateWritePrerender(plugin.PypeCreator):
write_data,
input=selected_node,
prenodes=[],
- review=False)
+ review=False,
+ linked_knobs=["channels", "___", "first", "last", "use_limit"])
# relinking to collected connections
for i, input in enumerate(inputs):
@@ -122,19 +123,9 @@ class CreateWritePrerender(plugin.PypeCreator):
w_node = n
write_node.end()
- # add inner write node Tab
- write_node.addKnob(nuke.Tab_Knob("WriteLinkedKnobs"))
-
- # linking knobs to group property panel
- linking_knobs = ["channels", "___", "first", "last", "use_limit"]
- for k in linking_knobs:
- if "___" in k:
- write_node.addKnob(nuke.Text_Knob(''))
- else:
- lnk = nuke.Link_Knob(k)
- lnk.makeLink(w_node.name(), k)
- lnk.setName(k.replace('_', ' ').capitalize())
- lnk.clearFlag(nuke.STARTLINE)
- write_node.addKnob(lnk)
+ if self.presets.get("use_range_limit"):
+ w_node["use_limit"].setValue(True)
+ w_node["first"].setValue(nuke.root()["first_frame"].value())
+ w_node["last"].setValue(nuke.root()["last_frame"].value())
return write_node
diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py
index 04983e9c75..a1381122ee 100644
--- a/openpype/hosts/nuke/plugins/create/create_write_render.py
+++ b/openpype/hosts/nuke/plugins/create/create_write_render.py
@@ -99,10 +99,35 @@ class CreateWriteRender(plugin.PypeCreator):
"fpath_template": ("{work}/renders/nuke/{subset}"
"/{subset}.{frame}.{ext}")})
+ # add crop node to cut off all outside of format bounding box
+ # get width and height
+ try:
+ width, height = (selected_node.width(), selected_node.height())
+ except AttributeError:
+ actual_format = nuke.root().knob('format').value()
+ width, height = (actual_format.width(), actual_format.height())
+
+ _prenodes = [
+ {
+ "name": "Crop01",
+ "class": "Crop",
+ "knobs": [
+ ("box", [
+ 0.0,
+ 0.0,
+ width,
+ height
+ ])
+ ],
+ "dependent": None
+ }
+ ]
+
write_node = lib.create_write_node(
self.data["subset"],
write_data,
- input=selected_node)
+ input=selected_node,
+ prenodes=_prenodes)
# relinking to collected connections
for i, input in enumerate(inputs):
diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py
index cdb0589525..00d96c6cd1 100644
--- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py
+++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py
@@ -81,17 +81,18 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
if target == "Use existing frames":
# Local rendering
self.log.info("flagged for no render")
+ families.append(family)
elif target == "Local":
# Local rendering
self.log.info("flagged for local render")
families.append("{}.local".format(family))
- family = families_ak.lower()
elif target == "On farm":
# Farm rendering
self.log.info("flagged for farm render")
instance.data["transfer"] = False
families.append("{}.farm".format(family))
- family = families_ak.lower()
+
+ family = families_ak.lower()
node.begin()
for i in nuke.allNodes():
diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py
index 5eaac89e84..0b5fbc0479 100644
--- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py
+++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py
@@ -1,5 +1,6 @@
import os
import re
+from pprint import pformat
import nuke
import pyblish.api
import openpype.api as pype
@@ -17,6 +18,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
def process(self, instance):
_families_test = [instance.data["family"]] + instance.data["families"]
+ self.log.debug("_families_test: {}".format(_families_test))
node = None
for x in instance:
@@ -133,22 +135,29 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
"outputDir": output_dir,
"ext": ext,
"label": label,
- "handleStart": handle_start,
- "handleEnd": handle_end,
- "frameStart": first_frame + handle_start,
- "frameEnd": last_frame - handle_end,
- "frameStartHandle": first_frame,
- "frameEndHandle": last_frame,
"outputType": output_type,
"colorspace": colorspace,
"deadlineChunkSize": deadlineChunkSize,
"deadlinePriority": deadlinePriority
})
- if "prerender" in _families_test:
+ if self.is_prerender(_families_test):
instance.data.update({
- "family": "prerender",
- "families": []
+ "handleStart": 0,
+ "handleEnd": 0,
+ "frameStart": first_frame,
+ "frameEnd": last_frame,
+ "frameStartHandle": first_frame,
+ "frameEndHandle": last_frame,
+ })
+ else:
+ instance.data.update({
+ "handleStart": handle_start,
+ "handleEnd": handle_end,
+ "frameStart": first_frame + handle_start,
+ "frameEnd": last_frame - handle_end,
+ "frameStartHandle": first_frame,
+ "frameEndHandle": last_frame,
})
# * Add audio to instance if exists.
@@ -170,4 +179,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
"filename": api.get_representation_path(repre_doc)
}]
- self.log.debug("instance.data: {}".format(instance.data))
+ self.log.debug("instance.data: {}".format(pformat(instance.data)))
+
+ def is_prerender(self, families):
+ return next((f for f in families if "prerender" in f), None)
diff --git a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py
index 8b71aff1ac..0c88014649 100644
--- a/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py
+++ b/openpype/hosts/nuke/plugins/publish/validate_rendered_frames.py
@@ -61,7 +61,6 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
hosts = ["nuke", "nukestudio"]
actions = [RepairCollectionActionToLocal, RepairCollectionActionToFarm]
-
def process(self, instance):
for repre in instance.data["representations"]:
@@ -78,10 +77,10 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
collection = collections[0]
- frame_length = int(
- instance.data["frameEndHandle"]
- - instance.data["frameStartHandle"] + 1
- )
+ fstartH = instance.data["frameStartHandle"]
+ fendH = instance.data["frameEndHandle"]
+
+ frame_length = int(fendH - fstartH + 1)
if frame_length != 1:
if len(collections) != 1:
@@ -95,7 +94,16 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
raise ValidationException(msg)
collected_frames_len = int(len(collection.indexes))
+ coll_start = min(collection.indexes)
+ coll_end = max(collection.indexes)
+
self.log.info("frame_length: {}".format(frame_length))
+ self.log.info("collected_frames_len: {}".format(
+ collected_frames_len))
+ self.log.info("fstartH-fendH: {}-{}".format(fstartH, fendH))
+ self.log.info(
+ "coll_start-coll_end: {}-{}".format(coll_start, coll_end))
+
self.log.info(
"len(collection.indexes): {}".format(collected_frames_len)
)
@@ -103,8 +111,11 @@ class ValidateRenderedFrames(pyblish.api.InstancePlugin):
if ("slate" in instance.data["families"]) \
and (frame_length != collected_frames_len):
collected_frames_len -= 1
+ fstartH += 1
- assert (collected_frames_len == frame_length), (
+ assert ((collected_frames_len >= frame_length)
+ and (coll_start <= fstartH)
+ and (coll_end >= fendH)), (
"{} missing frames. Use repair to render all frames"
).format(__name__)
diff --git a/openpype/hosts/nuke/plugins/publish/validate_write_bounding_box.py b/openpype/hosts/nuke/plugins/publish/validate_write_bounding_box.py
deleted file mode 100644
index e4b7c77a25..0000000000
--- a/openpype/hosts/nuke/plugins/publish/validate_write_bounding_box.py
+++ /dev/null
@@ -1,106 +0,0 @@
-import nuke
-
-import pyblish.api
-
-
-class RepairNukeBoundingBoxAction(pyblish.api.Action):
-
- label = "Repair"
- icon = "wrench"
- on = "failed"
-
- def process(self, context, plugin):
-
- # Get the errored instances
- failed = []
- for result in context.data["results"]:
- if (result["error"] is not None and result["instance"] is not None
- and result["instance"] not in failed):
- failed.append(result["instance"])
-
- # Apply pyblish.logic to get the instances for the plug-in
- instances = pyblish.api.instances_by_plugin(failed, plugin)
-
- for instance in instances:
- crop = instance[0].dependencies()[0]
- if crop.Class() != "Crop":
- crop = nuke.nodes.Crop(inputs=[instance[0].input(0)])
-
- xpos = instance[0].xpos()
- ypos = instance[0].ypos() - 26
-
- dependent_ypos = instance[0].dependencies()[0].ypos()
- if (instance[0].ypos() - dependent_ypos) <= 51:
- xpos += 110
-
- crop.setXYpos(xpos, ypos)
-
- instance[0].setInput(0, crop)
-
- crop["box"].setValue(
- (
- 0.0,
- 0.0,
- instance[0].input(0).width(),
- instance[0].input(0).height()
- )
- )
-
-
-class ValidateNukeWriteBoundingBox(pyblish.api.InstancePlugin):
- """Validates write bounding box.
-
- Ffmpeg does not support bounding boxes outside of the image
- resolution a crop is needed. This needs to validate all frames, as each
- rendered exr can break the ffmpeg transcode.
- """
-
- order = pyblish.api.ValidatorOrder
- optional = True
- families = ["render", "render.local", "render.farm"]
- label = "Write Bounding Box"
- hosts = ["nuke"]
- actions = [RepairNukeBoundingBoxAction]
-
- def process(self, instance):
-
- # Skip bounding box check if a crop node exists.
- if instance[0].dependencies()[0].Class() == "Crop":
- return
-
- msg = "Bounding box is outside the format."
- assert self.check_bounding_box(instance), msg
-
- def check_bounding_box(self, instance):
- node = instance[0]
-
- first_frame = instance.data["frameStart"]
- last_frame = instance.data["frameEnd"]
-
- format_width = node.format().width()
- format_height = node.format().height()
-
- # The trick is that we need to execute() some node every time we go to
- # a next frame, to update the context.
- # So we create a CurveTool that we can execute() on every frame.
- temporary_node = nuke.nodes.CurveTool()
- bbox_check = True
- for frame in range(first_frame, last_frame + 1):
- # Workaround to update the tree
- nuke.execute(temporary_node, frame, frame)
-
- x = node.bbox().x()
- y = node.bbox().y()
- w = node.bbox().w()
- h = node.bbox().h()
-
- if x < 0 or (x + w) > format_width:
- bbox_check = False
- break
-
- if y < 0 or (y + h) > format_height:
- bbox_check = False
- break
-
- nuke.delete(temporary_node)
- return bbox_check
diff --git a/openpype/hosts/photoshop/plugins/lib.py b/openpype/hosts/photoshop/plugins/lib.py
new file mode 100644
index 0000000000..74aff06114
--- /dev/null
+++ b/openpype/hosts/photoshop/plugins/lib.py
@@ -0,0 +1,26 @@
+import re
+
+
+def get_unique_layer_name(layers, asset_name, subset_name):
+ """
+ Gets all layer names and if 'asset_name_subset_name' is present, it
+ increases suffix by 1 (eg. creates unique layer name - for Loader)
+ Args:
+ layers (list) of dict with layers info (name, id etc.)
+ asset_name (string):
+ subset_name (string):
+
+ Returns:
+ (string): name_00X (without version)
+ """
+ name = "{}_{}".format(asset_name, subset_name)
+ names = {}
+ for layer in layers:
+ layer_name = re.sub(r'_\d{3}$', '', layer.name)
+ if layer_name in names.keys():
+ names[layer_name] = names[layer_name] + 1
+ else:
+ names[layer_name] = 1
+ occurrences = names.get(name, 0)
+
+ return "{}_{:0>3d}".format(name, occurrences + 1)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py
index 44cc96c96f..d043323768 100644
--- a/openpype/hosts/photoshop/plugins/load/load_image.py
+++ b/openpype/hosts/photoshop/plugins/load/load_image.py
@@ -1,7 +1,9 @@
-from avalon import api, photoshop
-import os
import re
+from avalon import api, photoshop
+
+from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
+
stub = photoshop.stub()
@@ -15,8 +17,9 @@ class ImageLoader(api.Loader):
representations = ["*"]
def load(self, context, name=None, namespace=None, data=None):
- layer_name = self._get_unique_layer_name(context["asset"]["name"],
- name)
+ layer_name = get_unique_layer_name(stub.get_layers(),
+ context["asset"]["name"],
+ name)
with photoshop.maintained_selection():
layer = stub.import_smart_object(self.fname, layer_name)
@@ -69,25 +72,3 @@ class ImageLoader(api.Loader):
def switch(self, container, representation):
self.update(container, representation)
-
- def _get_unique_layer_name(self, asset_name, subset_name):
- """
- Gets all layer names and if 'name' is present in them, increases
- suffix by 1 (eg. creates unique layer name - for Loader)
- Args:
- name (string): in format asset_subset
-
- Returns:
- (string): name_00X (without version)
- """
- name = "{}_{}".format(asset_name, subset_name)
- names = {}
- for layer in stub.get_layers():
- layer_name = re.sub(r'_\d{3}$', '', layer.name)
- if layer_name in names.keys():
- names[layer_name] = names[layer_name] + 1
- else:
- names[layer_name] = 1
- occurrences = names.get(name, 0)
-
- return "{}_{:0>3d}".format(name, occurrences + 1)
diff --git a/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
new file mode 100644
index 0000000000..8704627b12
--- /dev/null
+++ b/openpype/hosts/photoshop/plugins/load/load_image_from_sequence.py
@@ -0,0 +1,98 @@
+import os
+
+from avalon import api
+from avalon import photoshop
+from avalon.pipeline import get_representation_path_from_context
+from avalon.vendor import qargparse
+
+from openpype.lib import Anatomy
+from openpype.hosts.photoshop.plugins.lib import get_unique_layer_name
+
+stub = photoshop.stub()
+
+
+class ImageFromSequenceLoader(api.Loader):
+ """ Load specifing image from sequence
+
+ Used only as quick load of reference file from a sequence.
+
+ Plain ImageLoader picks first frame from sequence.
+
+ Loads only existing files - currently not possible to limit loaders
+ to single select - multiselect. If user selects multiple repres, list
+ for all of them is provided, but selection is only single file.
+ This loader will be triggered multiple times, but selected name will
+ match only to proper path.
+
+ Loader doesnt do containerization as there is currently no data model
+ of 'frame of rendered files' (only rendered sequence), update would be
+ difficult.
+ """
+
+ families = ["render"]
+ representations = ["*"]
+ options = []
+
+ def load(self, context, name=None, namespace=None, data=None):
+ if data.get("frame"):
+ self.fname = os.path.join(os.path.dirname(self.fname),
+ data["frame"])
+ if not os.path.exists(self.fname):
+ return
+
+ stub = photoshop.stub()
+ layer_name = get_unique_layer_name(stub.get_layers(),
+ context["asset"]["name"],
+ name)
+
+ with photoshop.maintained_selection():
+ layer = stub.import_smart_object(self.fname, layer_name)
+
+ self[:] = [layer]
+ namespace = namespace or layer_name
+
+ return namespace
+
+ @classmethod
+ def get_options(cls, repre_contexts):
+ """
+ Returns list of files for selected 'repre_contexts'.
+
+ It returns only files with same extension as in context as it is
+ expected that context points to sequence of frames.
+
+ Returns:
+ (list) of qargparse.Choice
+ """
+ files = []
+ for context in repre_contexts:
+ fname = get_representation_path_from_context(context)
+ _, file_extension = os.path.splitext(fname)
+
+ for file_name in os.listdir(os.path.dirname(fname)):
+ if not file_name.endswith(file_extension):
+ continue
+ files.append(file_name)
+
+ # return selection only if there is something
+ if not files or len(files) <= 1:
+ return []
+
+ return [
+ qargparse.Choice(
+ "frame",
+ label="Select specific file",
+ items=files,
+ default=0,
+ help="Which frame should be loaded?"
+ )
+ ]
+
+ def update(self, container, representation):
+ """No update possible, not containerized."""
+ pass
+
+ def remove(self, container):
+ """No update possible, not containerized."""
+ pass
+
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py
index 43ab13cd79..6913e0836d 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_context.py
@@ -34,7 +34,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
# presets
batch_extensions = ["edl", "xml", "psd"]
- default_families = ["ftrack"]
def process(self, context):
# get json paths from os and load them
@@ -213,10 +212,6 @@ class CollectContextDataSAPublish(pyblish.api.ContextPlugin):
subset = in_data["subset"]
# If instance data already contain families then use it
instance_families = in_data.get("families") or []
- # Make sure default families are in instance
- for default_family in self.default_families or []:
- if default_family not in instance_families:
- instance_families.append(default_family)
instance = context.create_instance(subset)
instance.data.update(
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py
index eb04217136..d753a3d9bb 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/collect_instances.py
@@ -16,12 +16,12 @@ class CollectInstances(pyblish.api.InstancePlugin):
subsets = {
"referenceMain": {
"family": "review",
- "families": ["clip", "ftrack"],
+ "families": ["clip"],
"extensions": [".mp4"]
},
"audioMain": {
"family": "audio",
- "families": ["clip", "ftrack"],
+ "families": ["clip"],
"extensions": [".wav"],
},
"shotMain": {
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py b/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py
deleted file mode 100644
index 5d9e8ddfb4..0000000000
--- a/openpype/hosts/standalonepublisher/plugins/publish/collect_matchmove.py
+++ /dev/null
@@ -1,29 +0,0 @@
-"""
-Requires:
- Nothing
-
-Provides:
- Instance
-"""
-
-import pyblish.api
-import logging
-
-
-log = logging.getLogger("collector")
-
-
-class CollectMatchmovePublish(pyblish.api.InstancePlugin):
- """
- Collector with only one reason for its existence - remove 'ftrack'
- family implicitly added by Standalone Publisher
- """
-
- label = "Collect Matchmove - SA Publish"
- order = pyblish.api.CollectorOrder
- families = ["matchmove"]
- hosts = ["standalonepublisher"]
-
- def process(self, instance):
- if "ftrack" in instance.data["families"]:
- instance.data["families"].remove("ftrack")
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py
index ac1dfa13d4..0792254716 100644
--- a/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py
+++ b/openpype/hosts/standalonepublisher/plugins/publish/extract_thumbnail.py
@@ -1,6 +1,5 @@
import os
import tempfile
-import subprocess
import pyblish.api
import openpype.api
import openpype.lib
@@ -67,7 +66,6 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
else:
# Convert to jpeg if not yet
full_input_path = os.path.join(thumbnail_repre["stagingDir"], file)
- full_input_path = '"{}"'.format(full_input_path)
self.log.info("input {}".format(full_input_path))
full_thumbnail_path = tempfile.mkstemp(suffix=".jpg")[1]
@@ -77,30 +75,37 @@ class ExtractThumbnailSP(pyblish.api.InstancePlugin):
ffmpeg_args = self.ffmpeg_args or {}
- jpeg_items = []
- jpeg_items.append("\"{}\"".format(ffmpeg_path))
- # override file if already exists
- jpeg_items.append("-y")
+ jpeg_items = [
+ "\"{}\"".format(ffmpeg_path),
+ # override file if already exists
+ "-y"
+ ]
+
# add input filters from peresets
jpeg_items.extend(ffmpeg_args.get("input") or [])
# input file
- jpeg_items.append("-i {}".format(full_input_path))
+ jpeg_items.append("-i \"{}\"".format(full_input_path))
# extract only single file
- jpeg_items.append("-vframes 1")
+ jpeg_items.append("-frames:v 1")
+ # Add black background for transparent images
+ jpeg_items.append((
+ "-filter_complex"
+ " \"color=black,format=rgb24[c]"
+ ";[c][0]scale2ref[c][i]"
+ ";[c][i]overlay=format=auto:shortest=1,setsar=1\""
+ ))
jpeg_items.extend(ffmpeg_args.get("output") or [])
# output file
- jpeg_items.append(full_thumbnail_path)
+ jpeg_items.append("\"{}\"".format(full_thumbnail_path))
subprocess_jpeg = " ".join(jpeg_items)
# run subprocess
self.log.debug("Executing: {}".format(subprocess_jpeg))
- subprocess.Popen(
- subprocess_jpeg,
- stdout=subprocess.PIPE,
- shell=True
+ openpype.api.run_subprocess(
+ subprocess_jpeg, shell=True, logger=self.log
)
# remove thumbnail key from origin repre
diff --git a/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py
new file mode 100644
index 0000000000..e3086fb638
--- /dev/null
+++ b/openpype/hosts/standalonepublisher/plugins/publish/validate_frame_ranges.py
@@ -0,0 +1,53 @@
+import re
+
+import pyblish.api
+import openpype.api
+from openpype import lib
+
+
+class ValidateFrameRange(pyblish.api.InstancePlugin):
+ """Validating frame range of rendered files against state in DB."""
+
+ label = "Validate Frame Range"
+ hosts = ["standalonepublisher"]
+ families = ["render"]
+ order = openpype.api.ValidateContentsOrder
+
+ optional = True
+ # published data might be sequence (.mov, .mp4) in that counting files
+ # doesnt make sense
+ check_extensions = ["exr", "dpx", "jpg", "jpeg", "png", "tiff", "tga",
+ "gif", "svg"]
+ skip_timelines_check = [] # skip for specific task names (regex)
+
+ def process(self, instance):
+ if any(re.search(pattern, instance.data["task"])
+ for pattern in self.skip_timelines_check):
+ self.log.info("Skipping for {} task".format(instance.data["task"]))
+
+ asset_data = lib.get_asset(instance.data["asset"])["data"]
+ frame_start = asset_data["frameStart"]
+ frame_end = asset_data["frameEnd"]
+ handle_start = asset_data["handleStart"]
+ handle_end = asset_data["handleEnd"]
+ duration = (frame_end - frame_start + 1) + handle_start + handle_end
+
+ repre = instance.data.get("representations", [None])
+ if not repre:
+ self.log.info("No representations, skipping.")
+ return
+
+ ext = repre[0]['ext'].replace(".", '')
+
+ if not ext or ext.lower() not in self.check_extensions:
+ self.log.warning("Cannot check for extension {}".format(ext))
+ return
+
+ frames = len(instance.data.get("representations", [None])[0]["files"])
+
+ err_msg = "Frame duration from DB:'{}' ". format(int(duration)) +\
+ " doesn't match number of files:'{}'".format(frames) +\
+ " Please change frame range for Asset or limit no. of files"
+ assert frames == duration, err_msg
+
+ self.log.debug("Valid ranges {} - {}".format(int(duration), frames))
diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py
index 585f0c87d7..af6c0f0eee 100644
--- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py
+++ b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py
@@ -1,5 +1,11 @@
-from avalon.tvpaint import pipeline, lib
+from avalon.api import CreatorError
+from avalon.tvpaint import (
+ pipeline,
+ lib,
+ CommunicationWrapper
+)
from openpype.hosts.tvpaint.api import plugin
+from openpype.lib import prepare_template_data
class CreateRenderlayer(plugin.Creator):
@@ -11,13 +17,61 @@ class CreateRenderlayer(plugin.Creator):
defaults = ["Main"]
rename_group = True
+ render_pass = "beauty"
- subset_template = "{family}_{name}"
rename_script_template = (
"tv_layercolor \"setcolor\""
" {clip_id} {group_id} {r} {g} {b} \"{name}\""
)
+ dynamic_subset_keys = ["render_pass", "render_layer", "group"]
+
+ @classmethod
+ def get_dynamic_data(
+ cls, variant, task_name, asset_id, project_name, host_name
+ ):
+ dynamic_data = super(CreateRenderlayer, cls).get_dynamic_data(
+ variant, task_name, asset_id, project_name, host_name
+ )
+ # Use render pass name from creator's plugin
+ dynamic_data["render_pass"] = cls.render_pass
+ # Add variant to render layer
+ dynamic_data["render_layer"] = variant
+ # Change family for subset name fill
+ dynamic_data["family"] = "render"
+
+ return dynamic_data
+
+ @classmethod
+ def get_default_variant(cls):
+ """Default value for variant in Creator tool.
+
+ Method checks if TVPaint implementation is running and tries to find
+ selected layers from TVPaint. If only one is selected it's name is
+ returned.
+
+ Returns:
+ str: Default variant name for Creator tool.
+ """
+ # Validate that communication is initialized
+ if CommunicationWrapper.communicator:
+ # Get currently selected layers
+ layers_data = lib.layers_data()
+
+ selected_layers = [
+ layer
+ for layer in layers_data
+ if layer["selected"]
+ ]
+ # Return layer name if only one is selected
+ if len(selected_layers) == 1:
+ return selected_layers[0]["name"]
+
+ # Use defaults
+ if cls.defaults:
+ return cls.defaults[0]
+ return None
+
def process(self):
self.log.debug("Query data from workfile.")
instances = pipeline.list_instances()
@@ -32,34 +86,44 @@ class CreateRenderlayer(plugin.Creator):
# Raise if there is no selection
if not group_ids:
- raise AssertionError("Nothing is selected.")
+ raise CreatorError("Nothing is selected.")
# This creator should run only on one group
if len(group_ids) > 1:
- raise AssertionError("More than one group is in selection.")
+ raise CreatorError("More than one group is in selection.")
group_id = tuple(group_ids)[0]
# If group id is `0` it is `default` group which is invalid
if group_id == 0:
- raise AssertionError(
+ raise CreatorError(
"Selection is not in group. Can't mark selection as Beauty."
)
self.log.debug(f"Selected group id is \"{group_id}\".")
self.data["group_id"] = group_id
- family = self.data["family"]
- # Extract entered name
- name = self.data["subset"][len(family):]
- self.log.info(f"Extracted name from subset name \"{name}\".")
- self.data["name"] = name
+ group_data = lib.groups_data()
+ group_name = None
+ for group in group_data:
+ if group["group_id"] == group_id:
+ group_name = group["name"]
+ break
- # Change subset name by template
- subset_name = self.subset_template.format(**{
- "family": self.family,
- "name": name
- })
- self.log.info(f"New subset name \"{subset_name}\".")
+ if group_name is None:
+ raise AssertionError(
+ "Couldn't find group by id \"{}\"".format(group_id)
+ )
+
+ subset_name_fill_data = {
+ "group": group_name
+ }
+
+ family = self.family = self.data["family"]
+
+ # Fill dynamic key 'group'
+ subset_name = self.data["subset"].format(
+ **prepare_template_data(subset_name_fill_data)
+ )
self.data["subset"] = subset_name
# Check for instances of same group
@@ -115,7 +179,7 @@ class CreateRenderlayer(plugin.Creator):
# Rename TVPaint group (keep color same)
# - groups can't contain spaces
- new_group_name = name.replace(" ", "_")
+ new_group_name = self.data["variant"].replace(" ", "_")
rename_script = self.rename_script_template.format(
clip_id=selected_group["clip_id"],
group_id=selected_group["group_id"],
diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py
index 09c68930f2..ad06520210 100644
--- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py
+++ b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py
@@ -1,5 +1,11 @@
-from avalon.tvpaint import pipeline, lib
+from avalon.api import CreatorError
+from avalon.tvpaint import (
+ pipeline,
+ lib,
+ CommunicationWrapper
+)
from openpype.hosts.tvpaint.api import plugin
+from openpype.lib import prepare_template_data
class CreateRenderPass(plugin.Creator):
@@ -14,7 +20,49 @@ class CreateRenderPass(plugin.Creator):
icon = "cube"
defaults = ["Main"]
- subset_template = "{family}_{render_layer}_{pass}"
+ dynamic_subset_keys = ["render_pass", "render_layer"]
+
+ @classmethod
+ def get_dynamic_data(
+ cls, variant, task_name, asset_id, project_name, host_name
+ ):
+ dynamic_data = super(CreateRenderPass, cls).get_dynamic_data(
+ variant, task_name, asset_id, project_name, host_name
+ )
+ dynamic_data["render_pass"] = variant
+ dynamic_data["family"] = "render"
+
+ return dynamic_data
+
+ @classmethod
+ def get_default_variant(cls):
+ """Default value for variant in Creator tool.
+
+ Method checks if TVPaint implementation is running and tries to find
+ selected layers from TVPaint. If only one is selected it's name is
+ returned.
+
+ Returns:
+ str: Default variant name for Creator tool.
+ """
+ # Validate that communication is initialized
+ if CommunicationWrapper.communicator:
+ # Get currently selected layers
+ layers_data = lib.layers_data()
+
+ selected_layers = [
+ layer
+ for layer in layers_data
+ if layer["selected"]
+ ]
+ # Return layer name if only one is selected
+ if len(selected_layers) == 1:
+ return selected_layers[0]["name"]
+
+ # Use defaults
+ if cls.defaults:
+ return cls.defaults[0]
+ return None
def process(self):
self.log.debug("Query data from workfile.")
@@ -32,11 +80,11 @@ class CreateRenderPass(plugin.Creator):
# Raise if nothing is selected
if not selected_layers:
- raise AssertionError("Nothing is selected.")
+ raise CreatorError("Nothing is selected.")
# Raise if layers from multiple groups are selected
if len(group_ids) != 1:
- raise AssertionError("More than one group is in selection.")
+ raise CreatorError("More than one group is in selection.")
group_id = tuple(group_ids)[0]
self.log.debug(f"Selected group id is \"{group_id}\".")
@@ -53,34 +101,40 @@ class CreateRenderPass(plugin.Creator):
# Beauty is required for this creator so raise if was not found
if beauty_instance is None:
- raise AssertionError("Beauty pass does not exist yet.")
+ raise CreatorError("Beauty pass does not exist yet.")
- render_layer = beauty_instance["name"]
+ subset_name = self.data["subset"]
+
+ subset_name_fill_data = {}
+
+ # Backwards compatibility
+ # - beauty may be created with older creator where variant was not
+ # stored
+ if "variant" not in beauty_instance:
+ render_layer = beauty_instance["name"]
+ else:
+ render_layer = beauty_instance["variant"]
+
+ subset_name_fill_data["render_layer"] = render_layer
+
+ # Format dynamic keys in subset name
+ new_subset_name = subset_name.format(
+ **prepare_template_data(subset_name_fill_data)
+ )
+ self.data["subset"] = new_subset_name
+ self.log.info(f"New subset name is \"{new_subset_name}\".")
- # Extract entered name
family = self.data["family"]
- name = self.data["subset"]
- # Is this right way how to get name?
- name = name[len(family):]
- self.log.info(f"Extracted name from subset name \"{name}\".")
+ variant = self.data["variant"]
self.data["group_id"] = group_id
- self.data["pass"] = name
+ self.data["pass"] = variant
self.data["render_layer"] = render_layer
# Collect selected layer ids to be stored into instance
layer_names = [layer["name"] for layer in selected_layers]
self.data["layer_names"] = layer_names
- # Replace `beauty` in beauty's subset name with entered name
- subset_name = self.subset_template.format(**{
- "family": family,
- "render_layer": render_layer,
- "pass": name
- })
- self.data["subset"] = subset_name
- self.log.info(f"New subset name is \"{subset_name}\".")
-
# Check if same instance already exists
existing_instance = None
existing_instance_idx = None
@@ -88,7 +142,7 @@ class CreateRenderPass(plugin.Creator):
if (
instance["family"] == family
and instance["group_id"] == group_id
- and instance["pass"] == name
+ and instance["pass"] == variant
):
existing_instance = instance
existing_instance_idx = idx
@@ -97,7 +151,7 @@ class CreateRenderPass(plugin.Creator):
if existing_instance is not None:
self.log.info(
f"Render pass instance for group id {group_id}"
- f" and name \"{name}\" already exists, overriding."
+ f" and name \"{variant}\" already exists, overriding."
)
instances[existing_instance_idx] = self.data
else:
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
index 9b11f9fe80..e496b144cd 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py
@@ -4,6 +4,8 @@ import copy
import pyblish.api
from avalon import io
+from openpype.lib import get_subset_name
+
class CollectInstances(pyblish.api.ContextPlugin):
label = "Collect Instances"
@@ -62,9 +64,38 @@ class CollectInstances(pyblish.api.ContextPlugin):
# Different instance creation based on family
instance = None
if family == "review":
- # Change subset name
+ # Change subset name of review instance
+
+ # Collect asset doc to get asset id
+ # - not sure if it's good idea to require asset id in
+ # get_subset_name?
+ asset_name = context.data["workfile_context"]["asset"]
+ asset_doc = io.find_one(
+ {
+ "type": "asset",
+ "name": asset_name
+ },
+ {"_id": 1}
+ )
+ asset_id = None
+ if asset_doc:
+ asset_id = asset_doc["_id"]
+
+ # Project name from workfile context
+ project_name = context.data["workfile_context"]["project"]
+ # Host name from environemnt variable
+ host_name = os.environ["AVALON_APP"]
+ # Use empty variant value
+ variant = ""
task_name = io.Session["AVALON_TASK"]
- new_subset_name = "{}{}".format(family, task_name.capitalize())
+ new_subset_name = get_subset_name(
+ family,
+ variant,
+ task_name,
+ asset_id,
+ project_name,
+ host_name
+ )
instance_data["subset"] = new_subset_name
instance = context.create_instance(**instance_data)
@@ -72,8 +103,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance.data["layers"] = copy.deepcopy(
context.data["layersData"]
)
- # Add ftrack family
- instance.data["families"].append("ftrack")
elif family == "renderLayer":
instance = self.create_render_layer_instance(
@@ -119,19 +148,23 @@ class CollectInstances(pyblish.api.ContextPlugin):
name = instance_data["name"]
# Change label
subset_name = instance_data["subset"]
- instance_data["label"] = "{}_Beauty".format(name)
- # Change subset name
- # Final family of an instance will be `render`
- new_family = "render"
- task_name = io.Session["AVALON_TASK"]
- new_subset_name = "{}{}_{}_Beauty".format(
- new_family, task_name.capitalize(), name
- )
- instance_data["subset"] = new_subset_name
- self.log.debug("Changed subset name \"{}\"->\"{}\"".format(
- subset_name, new_subset_name
- ))
+ # Backwards compatibility
+ # - subset names were not stored as final subset names during creation
+ if "variant" not in instance_data:
+ instance_data["label"] = "{}_Beauty".format(name)
+
+ # Change subset name
+ # Final family of an instance will be `render`
+ new_family = "render"
+ task_name = io.Session["AVALON_TASK"]
+ new_subset_name = "{}{}_{}_Beauty".format(
+ new_family, task_name.capitalize(), name
+ )
+ instance_data["subset"] = new_subset_name
+ self.log.debug("Changed subset name \"{}\"->\"{}\"".format(
+ subset_name, new_subset_name
+ ))
# Get all layers for the layer
layers_data = context.data["layersData"]
@@ -151,9 +184,6 @@ class CollectInstances(pyblish.api.ContextPlugin):
instance_data["layers"] = group_layers
- # Add ftrack family
- instance_data["families"].append("ftrack")
-
return context.create_instance(**instance_data)
def create_render_pass_instance(self, context, instance_data):
@@ -163,20 +193,23 @@ class CollectInstances(pyblish.api.ContextPlugin):
)
# Change label
render_layer = instance_data["render_layer"]
- instance_data["label"] = "{}_{}".format(render_layer, pass_name)
- # Change subset name
- # Final family of an instance will be `render`
- new_family = "render"
- old_subset_name = instance_data["subset"]
- task_name = io.Session["AVALON_TASK"]
- new_subset_name = "{}{}_{}_{}".format(
- new_family, task_name.capitalize(), render_layer, pass_name
- )
- instance_data["subset"] = new_subset_name
- self.log.debug("Changed subset name \"{}\"->\"{}\"".format(
- old_subset_name, new_subset_name
- ))
+ # Backwards compatibility
+ # - subset names were not stored as final subset names during creation
+ if "variant" not in instance_data:
+ instance_data["label"] = "{}_{}".format(render_layer, pass_name)
+ # Change subset name
+ # Final family of an instance will be `render`
+ new_family = "render"
+ old_subset_name = instance_data["subset"]
+ task_name = io.Session["AVALON_TASK"]
+ new_subset_name = "{}{}_{}_{}".format(
+ new_family, task_name.capitalize(), render_layer, pass_name
+ )
+ instance_data["subset"] = new_subset_name
+ self.log.debug("Changed subset name \"{}\"->\"{}\"".format(
+ old_subset_name, new_subset_name
+ ))
layers_data = context.data["layersData"]
layers_by_name = {
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
index b059be90bf..b61fec895f 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py
@@ -3,6 +3,8 @@ import json
import pyblish.api
from avalon import io
+from openpype.lib import get_subset_name
+
class CollectWorkfile(pyblish.api.ContextPlugin):
label = "Collect Workfile"
@@ -20,8 +22,38 @@ class CollectWorkfile(pyblish.api.ContextPlugin):
basename, ext = os.path.splitext(filename)
instance = context.create_instance(name=basename)
+ # Get subset name of workfile instance
+ # Collect asset doc to get asset id
+ # - not sure if it's good idea to require asset id in
+ # get_subset_name?
+ family = "workfile"
+ asset_name = context.data["workfile_context"]["asset"]
+ asset_doc = io.find_one(
+ {
+ "type": "asset",
+ "name": asset_name
+ },
+ {"_id": 1}
+ )
+ asset_id = None
+ if asset_doc:
+ asset_id = asset_doc["_id"]
+
+ # Project name from workfile context
+ project_name = context.data["workfile_context"]["project"]
+ # Host name from environemnt variable
+ host_name = os.environ["AVALON_APP"]
+ # Use empty variant value
+ variant = ""
task_name = io.Session["AVALON_TASK"]
- subset_name = "workfile" + task_name.capitalize()
+ subset_name = get_subset_name(
+ family,
+ variant,
+ task_name,
+ asset_id,
+ project_name,
+ host_name
+ )
# Create Workfile instance
instance.data.update({
diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py
index 13c6c9eb78..d8bb03f541 100644
--- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py
+++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py
@@ -1,5 +1,6 @@
import os
import json
+import tempfile
import pyblish.api
import avalon.api
@@ -153,9 +154,45 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
"sceneMarkIn": int(mark_in_frame),
"sceneMarkInState": mark_in_state == "set",
"sceneMarkOut": int(mark_out_frame),
- "sceneMarkOutState": mark_out_state == "set"
+ "sceneMarkOutState": mark_out_state == "set",
+ "sceneBgColor": self._get_bg_color()
}
self.log.debug(
"Scene data: {}".format(json.dumps(scene_data, indent=4))
)
context.data.update(scene_data)
+
+ def _get_bg_color(self):
+ """Background color set on scene.
+
+ Is important for review exporting where scene bg color is used as
+ background.
+ """
+ output_file = tempfile.NamedTemporaryFile(
+ mode="w", prefix="a_tvp_", suffix=".txt", delete=False
+ )
+ output_file.close()
+ output_filepath = output_file.name.replace("\\", "/")
+ george_script_lines = [
+ # Variable containing full path to output file
+ "output_path = \"{}\"".format(output_filepath),
+ "tv_background",
+ "bg_color = result",
+ # Write data to output file
+ (
+ "tv_writetextfile"
+ " \"strict\" \"append\" '\"'output_path'\"' bg_color"
+ )
+ ]
+
+ george_script = "\n".join(george_script_lines)
+ lib.execute_george_through_file(george_script)
+
+ with open(output_filepath, "r") as stream:
+ data = stream.read()
+
+ os.remove(output_filepath)
+ data = data.strip()
+ if not data:
+ return None
+ return data.split(" ")
diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
index 007b5c41f1..536df2adb0 100644
--- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
+++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py
@@ -1,5 +1,6 @@
import os
import shutil
+import copy
import tempfile
import pyblish.api
@@ -13,6 +14,9 @@ class ExtractSequence(pyblish.api.Extractor):
hosts = ["tvpaint"]
families = ["review", "renderPass", "renderLayer"]
+ # Modifiable with settings
+ review_bg = [255, 255, 255, 255]
+
def process(self, instance):
self.log.info(
"* Processing instance \"{}\"".format(instance.data["label"])
@@ -53,6 +57,8 @@ class ExtractSequence(pyblish.api.Extractor):
handle_start = instance.context.data["handleStart"]
handle_end = instance.context.data["handleEnd"]
+ scene_bg_color = instance.context.data["sceneBgColor"]
+
# --- Fallbacks ----------------------------------------------------
# This is required if validations of ranges are ignored.
# - all of this code won't change processing if range to render
@@ -120,7 +126,8 @@ class ExtractSequence(pyblish.api.Extractor):
if instance.data["family"] == "review":
output_filenames, thumbnail_fullpath = self.render_review(
- filename_template, output_dir, mark_in, mark_out
+ filename_template, output_dir, mark_in, mark_out,
+ scene_bg_color
)
else:
# Render output
@@ -241,7 +248,9 @@ class ExtractSequence(pyblish.api.Extractor):
for path in repre_filepaths
]
- def render_review(self, filename_template, output_dir, mark_in, mark_out):
+ def render_review(
+ self, filename_template, output_dir, mark_in, mark_out, scene_bg_color
+ ):
""" Export images from TVPaint using `tv_savesequence` command.
Args:
@@ -252,6 +261,8 @@ class ExtractSequence(pyblish.api.Extractor):
output_dir (str): Directory where files will be stored.
mark_in (int): Starting frame index from which export will begin.
mark_out (int): On which frame index export will end.
+ scene_bg_color (list): Bg color set in scene. Result of george
+ script command `tv_background`.
Retruns:
tuple: With 2 items first is list of filenames second is path to
@@ -263,7 +274,11 @@ class ExtractSequence(pyblish.api.Extractor):
filename_template.format(frame=mark_in)
)
+ bg_color = self._get_review_bg_color()
+
george_script_lines = [
+ # Change bg color to color from settings
+ "tv_background \"color\" {} {} {}".format(*bg_color),
"tv_SaveMode \"PNG\"",
"export_path = \"{}\"".format(
first_frame_filepath.replace("\\", "/")
@@ -272,6 +287,18 @@ class ExtractSequence(pyblish.api.Extractor):
mark_in, mark_out
)
]
+ if scene_bg_color:
+ # Change bg color back to previous scene bg color
+ _scene_bg_color = copy.deepcopy(scene_bg_color)
+ bg_type = _scene_bg_color.pop(0)
+ orig_color_command = [
+ "tv_background",
+ "\"{}\"".format(bg_type)
+ ]
+ orig_color_command.extend(_scene_bg_color)
+
+ george_script_lines.append(" ".join(orig_color_command))
+
lib.execute_george_through_file("\n".join(george_script_lines))
first_frame_filepath = None
@@ -291,12 +318,13 @@ class ExtractSequence(pyblish.api.Extractor):
if first_frame_filepath is None:
first_frame_filepath = filepath
- thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
+ thumbnail_filepath = None
if first_frame_filepath and os.path.exists(first_frame_filepath):
+ thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
source_img = Image.open(first_frame_filepath)
- thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
- thumbnail_obj.paste(source_img)
- thumbnail_obj.save(thumbnail_filepath)
+ if source_img.mode.lower() != "rgb":
+ source_img = source_img.convert("RGB")
+ source_img.save(thumbnail_filepath)
return output_filenames, thumbnail_filepath
@@ -392,12 +420,35 @@ class ExtractSequence(pyblish.api.Extractor):
if thumbnail_src_filepath and os.path.exists(thumbnail_src_filepath):
source_img = Image.open(thumbnail_src_filepath)
thumbnail_filepath = os.path.join(output_dir, "thumbnail.jpg")
- thumbnail_obj = Image.new("RGB", source_img.size, (255, 255, 255))
- thumbnail_obj.paste(source_img)
- thumbnail_obj.save(thumbnail_filepath)
+ # Composite background only on rgba images
+ # - just making sure
+ if source_img.mode.lower() == "rgba":
+ bg_color = self._get_review_bg_color()
+ self.log.debug("Adding thumbnail background color {}.".format(
+ " ".join([str(val) for val in bg_color])
+ ))
+ bg_image = Image.new("RGBA", source_img.size, bg_color)
+ thumbnail_obj = Image.alpha_composite(bg_image, source_img)
+ thumbnail_obj.convert("RGB").save(thumbnail_filepath)
+
+ else:
+ self.log.info((
+ "Source for thumbnail has mode \"{}\" (Expected: RGBA)."
+ " Can't use thubmanail background color."
+ ).format(source_img.mode))
+ source_img.save(thumbnail_filepath)
return output_filenames, thumbnail_filepath
+ def _get_review_bg_color(self):
+ red = green = blue = 255
+ if self.review_bg:
+ if len(self.review_bg) == 4:
+ red, green, blue, _ = self.review_bg
+ elif len(self.review_bg) == 3:
+ red, green, blue = self.review_bg
+ return (red, green, blue)
+
def _render_layer(
self,
layer,
diff --git a/openpype/lib/abstract_submit_deadline.py b/openpype/lib/abstract_submit_deadline.py
index 12014ddfb5..4a052a4ee2 100644
--- a/openpype/lib/abstract_submit_deadline.py
+++ b/openpype/lib/abstract_submit_deadline.py
@@ -18,6 +18,48 @@ import pyblish.api
from .abstract_metaplugins import AbstractMetaInstancePlugin
+def requests_post(*args, **kwargs):
+ """Wrap request post method.
+
+ Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
+ variable is found. This is useful when Deadline or Muster server are
+ running with self-signed certificates and their certificate is not
+ added to trusted certificates on client machines.
+
+ Warning:
+ Disabling SSL certificate validation is defeating one line
+ of defense SSL is providing and it is not recommended.
+
+ """
+ if 'verify' not in kwargs:
+ kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
+ True) else True # noqa
+ # add 10sec timeout before bailing out
+ kwargs['timeout'] = 10
+ return requests.post(*args, **kwargs)
+
+
+def requests_get(*args, **kwargs):
+ """Wrap request get method.
+
+ Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
+ variable is found. This is useful when Deadline or Muster server are
+ running with self-signed certificates and their certificate is not
+ added to trusted certificates on client machines.
+
+ Warning:
+ Disabling SSL certificate validation is defeating one line
+ of defense SSL is providing and it is not recommended.
+
+ """
+ if 'verify' not in kwargs:
+ kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL",
+ True) else True # noqa
+ # add 10sec timeout before bailing out
+ kwargs['timeout'] = 10
+ return requests.get(*args, **kwargs)
+
+
@attr.s
class DeadlineJobInfo(object):
"""Mapping of all Deadline *JobInfo* attributes.
@@ -579,7 +621,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
"""
url = "{}/api/jobs".format(self._deadline_url)
- response = self._requests_post(url, json=payload)
+ response = requests_post(url, json=payload)
if not response.ok:
self.log.error("Submission failed!")
self.log.error(response.status_code)
@@ -592,41 +634,3 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin):
self._instance.data["deadlineSubmissionJob"] = result
return result["_id"]
-
- def _requests_post(self, *args, **kwargs):
- """Wrap request post method.
-
- Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
- variable is found. This is useful when Deadline or Muster server are
- running with self-signed certificates and their certificate is not
- added to trusted certificates on client machines.
-
- Warning:
- Disabling SSL certificate validation is defeating one line
- of defense SSL is providing and it is not recommended.
-
- """
- if 'verify' not in kwargs:
- kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
- # add 10sec timeout before bailing out
- kwargs['timeout'] = 10
- return requests.post(*args, **kwargs)
-
- def _requests_get(self, *args, **kwargs):
- """Wrap request get method.
-
- Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment
- variable is found. This is useful when Deadline or Muster server are
- running with self-signed certificates and their certificate is not
- added to trusted certificates on client machines.
-
- Warning:
- Disabling SSL certificate validation is defeating one line
- of defense SSL is providing and it is not recommended.
-
- """
- if 'verify' not in kwargs:
- kwargs['verify'] = False if os.getenv("OPENPYPE_DONT_VERIFY_SSL", True) else True # noqa
- # add 10sec timeout before bailing out
- kwargs['timeout'] = 10
- return requests.get(*args, **kwargs)
diff --git a/openpype/lib/anatomy.py b/openpype/lib/anatomy.py
index c16c6e2e99..7a4a55363c 100644
--- a/openpype/lib/anatomy.py
+++ b/openpype/lib/anatomy.py
@@ -733,6 +733,9 @@ class Templates:
continue
default_key_values[key] = templates.pop(key)
+ # Pop "others" key before before expected keys are processed
+ other_templates = templates.pop("others") or {}
+
keys_by_subkey = {}
for sub_key, sub_value in templates.items():
key_values = {}
@@ -740,7 +743,6 @@ class Templates:
key_values.update(sub_value)
keys_by_subkey[sub_key] = cls.prepare_inner_keys(key_values)
- other_templates = templates.get("others") or {}
for sub_key, sub_value in other_templates.items():
if sub_key in keys_by_subkey:
log.warning((
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index d82b7cd847..9866400928 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -191,26 +191,32 @@ class Application:
self.full_label = full_label
self._environment = data.get("environment") or {}
+ arguments = data.get("arguments")
+ if isinstance(arguments, dict):
+ arguments = arguments.get(platform.system().lower())
+
+ if not arguments:
+ arguments = []
+ self.arguments = arguments
+
+ if "executables" not in data:
+ self.executables = [
+ UndefinedApplicationExecutable()
+ ]
+ return
+
_executables = data["executables"]
+ if isinstance(_executables, dict):
+ _executables = _executables.get(platform.system().lower())
+
if not _executables:
_executables = []
- elif isinstance(_executables, dict):
- _executables = _executables.get(platform.system().lower()) or []
-
- _arguments = data["arguments"]
- if not _arguments:
- _arguments = []
-
- elif isinstance(_arguments, dict):
- _arguments = _arguments.get(platform.system().lower()) or []
-
executables = []
for executable in _executables:
executables.append(ApplicationExecutable(executable))
self.executables = executables
- self.arguments = _arguments
def __repr__(self):
return "<{}> - {}".format(self.__class__.__name__, self.full_name)
@@ -454,6 +460,12 @@ class ApplicationExecutable:
if os.path.exists(_executable):
executable = _executable
+ # Try to format executable with environments
+ try:
+ executable = executable.format(**os.environ)
+ except Exception:
+ pass
+
self.executable_path = executable
def __str__(self):
@@ -484,6 +496,27 @@ class ApplicationExecutable:
return bool(self._realpath())
+class UndefinedApplicationExecutable(ApplicationExecutable):
+ """Some applications do not require executable path from settings.
+
+ In that case this class is used to "fake" existing executable.
+ """
+ def __init__(self):
+ pass
+
+ def __str__(self):
+ return self.__class__.__name__
+
+ def __repr__(self):
+ return "<{}>".format(self.__class__.__name__)
+
+ def as_args(self):
+ return []
+
+ def exists(self):
+ return True
+
+
@six.add_metaclass(ABCMeta)
class LaunchHook:
"""Abstract base class of launch hook."""
@@ -1126,6 +1159,9 @@ def prepare_host_environments(data, implementation_envs=True):
def apply_project_environments_value(project_name, env, project_settings=None):
"""Apply project specific environments on passed environments.
+ The enviornments are applied on passed `env` argument value so it is not
+ required to apply changes back.
+
Args:
project_name (str): Name of project for which environemnts should be
received.
@@ -1134,6 +1170,9 @@ def apply_project_environments_value(project_name, env, project_settings=None):
project_settings (dict): Project settings for passed project name.
Optional if project settings are already prepared.
+ Returns:
+ dict: Passed env values with applied project environments.
+
Raises:
KeyError: If project settings do not contain keys for project specific
environments.
@@ -1144,10 +1183,9 @@ def apply_project_environments_value(project_name, env, project_settings=None):
project_settings = get_project_settings(project_name)
env_value = project_settings["global"]["project_environments"]
- if not env_value:
- return env
- parsed = acre.parse(env_value)
- return _merge_env(parsed, env)
+ if env_value:
+ env.update(_merge_env(acre.parse(env_value), env))
+ return env
def prepare_context_environments(data):
@@ -1176,9 +1214,8 @@ def prepare_context_environments(data):
# Load project specific environments
project_name = project_doc["name"]
- data["env"] = apply_project_environments_value(
- project_name, data["env"]
- )
+ # Apply project specific environments on current env value
+ apply_project_environments_value(project_name, data["env"])
app = data["app"]
workdir_data = get_workdir_data(
diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py
index a5254af0da..1f2fb7a46e 100644
--- a/openpype/lib/plugin_tools.py
+++ b/openpype/lib/plugin_tools.py
@@ -34,7 +34,8 @@ def get_subset_name(
asset_id,
project_name=None,
host_name=None,
- default_template=None
+ default_template=None,
+ dynamic_data=None
):
if not family:
return ""
@@ -68,11 +69,16 @@ def get_subset_name(
if not task_name and "{task" in template.lower():
raise TaskNotSetError()
- fill_pairs = (
- ("variant", variant),
- ("family", family),
- ("task", task_name)
- )
+ fill_pairs = {
+ "variant": variant,
+ "family": family,
+ "task": task_name
+ }
+ if dynamic_data:
+ # Dynamic data may override default values
+ for key, value in dynamic_data.items():
+ fill_pairs[key] = value
+
return template.format(**prepare_template_data(fill_pairs))
@@ -91,7 +97,8 @@ def prepare_template_data(fill_pairs):
"""
fill_data = {}
- for key, value in fill_pairs:
+ regex = re.compile(r"[a-zA-Z0-9]")
+ for key, value in dict(fill_pairs).items():
# Handle cases when value is `None` (standalone publisher)
if value is None:
continue
@@ -102,13 +109,18 @@ def prepare_template_data(fill_pairs):
# Capitalize only first char of value
# - conditions are because of possible index errors
+ # - regex is to skip symbols that are not chars or numbers
+ # - e.g. "{key}" which starts with curly bracket
capitalized = ""
- if value:
- # Upper first character
- capitalized += value[0].upper()
- # Append rest of string if there is any
- if len(value) > 1:
- capitalized += value[1:]
+ for idx in range(len(value or "")):
+ char = value[idx]
+ if not regex.match(char):
+ capitalized += char
+ else:
+ capitalized += char.upper()
+ capitalized += value[idx + 1:]
+ break
+
fill_data[key.capitalize()] = capitalized
return fill_data
diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py
index debeeed6bf..d6fb9c0aef 100644
--- a/openpype/modules/__init__.py
+++ b/openpype/modules/__init__.py
@@ -39,6 +39,7 @@ from .deadline import DeadlineModule
from .project_manager_action import ProjectManagerAction
from .standalonepublish_action import StandAlonePublishAction
from .sync_server import SyncServerModule
+from .slack import SlackIntegrationModule
__all__ = (
@@ -77,5 +78,7 @@ __all__ = (
"ProjectManagerAction",
"StandAlonePublishAction",
- "SyncServerModule"
+ "SyncServerModule",
+
+ "SlackIntegrationModule"
)
diff --git a/openpype/modules/clockify/clockify_api.py b/openpype/modules/clockify/clockify_api.py
index 3f0a9799b4..6af911fffc 100644
--- a/openpype/modules/clockify/clockify_api.py
+++ b/openpype/modules/clockify/clockify_api.py
@@ -36,6 +36,7 @@ class ClockifyAPI:
self._secure_registry = None
+ @property
def secure_registry(self):
if self._secure_registry is None:
self._secure_registry = OpenPypeSecureRegistry("clockify")
diff --git a/openpype/modules/clockify/widgets.py b/openpype/modules/clockify/widgets.py
index 76f3a3f365..fc8e7fa8a3 100644
--- a/openpype/modules/clockify/widgets.py
+++ b/openpype/modules/clockify/widgets.py
@@ -1,6 +1,5 @@
from Qt import QtCore, QtGui, QtWidgets
-from avalon import style
-from openpype import resources
+from openpype import resources, style
class MessageWidget(QtWidgets.QWidget):
@@ -22,14 +21,6 @@ class MessageWidget(QtWidgets.QWidget):
QtCore.Qt.WindowMinimizeButtonHint
)
- # Font
- self.font = QtGui.QFont()
- self.font.setFamily("DejaVu Sans Condensed")
- self.font.setPointSize(9)
- self.font.setBold(True)
- self.font.setWeight(50)
- self.font.setKerning(True)
-
# Size setting
self.resize(self.SIZE_W, self.SIZE_H)
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
@@ -53,7 +44,6 @@ class MessageWidget(QtWidgets.QWidget):
labels = []
for message in messages:
label = QtWidgets.QLabel(message)
- label.setFont(self.font)
label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
label.setTextFormat(QtCore.Qt.RichText)
label.setWordWrap(True)
@@ -103,84 +93,64 @@ class ClockifySettings(QtWidgets.QWidget):
icon = QtGui.QIcon(resources.pype_icon_filepath())
self.setWindowIcon(icon)
+ self.setWindowTitle("Clockify settings")
self.setWindowFlags(
QtCore.Qt.WindowCloseButtonHint |
QtCore.Qt.WindowMinimizeButtonHint
)
- self._translate = QtCore.QCoreApplication.translate
-
- # Font
- self.font = QtGui.QFont()
- self.font.setFamily("DejaVu Sans Condensed")
- self.font.setPointSize(9)
- self.font.setBold(True)
- self.font.setWeight(50)
- self.font.setKerning(True)
-
# Size setting
self.resize(self.SIZE_W, self.SIZE_H)
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
self.setMaximumSize(QtCore.QSize(self.SIZE_W+100, self.SIZE_H+100))
self.setStyleSheet(style.load_stylesheet())
- self.setLayout(self._main())
- self.setWindowTitle('Clockify settings')
+ self._ui_init()
- def _main(self):
- self.main = QtWidgets.QVBoxLayout()
- self.main.setObjectName("main")
+ def _ui_init(self):
+ label_api_key = QtWidgets.QLabel("Clockify API key:")
- self.form = QtWidgets.QFormLayout()
- self.form.setContentsMargins(10, 15, 10, 5)
- self.form.setObjectName("form")
+ input_api_key = QtWidgets.QLineEdit()
+ input_api_key.setFrame(True)
+ input_api_key.setPlaceholderText("e.g. XX1XxXX2x3x4xXxx")
- self.label_api_key = QtWidgets.QLabel("Clockify API key:")
- self.label_api_key.setFont(self.font)
- self.label_api_key.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
- self.label_api_key.setTextFormat(QtCore.Qt.RichText)
- self.label_api_key.setObjectName("label_api_key")
+ error_label = QtWidgets.QLabel("")
+ error_label.setTextFormat(QtCore.Qt.RichText)
+ error_label.setWordWrap(True)
+ error_label.hide()
- self.input_api_key = QtWidgets.QLineEdit()
- self.input_api_key.setEnabled(True)
- self.input_api_key.setFrame(True)
- self.input_api_key.setObjectName("input_api_key")
- self.input_api_key.setPlaceholderText(
- self._translate("main", "e.g. XX1XxXX2x3x4xXxx")
- )
+ form_layout = QtWidgets.QFormLayout()
+ form_layout.setContentsMargins(10, 15, 10, 5)
+ form_layout.addRow(label_api_key, input_api_key)
+ form_layout.addRow(error_label)
- self.error_label = QtWidgets.QLabel("")
- self.error_label.setFont(self.font)
- self.error_label.setTextFormat(QtCore.Qt.RichText)
- self.error_label.setObjectName("error_label")
- self.error_label.setWordWrap(True)
- self.error_label.hide()
+ btn_ok = QtWidgets.QPushButton("Ok")
+ btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer')
- self.form.addRow(self.label_api_key, self.input_api_key)
- self.form.addRow(self.error_label)
-
- self.btn_group = QtWidgets.QHBoxLayout()
- self.btn_group.addStretch(1)
- self.btn_group.setObjectName("btn_group")
-
- self.btn_ok = QtWidgets.QPushButton("Ok")
- self.btn_ok.setToolTip('Sets Clockify API Key so can Start/Stop timer')
- self.btn_ok.clicked.connect(self.click_ok)
-
- self.btn_cancel = QtWidgets.QPushButton("Cancel")
+ btn_cancel = QtWidgets.QPushButton("Cancel")
cancel_tooltip = 'Application won\'t start'
if self.optional:
cancel_tooltip = 'Close this window'
- self.btn_cancel.setToolTip(cancel_tooltip)
- self.btn_cancel.clicked.connect(self._close_widget)
+ btn_cancel.setToolTip(cancel_tooltip)
- self.btn_group.addWidget(self.btn_ok)
- self.btn_group.addWidget(self.btn_cancel)
+ btn_group = QtWidgets.QHBoxLayout()
+ btn_group.addStretch(1)
+ btn_group.addWidget(btn_ok)
+ btn_group.addWidget(btn_cancel)
- self.main.addLayout(self.form)
- self.main.addLayout(self.btn_group)
+ main_layout = QtWidgets.QVBoxLayout(self)
+ main_layout.addLayout(form_layout)
+ main_layout.addLayout(btn_group)
- return self.main
+ btn_ok.clicked.connect(self.click_ok)
+ btn_cancel.clicked.connect(self._close_widget)
+
+ self.label_api_key = label_api_key
+ self.input_api_key = input_api_key
+ self.error_label = error_label
+
+ self.btn_ok = btn_ok
+ self.btn_cancel = btn_cancel
def setError(self, msg):
self.error_label.setText(msg)
@@ -212,6 +182,17 @@ class ClockifySettings(QtWidgets.QWidget):
"Entered invalid API key"
)
+ def showEvent(self, event):
+ super(ClockifySettings, self).showEvent(event)
+
+ # Make btns same width
+ max_width = max(
+ self.btn_ok.sizeHint().width(),
+ self.btn_cancel.sizeHint().width()
+ )
+ self.btn_ok.setMinimumWidth(max_width)
+ self.btn_cancel.setMinimumWidth(max_width)
+
def closeEvent(self, event):
if self.optional is True:
event.ignore()
diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
index 52ed9bba76..41f8337fd8 100644
--- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -231,7 +231,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
args = [
'publish',
- roothless_metadata_path
+ roothless_metadata_path,
+ "--targets {}".format("deadline")
]
# Generate the payload for Deadline submission
diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
new file mode 100644
index 0000000000..c71b5106ec
--- /dev/null
+++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
@@ -0,0 +1,186 @@
+import os
+import json
+import pyblish.api
+
+from avalon.vendor import requests
+
+from openpype.api import get_system_settings
+from openpype.lib.abstract_submit_deadline import requests_get
+from openpype.lib.delivery import collect_frames
+
+
+class ValidateExpectedFiles(pyblish.api.InstancePlugin):
+ """Compare rendered and expected files"""
+
+ label = "Validate rendered files from Deadline"
+ order = pyblish.api.ValidatorOrder
+ families = ["render"]
+ targets = ["deadline"]
+
+ # check if actual frame range on render job wasn't different
+ # case when artists wants to render only subset of frames
+ allow_user_override = True
+
+ def process(self, instance):
+ frame_list = self._get_frame_list(instance.data["render_job_id"])
+
+ for repre in instance.data["representations"]:
+ expected_files = self._get_expected_files(repre)
+
+ staging_dir = repre["stagingDir"]
+ existing_files = self._get_existing_files(staging_dir)
+
+ expected_non_existent = expected_files.difference(
+ existing_files)
+ if len(expected_non_existent) != 0:
+ self.log.info("Some expected files missing {}".format(
+ expected_non_existent))
+
+ if self.allow_user_override:
+ file_name_template, frame_placeholder = \
+ self._get_file_name_template_and_placeholder(
+ expected_files)
+
+ if not file_name_template:
+ return
+
+ real_expected_rendered = self._get_real_render_expected(
+ file_name_template,
+ frame_placeholder,
+ frame_list)
+
+ real_expected_non_existent = \
+ real_expected_rendered.difference(existing_files)
+ if len(real_expected_non_existent) != 0:
+ raise RuntimeError("Still missing some files {}".
+ format(real_expected_non_existent))
+ self.log.info("Update range from actual job range")
+ repre["files"] = sorted(list(real_expected_rendered))
+ else:
+ raise RuntimeError("Some expected files missing {}".format(
+ expected_non_existent))
+
+ def _get_frame_list(self, original_job_id):
+ """
+ Returns list of frame ranges from all render job.
+
+ Render job might be requeried so job_id in metadata.json is invalid
+ GlobalJobPreload injects current ids to RENDER_JOB_IDS.
+
+ Args:
+ original_job_id (str)
+ Returns:
+ (list)
+ """
+ all_frame_lists = []
+ render_job_ids = os.environ.get("RENDER_JOB_IDS")
+ if render_job_ids:
+ render_job_ids = render_job_ids.split(',')
+ else: # fallback
+ render_job_ids = [original_job_id]
+
+ for job_id in render_job_ids:
+ job_info = self._get_job_info(job_id)
+ frame_list = job_info["Props"]["Frames"]
+ if frame_list:
+ all_frame_lists.extend(frame_list.split(','))
+
+ return all_frame_lists
+
+ def _get_real_render_expected(self, file_name_template, frame_placeholder,
+ frame_list):
+ """
+ Calculates list of names of expected rendered files.
+
+ Might be different from job expected files if user explicitly and
+ manually change frame list on Deadline job.
+ """
+ real_expected_rendered = set()
+ src_padding_exp = "%0{}d".format(len(frame_placeholder))
+ for frames in frame_list:
+ if '-' not in frames: # single frame
+ frames = "{}-{}".format(frames, frames)
+
+ start, end = frames.split('-')
+ for frame in range(int(start), int(end) + 1):
+ ren_name = file_name_template.replace(
+ frame_placeholder, src_padding_exp % frame)
+ real_expected_rendered.add(ren_name)
+
+ return real_expected_rendered
+
+ def _get_file_name_template_and_placeholder(self, files):
+ """Returns file name with frame replaced with # and this placeholder"""
+ sources_and_frames = collect_frames(files)
+
+ file_name_template = frame_placeholder = None
+ for file_name, frame in sources_and_frames.items():
+ frame_placeholder = "#" * len(frame)
+ file_name_template = os.path.basename(
+ file_name.replace(frame, frame_placeholder))
+ break
+
+ return file_name_template, frame_placeholder
+
+ def _get_job_info(self, job_id):
+ """
+ Calls DL for actual job info for 'job_id'
+
+ Might be different than job info saved in metadata.json if user
+ manually changes job pre/during rendering.
+ """
+ deadline_url = (
+ get_system_settings()
+ ["modules"]
+ ["deadline"]
+ ["DEADLINE_REST_URL"]
+ )
+ assert deadline_url, "Requires DEADLINE_REST_URL"
+
+ url = "{}/api/jobs?JobID={}".format(deadline_url, job_id)
+ try:
+ response = requests_get(url)
+ except requests.exceptions.ConnectionError:
+ print("Deadline is not accessible at {}".format(deadline_url))
+ # self.log("Deadline is not accessible at {}".format(deadline_url))
+ return {}
+
+ if not response.ok:
+ self.log.error("Submission failed!")
+ self.log.error(response.status_code)
+ self.log.error(response.content)
+ raise RuntimeError(response.text)
+
+ json_content = response.json()
+ if json_content:
+ return json_content.pop()
+ return {}
+
+ def _parse_metadata_json(self, json_path):
+ if not os.path.exists(json_path):
+ msg = "Metadata file {} doesn't exist".format(json_path)
+ raise RuntimeError(msg)
+
+ with open(json_path) as fp:
+ try:
+ return json.load(fp)
+ except Exception as exc:
+ self.log.error(
+ "Error loading json: "
+ "{} - Exception: {}".format(json_path, exc)
+ )
+
+ def _get_existing_files(self, out_dir):
+ """Returns set of existing file names from 'out_dir'"""
+ existing_files = set()
+ for file_name in os.listdir(out_dir):
+ existing_files.add(file_name)
+ return existing_files
+
+ def _get_expected_files(self, repre):
+ """Returns set of file names from metadata.json"""
+ expected_files = set()
+
+ for file_name in repre["files"]:
+ expected_files.add(file_name)
+ return expected_files
diff --git a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py
index c0b3137455..81719258e1 100644
--- a/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py
+++ b/openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py
@@ -2,7 +2,10 @@ import collections
import datetime
import ftrack_api
-from openpype.modules.ftrack.lib import BaseEvent
+from openpype.modules.ftrack.lib import (
+ BaseEvent,
+ query_custom_attributes
+)
class PushFrameValuesToTaskEvent(BaseEvent):
@@ -55,10 +58,6 @@ class PushFrameValuesToTaskEvent(BaseEvent):
if entity_info.get("entityType") != "task":
continue
- # Skip `Task` entity type
- if entity_info["entity_type"].lower() == "task":
- continue
-
# Care only about changes of status
changes = entity_info.get("changes")
if not changes:
@@ -74,6 +73,14 @@ class PushFrameValuesToTaskEvent(BaseEvent):
if project_id is None:
continue
+ # Skip `Task` entity type if parent didn't change
+ if entity_info["entity_type"].lower() == "task":
+ if (
+ "parent_id" not in changes
+ or changes["parent_id"]["new"] is None
+ ):
+ continue
+
if project_id not in entities_info_by_project_id:
entities_info_by_project_id[project_id] = []
entities_info_by_project_id[project_id].append(entity_info)
@@ -117,11 +124,24 @@ class PushFrameValuesToTaskEvent(BaseEvent):
))
return
+ interest_attributes = set(interest_attributes)
+ interest_entity_types = set(interest_entity_types)
+
+ # Separate value changes and task parent changes
+ _entities_info = []
+ task_parent_changes = []
+ for entity_info in entities_info:
+ if entity_info["entity_type"].lower() == "task":
+ task_parent_changes.append(entity_info)
+ else:
+ _entities_info.append(entity_info)
+ entities_info = _entities_info
+
# Filter entities info with changes
interesting_data, changed_keys_by_object_id = self.filter_changes(
session, event, entities_info, interest_attributes
)
- if not interesting_data:
+ if not interesting_data and not task_parent_changes:
return
# Prepare object types
@@ -131,6 +151,289 @@ class PushFrameValuesToTaskEvent(BaseEvent):
name_low = object_type["name"].lower()
object_types_by_name[name_low] = object_type
+ # NOTE it would be nice to check if `interesting_data` do not contain
+ # value changs of tasks that were created or moved
+ # - it is a complex way how to find out
+ if interesting_data:
+ self.process_attribute_changes(
+ session, object_types_by_name,
+ interesting_data, changed_keys_by_object_id,
+ interest_entity_types, interest_attributes
+ )
+
+ if task_parent_changes:
+ self.process_task_parent_change(
+ session, object_types_by_name, task_parent_changes,
+ interest_entity_types, interest_attributes
+ )
+
+ def process_task_parent_change(
+ self, session, object_types_by_name, task_parent_changes,
+ interest_entity_types, interest_attributes
+ ):
+ """Push custom attribute values if task parent has changed.
+
+ Parent is changed if task is created or if is moved under different
+ entity. We don't care about all task changes only about those that
+ have it's parent in interest types (from settings).
+
+ Tasks hierarchical value should be unset or set based on parents
+ real hierarchical value and non hierarchical custom attribute value
+ should be set to hierarchical value.
+ """
+ # Store task ids which were created or moved under parent with entity
+ # type defined in settings (interest_entity_types).
+ task_ids = set()
+ # Store parent ids of matching task ids
+ matching_parent_ids = set()
+ # Store all entity ids of all entities to be able query hierarchical
+ # values.
+ whole_hierarchy_ids = set()
+ # Store parent id of each entity id
+ parent_id_by_entity_id = {}
+ for entity_info in task_parent_changes:
+ # Ignore entities with less parents than 2
+ # NOTE entity itself is also part of "parents" value
+ parents = entity_info.get("parents") or []
+ if len(parents) < 2:
+ continue
+
+ parent_info = parents[1]
+ # Check if parent has entity type we care about.
+ if parent_info["entity_type"] not in interest_entity_types:
+ continue
+
+ task_ids.add(entity_info["entityId"])
+ matching_parent_ids.add(parent_info["entityId"])
+
+ # Store whole hierarchi of task entity
+ prev_id = None
+ for item in parents:
+ item_id = item["entityId"]
+ whole_hierarchy_ids.add(item_id)
+
+ if prev_id is None:
+ prev_id = item_id
+ continue
+
+ parent_id_by_entity_id[prev_id] = item_id
+ if item["entityType"] == "show":
+ break
+ prev_id = item_id
+
+ # Just skip if nothing is interesting for our settings
+ if not matching_parent_ids:
+ return
+
+ # Query object type ids of parent ids for custom attribute
+ # definitions query
+ entities = session.query(
+ "select object_type_id from TypedContext where id in ({})".format(
+ self.join_query_keys(matching_parent_ids)
+ )
+ )
+
+ # Prepare task object id
+ task_object_id = object_types_by_name["task"]["id"]
+
+ # All object ids for which we're querying custom attribute definitions
+ object_type_ids = set()
+ object_type_ids.add(task_object_id)
+ for entity in entities:
+ object_type_ids.add(entity["object_type_id"])
+
+ attrs_by_obj_id, hier_attrs = self.attrs_configurations(
+ session, object_type_ids, interest_attributes
+ )
+
+ # Skip if all task attributes are not available
+ task_attrs = attrs_by_obj_id.get(task_object_id)
+ if not task_attrs:
+ return
+
+ # Skip attributes that is not in both hierarchical and nonhierarchical
+ # TODO be able to push values if hierarchical is available
+ for key in interest_attributes:
+ if key not in hier_attrs:
+ task_attrs.pop(key, None)
+
+ elif key not in task_attrs:
+ hier_attrs.pop(key)
+
+ # Skip if nothing remained
+ if not task_attrs:
+ return
+
+ # Do some preparations for custom attribute values query
+ attr_key_by_id = {}
+ nonhier_id_by_key = {}
+ hier_attr_ids = []
+ for key, attr_id in hier_attrs.items():
+ attr_key_by_id[attr_id] = key
+ hier_attr_ids.append(attr_id)
+
+ conf_ids = list(hier_attr_ids)
+ for key, attr_id in task_attrs.items():
+ attr_key_by_id[attr_id] = key
+ nonhier_id_by_key[key] = attr_id
+ conf_ids.append(attr_id)
+
+ # Query custom attribute values
+ # - result does not contain values for all entities only result of
+ # query callback to ftrack server
+ result = query_custom_attributes(
+ session, conf_ids, whole_hierarchy_ids
+ )
+
+ # Prepare variables where result will be stored
+ # - hierachical values should not contain attribute with value by
+ # default
+ hier_values_by_entity_id = {
+ entity_id: {}
+ for entity_id in whole_hierarchy_ids
+ }
+ # - real values of custom attributes
+ values_by_entity_id = {
+ entity_id: {
+ attr_id: None
+ for attr_id in conf_ids
+ }
+ for entity_id in whole_hierarchy_ids
+ }
+ for item in result:
+ attr_id = item["configuration_id"]
+ entity_id = item["entity_id"]
+ value = item["value"]
+
+ values_by_entity_id[entity_id][attr_id] = value
+
+ if attr_id in hier_attr_ids and value is not None:
+ hier_values_by_entity_id[entity_id][attr_id] = value
+
+ # Prepare values for all task entities
+ # - going through all parents and storing first value value
+ # - store None to those that are already known that do not have set
+ # value at all
+ for task_id in tuple(task_ids):
+ for attr_id in hier_attr_ids:
+ entity_ids = []
+ value = None
+ entity_id = task_id
+ while value is None:
+ entity_value = hier_values_by_entity_id[entity_id]
+ if attr_id in entity_value:
+ value = entity_value[attr_id]
+ if value is None:
+ break
+
+ if value is None:
+ entity_ids.append(entity_id)
+
+ entity_id = parent_id_by_entity_id.get(entity_id)
+ if entity_id is None:
+ break
+
+ for entity_id in entity_ids:
+ hier_values_by_entity_id[entity_id][attr_id] = value
+
+ # Prepare changes to commit
+ changes = []
+ for task_id in tuple(task_ids):
+ parent_id = parent_id_by_entity_id[task_id]
+ for attr_id in hier_attr_ids:
+ attr_key = attr_key_by_id[attr_id]
+ nonhier_id = nonhier_id_by_key[attr_key]
+
+ # Real value of hierarchical attribute on parent
+ # - If is none then should be unset
+ real_parent_value = values_by_entity_id[parent_id][attr_id]
+ # Current hierarchical value of a task
+ # - Will be compared to real parent value
+ hier_value = hier_values_by_entity_id[task_id][attr_id]
+
+ # Parent value that can be inherited from it's parent entity
+ parent_value = hier_values_by_entity_id[parent_id][attr_id]
+ # Task value of nonhierarchical custom attribute
+ nonhier_value = values_by_entity_id[task_id][nonhier_id]
+
+ if real_parent_value != hier_value:
+ changes.append({
+ "new_value": real_parent_value,
+ "attr_id": attr_id,
+ "entity_id": task_id,
+ "attr_key": attr_key
+ })
+
+ if parent_value != nonhier_value:
+ changes.append({
+ "new_value": parent_value,
+ "attr_id": nonhier_id,
+ "entity_id": task_id,
+ "attr_key": attr_key
+ })
+
+ self._commit_changes(session, changes)
+
+ def _commit_changes(self, session, changes):
+ uncommited_changes = False
+ for idx, item in enumerate(changes):
+ new_value = item["new_value"]
+ attr_id = item["attr_id"]
+ entity_id = item["entity_id"]
+ attr_key = item["attr_key"]
+
+ entity_key = collections.OrderedDict()
+ entity_key["configuration_id"] = attr_id
+ entity_key["entity_id"] = entity_id
+ self._cached_changes.append({
+ "attr_key": attr_key,
+ "entity_id": entity_id,
+ "value": new_value,
+ "time": datetime.datetime.now()
+ })
+ if new_value is None:
+ op = ftrack_api.operation.DeleteEntityOperation(
+ "CustomAttributeValue",
+ entity_key
+ )
+ else:
+ op = ftrack_api.operation.UpdateEntityOperation(
+ "ContextCustomAttributeValue",
+ entity_key,
+ "value",
+ ftrack_api.symbol.NOT_SET,
+ new_value
+ )
+
+ session.recorded_operations.push(op)
+ self.log.info((
+ "Changing Custom Attribute \"{}\" to value"
+ " \"{}\" on entity: {}"
+ ).format(attr_key, new_value, entity_id))
+
+ if (idx + 1) % 20 == 0:
+ uncommited_changes = False
+ try:
+ session.commit()
+ except Exception:
+ session.rollback()
+ self.log.warning(
+ "Changing of values failed.", exc_info=True
+ )
+ else:
+ uncommited_changes = True
+ if uncommited_changes:
+ try:
+ session.commit()
+ except Exception:
+ session.rollback()
+ self.log.warning("Changing of values failed.", exc_info=True)
+
+ def process_attribute_changes(
+ self, session, object_types_by_name,
+ interesting_data, changed_keys_by_object_id,
+ interest_entity_types, interest_attributes
+ ):
# Prepare task object id
task_object_id = object_types_by_name["task"]["id"]
@@ -216,13 +519,13 @@ class PushFrameValuesToTaskEvent(BaseEvent):
task_entity_ids.add(task_id)
parent_id_by_task_id[task_id] = task_entity["parent_id"]
- self.finalize(
+ self.finalize_attribute_changes(
session, interesting_data,
changed_keys, attrs_by_obj_id, hier_attrs,
task_entity_ids, parent_id_by_task_id
)
- def finalize(
+ def finalize_attribute_changes(
self, session, interesting_data,
changed_keys, attrs_by_obj_id, hier_attrs,
task_entity_ids, parent_id_by_task_id
@@ -248,6 +551,7 @@ class PushFrameValuesToTaskEvent(BaseEvent):
session, attr_ids, entity_ids, task_entity_ids, hier_attrs
)
+ changes = []
for entity_id, current_values in current_values_by_id.items():
parent_id = parent_id_by_task_id.get(entity_id)
if not parent_id:
@@ -272,39 +576,13 @@ class PushFrameValuesToTaskEvent(BaseEvent):
if new_value == old_value:
continue
- entity_key = collections.OrderedDict()
- entity_key["configuration_id"] = attr_id
- entity_key["entity_id"] = entity_id
- self._cached_changes.append({
- "attr_key": attr_key,
+ changes.append({
+ "new_value": new_value,
+ "attr_id": attr_id,
"entity_id": entity_id,
- "value": new_value,
- "time": datetime.datetime.now()
+ "attr_key": attr_key
})
- if new_value is None:
- op = ftrack_api.operation.DeleteEntityOperation(
- "CustomAttributeValue",
- entity_key
- )
- else:
- op = ftrack_api.operation.UpdateEntityOperation(
- "ContextCustomAttributeValue",
- entity_key,
- "value",
- ftrack_api.symbol.NOT_SET,
- new_value
- )
-
- session.recorded_operations.push(op)
- self.log.info((
- "Changing Custom Attribute \"{}\" to value"
- " \"{}\" on entity: {}"
- ).format(attr_key, new_value, entity_id))
- try:
- session.commit()
- except Exception:
- session.rollback()
- self.log.warning("Changing of values failed.", exc_info=True)
+ self._commit_changes(session, changes)
def filter_changes(
self, session, event, entities_info, interest_attributes
diff --git a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
index d20e2ff5a8..f215bedcc2 100644
--- a/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
+++ b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
@@ -66,15 +66,7 @@ class VersionToTaskStatus(BaseEvent):
))
return
- _status_mapping = event_settings["mapping"]
- if not _status_mapping:
- self.log.debug(
- "Project \"{}\" does not have set mapping for {}".format(
- project_name, self.__class__.__name__
- )
- )
- return
-
+ _status_mapping = event_settings["mapping"] or {}
status_mapping = {
key.lower(): value
for key, value in _status_mapping.items()
diff --git a/openpype/modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/ftrack/ftrack_server/socket_thread.py
index fd407bb9f5..eb8ec4d06c 100644
--- a/openpype/modules/ftrack/ftrack_server/socket_thread.py
+++ b/openpype/modules/ftrack/ftrack_server/socket_thread.py
@@ -66,7 +66,16 @@ class SocketThread(threading.Thread):
*self.additional_args,
str(self.port)
)
- self.subproc = subprocess.Popen(args, env=env, stdin=subprocess.PIPE)
+ kwargs = {
+ "env": env,
+ "stdin": subprocess.PIPE
+ }
+ if not sys.stdout:
+ # Redirect to devnull if stdout is None
+ kwargs["stdout"] = subprocess.DEVNULL
+ kwargs["stderr"] = subprocess.DEVNULL
+
+ self.subproc = subprocess.Popen(args, **kwargs)
# Listen for incoming connections
sock.listen(1)
diff --git a/openpype/modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py
index ce6d5284b6..9dc2d67279 100644
--- a/openpype/modules/ftrack/lib/__init__.py
+++ b/openpype/modules/ftrack/lib/__init__.py
@@ -13,7 +13,8 @@ from .custom_attributes import (
default_custom_attributes_definition,
app_definitions_from_app_manager,
tool_definitions_from_app_manager,
- get_openpype_attr
+ get_openpype_attr,
+ query_custom_attributes
)
from . import avalon_sync
@@ -37,6 +38,7 @@ __all__ = (
"app_definitions_from_app_manager",
"tool_definitions_from_app_manager",
"get_openpype_attr",
+ "query_custom_attributes",
"avalon_sync",
diff --git a/openpype/modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py
index 5d1da005dc..2458308af5 100644
--- a/openpype/modules/ftrack/lib/avalon_sync.py
+++ b/openpype/modules/ftrack/lib/avalon_sync.py
@@ -402,16 +402,18 @@ class SyncEntitiesFactory:
items = []
items.append({
"type": "label",
- "value": "# Can't access Custom attribute <{}>".format(
- CUST_ATTR_ID_KEY
- )
+ "value": (
+ "# Can't access Custom attribute: \"{}\""
+ ).format(CUST_ATTR_ID_KEY)
})
items.append({
"type": "label",
"value": (
- "
- Check if user \"{}\" has permissions"
- " to access the Custom attribute
"
- ).format(self._api_key)
+ "
- Check if your User and API key has permissions"
+ " to access the Custom attribute."
+ " Username:\"{}\""
+ " API key:\"{}\"
"
+ ).format(self._api_user, self._api_key)
})
items.append({
"type": "label",
diff --git a/openpype/modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py
index f6b82c90b1..53facd4ab2 100644
--- a/openpype/modules/ftrack/lib/custom_attributes.py
+++ b/openpype/modules/ftrack/lib/custom_attributes.py
@@ -81,3 +81,60 @@ def get_openpype_attr(session, split_hierarchical=True, query_keys=None):
return custom_attributes, hier_custom_attributes
return custom_attributes
+
+
+def join_query_keys(keys):
+ """Helper to join keys to query."""
+ return ",".join(["\"{}\"".format(key) for key in keys])
+
+
+def query_custom_attributes(session, conf_ids, entity_ids, table_name=None):
+ """Query custom attribute values from ftrack database.
+
+ Using ftrack call method result may differ based on used table name and
+ version of ftrack server.
+
+ Args:
+ session(ftrack_api.Session): Connected ftrack session.
+ conf_id(list, set, tuple): Configuration(attribute) ids which are
+ queried.
+ entity_ids(list, set, tuple): Entity ids for which are values queried.
+ table_name(str): Table nam from which values are queried. Not
+ recommended to change until you know what it means.
+ """
+ output = []
+ # Just skip
+ if not conf_ids or not entity_ids:
+ return output
+
+ if table_name is None:
+ table_name = "ContextCustomAttributeValue"
+
+ # Prepare values to query
+ attributes_joined = join_query_keys(conf_ids)
+ attributes_len = len(conf_ids)
+
+ # Query values in chunks
+ chunk_size = int(5000 / attributes_len)
+ # Make sure entity_ids is `list` for chunk selection
+ entity_ids = list(entity_ids)
+ for idx in range(0, len(entity_ids), chunk_size):
+ entity_ids_joined = join_query_keys(
+ entity_ids[idx:idx + chunk_size]
+ )
+
+ call_expr = [{
+ "action": "query",
+ "expression": (
+ "select value, entity_id from {}"
+ " where entity_id in ({}) and configuration_id in ({})"
+ ).format(table_name, entity_ids_joined, attributes_joined)
+ }]
+ if hasattr(session, "call"):
+ [result] = session.call(call_expr)
+ else:
+ [result] = session._call(call_expr)
+
+ for item in result["data"]:
+ output.append(item)
+ return output
diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py
index e6daed9a33..b505a429b5 100644
--- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py
+++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py
@@ -1,29 +1,107 @@
+"""
+Requires:
+ none
+
+Provides:
+ instance -> families ([])
+"""
import pyblish.api
+import avalon.api
+
+from openpype.lib.plugin_tools import filter_profiles
-class CollectFtrackFamilies(pyblish.api.InstancePlugin):
- """Collect family for ftrack publishing
-
- Add ftrack family to those instance that should be published to ftrack
-
+class CollectFtrackFamily(pyblish.api.InstancePlugin):
"""
+ Adds explicitly 'ftrack' to families to upload instance to FTrack.
- order = pyblish.api.CollectorOrder + 0.3
- label = 'Add ftrack family'
- families = ["model",
- "setdress",
- "model",
- "animation",
- "look",
- "rig",
- "camera"
- ]
- hosts = ["maya"]
+ Uses selection by combination of hosts/families/tasks names via
+ profiles resolution.
+
+ Triggered everywhere, checks instance against configured.
+
+ Checks advanced filtering which works on 'families' not on main
+ 'family', as some variants dynamically resolves addition of ftrack
+ based on 'families' (editorial drives it by presence of 'review')
+ """
+ label = "Collect Ftrack Family"
+ order = pyblish.api.CollectorOrder + 0.4998
+
+ profiles = None
def process(self, instance):
+ if not self.profiles:
+ self.log.warning("No profiles present for adding Ftrack family")
+ return
- # make ftrack publishable
- if instance.data.get('families'):
- instance.data['families'].append('ftrack')
+ task_name = instance.data.get("task",
+ avalon.api.Session["AVALON_TASK"])
+ host_name = avalon.api.Session["AVALON_APP"]
+ family = instance.data["family"]
+
+ filtering_criteria = {
+ "hosts": host_name,
+ "families": family,
+ "tasks": task_name
+ }
+ profile = filter_profiles(self.profiles, filtering_criteria,
+ logger=self.log)
+
+ if profile:
+ families = instance.data.get("families")
+ add_ftrack_family = profile["add_ftrack_family"]
+
+ additional_filters = profile.get("additional_filters")
+ if additional_filters:
+ add_ftrack_family = self._get_add_ftrack_f_from_addit_filters(
+ additional_filters,
+ families,
+ add_ftrack_family
+ )
+
+ if add_ftrack_family:
+ self.log.debug("Adding ftrack family for '{}'".
+ format(instance.data.get("family")))
+
+ if families and "ftrack" not in families:
+ instance.data["families"].append("ftrack")
+ else:
+ instance.data["families"] = ["ftrack"]
else:
- instance.data['families'] = ['ftrack']
+ self.log.debug("Instance '{}' doesn't match any profile".format(
+ instance.data.get("family")))
+
+ def _get_add_ftrack_f_from_addit_filters(self,
+ additional_filters,
+ families,
+ add_ftrack_family):
+ """
+ Compares additional filters - working on instance's families.
+
+ Triggered for more detailed filtering when main family matches,
+ but content of 'families' actually matter.
+ (For example 'review' in 'families' should result in adding to
+ Ftrack)
+
+ Args:
+ additional_filters (dict) - from Setting
+ families (list) - subfamilies
+ add_ftrack_family (bool) - add ftrack to families if True
+ """
+ override_filter = None
+ override_filter_value = -1
+ for additional_filter in additional_filters:
+ filter_families = set(additional_filter["families"])
+ valid = filter_families <= set(families) # issubset
+ if not valid:
+ continue
+
+ value = len(filter_families)
+ if value > override_filter_value:
+ override_filter = additional_filter
+ override_filter_value = value
+
+ if override_filter:
+ add_ftrack_family = override_filter["add_ftrack_family"]
+
+ return add_ftrack_family
diff --git a/openpype/modules/ftrack/tray/login_dialog.py b/openpype/modules/ftrack/tray/login_dialog.py
index a6360a7380..cc5689bee5 100644
--- a/openpype/modules/ftrack/tray/login_dialog.py
+++ b/openpype/modules/ftrack/tray/login_dialog.py
@@ -1,6 +1,6 @@
import os
import requests
-from avalon import style
+from openpype import style
from openpype.modules.ftrack.lib import credentials
from . import login_tools
from openpype import resources
@@ -46,8 +46,11 @@ class CredentialsDialog(QtWidgets.QDialog):
self.user_label = QtWidgets.QLabel("Username:")
self.api_label = QtWidgets.QLabel("API Key:")
- self.ftsite_input = QtWidgets.QLineEdit()
- self.ftsite_input.setReadOnly(True)
+ self.ftsite_input = QtWidgets.QLabel()
+ self.ftsite_input.setTextInteractionFlags(
+ QtCore.Qt.TextBrowserInteraction
+ )
+ # self.ftsite_input.setReadOnly(True)
self.ftsite_input.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor))
self.user_input = QtWidgets.QLineEdit()
diff --git a/openpype/modules/log_viewer/tray/app.py b/openpype/modules/log_viewer/tray/app.py
index c0e180c8a1..9aab37cd20 100644
--- a/openpype/modules/log_viewer/tray/app.py
+++ b/openpype/modules/log_viewer/tray/app.py
@@ -1,6 +1,6 @@
from Qt import QtWidgets, QtCore
from .widgets import LogsWidget, OutputWidget
-from avalon import style
+from openpype import style
class LogsWindow(QtWidgets.QWidget):
@@ -14,7 +14,7 @@ class LogsWindow(QtWidgets.QWidget):
main_layout = QtWidgets.QHBoxLayout()
- log_splitter = QtWidgets.QSplitter()
+ log_splitter = QtWidgets.QSplitter(self)
log_splitter.setOrientation(QtCore.Qt.Horizontal)
log_splitter.addWidget(logs_widget)
log_splitter.addWidget(log_detail)
diff --git a/openpype/modules/log_viewer/tray/widgets.py b/openpype/modules/log_viewer/tray/widgets.py
index cd0df283bf..b9a8499a4c 100644
--- a/openpype/modules/log_viewer/tray/widgets.py
+++ b/openpype/modules/log_viewer/tray/widgets.py
@@ -83,7 +83,6 @@ class CustomCombo(QtWidgets.QWidget):
self.setLayout(layout)
- # toolmenu.selection_changed.connect(self.on_selection_changed)
toolmenu.selection_changed.connect(self.selection_changed)
self.toolbutton = toolbutton
@@ -119,7 +118,6 @@ class LogsWidget(QtWidgets.QWidget):
filter_layout = QtWidgets.QHBoxLayout()
- # user_filter = SearchComboBox(self, "Users")
user_filter = CustomCombo("Users", self)
users = model.dbcon.distinct("username")
user_filter.populate(users)
@@ -128,21 +126,18 @@ class LogsWidget(QtWidgets.QWidget):
proxy_model.update_users_filter(users)
level_filter = CustomCombo("Levels", self)
- # levels = [(level, True) for level in model.dbcon.distinct("level")]
levels = model.dbcon.distinct("level")
level_filter.addItems(levels)
level_filter.selection_changed.connect(self._level_changed)
detail_widget.update_level_filter(levels)
- spacer = QtWidgets.QWidget()
-
icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(icon, "")
filter_layout.addWidget(user_filter)
filter_layout.addWidget(level_filter)
- filter_layout.addWidget(spacer, 1)
+ filter_layout.addStretch(1)
filter_layout.addWidget(refresh_btn)
view = QtWidgets.QTreeView(self)
diff --git a/openpype/modules/muster/widget_login.py b/openpype/modules/muster/widget_login.py
index d9af4cb99f..231b52c6bd 100644
--- a/openpype/modules/muster/widget_login.py
+++ b/openpype/modules/muster/widget_login.py
@@ -1,13 +1,12 @@
import os
from Qt import QtCore, QtGui, QtWidgets
-from avalon import style
-from openpype import resources
+from openpype import resources, style
class MusterLogin(QtWidgets.QWidget):
SIZE_W = 300
- SIZE_H = 130
+ SIZE_H = 150
loginSignal = QtCore.Signal(object, object, object)
@@ -123,7 +122,6 @@ class MusterLogin(QtWidgets.QWidget):
super().keyPressEvent(key_event)
def setError(self, msg):
-
self.error_label.setText(msg)
self.error_label.show()
@@ -149,6 +147,17 @@ class MusterLogin(QtWidgets.QWidget):
def save_credentials(self, username, password):
self.module.get_auth_token(username, password)
+ def showEvent(self, event):
+ super(MusterLogin, self).showEvent(event)
+
+ # Make btns same width
+ max_width = max(
+ self.btn_ok.sizeHint().width(),
+ self.btn_cancel.sizeHint().width()
+ )
+ self.btn_ok.setMinimumWidth(max_width)
+ self.btn_cancel.setMinimumWidth(max_width)
+
def closeEvent(self, event):
event.ignore()
self._close_widget()
diff --git a/openpype/modules/settings_action.py b/openpype/modules/settings_action.py
index 1035dc0dcd..9db4a252bc 100644
--- a/openpype/modules/settings_action.py
+++ b/openpype/modules/settings_action.py
@@ -114,6 +114,7 @@ class LocalSettingsAction(PypeModule, ITrayAction):
# Tray attributes
self.settings_window = None
+ self._first_trigger = True
def connect_with_modules(self, *_a, **_kw):
return
@@ -153,6 +154,9 @@ class LocalSettingsAction(PypeModule, ITrayAction):
self.settings_window.raise_()
self.settings_window.activateWindow()
- # Reset content if was not visible
- if not was_visible:
+ # Do not reset if it's first trigger of action
+ if self._first_trigger:
+ self._first_trigger = False
+ elif not was_visible:
+ # Reset content if was not visible
self.settings_window.reset()
diff --git a/openpype/modules/slack/README.md b/openpype/modules/slack/README.md
new file mode 100644
index 0000000000..baf0f9a1ec
--- /dev/null
+++ b/openpype/modules/slack/README.md
@@ -0,0 +1,50 @@
+Slack notification for publishing
+---------------------------------
+
+This module allows configuring profiles(when to trigger, for which combination of task, host and family)
+and templates(could contain {} placeholder, as "{asset} published").
+
+These need to be configured in
+```Project settings > Slack > Publish plugins > Notification to Slack```
+
+Slack module must be enabled in System Setting, could be configured per Project.
+
+## App installation
+
+Slack app needs to be installed to company's workspace. Attached .yaml file could be
+used, follow instruction https://api.slack.com/reference/manifests#using
+
+## Settings
+
+### Token
+Most important for module to work is to fill authentication token
+```Project settings > Slack > Publish plugins > Token```
+
+This token should be available after installation of app in Slack dashboard.
+It is possible to create multiple tokens and configure different scopes for them.
+
+### Profiles
+Profiles are used to select when to trigger notification. One or multiple profiles
+could be configured, 'family', 'task name' (regex available) and host combination is needed.
+
+Eg. If I want to be notified when render is published from Maya, setting is:
+
+- family: 'render'
+- host: 'Maya'
+
+### Channel
+Message could be delivered to one or multiple channels, by default app allows Slack bot
+to send messages to 'public' channels (eg. bot doesn't need to join channel first).
+
+This could be configured in Slack dashboard and scopes might be modified.
+
+### Message
+Placeholders {} could be used in message content which will be filled during runtime.
+Only keys available in 'anatomyData' are currently implemented.
+
+Example of message content:
+```{SUBSET} for {Asset} was published.```
+
+Integration can upload 'thumbnail' file (if present in instance), for that bot must be
+manually added to target channel by Slack admin!
+(In target channel write: ```/invite @OpenPypeNotifier``)
\ No newline at end of file
diff --git a/openpype/modules/slack/__init__.py b/openpype/modules/slack/__init__.py
new file mode 100644
index 0000000000..0a09d24ed4
--- /dev/null
+++ b/openpype/modules/slack/__init__.py
@@ -0,0 +1,9 @@
+from .slack_module import (
+ SlackIntegrationModule,
+ SLACK_MODULE_DIR
+)
+
+__all__ = (
+ "SlackIntegrationModule",
+ "SLACK_MODULE_DIR"
+)
diff --git a/openpype/modules/slack/launch_hooks/pre_python2_vendor.py b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py
new file mode 100644
index 0000000000..a2c1f8a9e0
--- /dev/null
+++ b/openpype/modules/slack/launch_hooks/pre_python2_vendor.py
@@ -0,0 +1,34 @@
+import os
+from openpype.lib import PreLaunchHook
+from openpype.modules.slack import SLACK_MODULE_DIR
+
+
+class PrePython2Support(PreLaunchHook):
+ """Add python slack api module for Python 2 to PYTHONPATH.
+
+ Path to vendor modules is added to the beginning of PYTHONPATH.
+ """
+
+ def execute(self):
+ if not self.application.use_python_2:
+ return
+
+ self.log.info("Adding Slack Python 2 packages to PYTHONPATH.")
+
+ # Prepare vendor dir path
+ python_2_vendor = os.path.join(SLACK_MODULE_DIR, "python2_vendor")
+
+ # Add Python 2 modules
+ python_paths = [
+ # `python-ftrack-api`
+ os.path.join(python_2_vendor, "python-slack-sdk-1", "slackclient"),
+ os.path.join(python_2_vendor, "python-slack-sdk-1")
+ ]
+ self.log.info("python_paths {}".format(python_paths))
+ # Load PYTHONPATH from current launch context
+ python_path = self.launch_context.env.get("PYTHONPATH")
+ if python_path:
+ python_paths.append(python_path)
+
+ # Set new PYTHONPATH to launch context environments
+ self.launch_context.env["PYTHONPATH"] = os.pathsep.join(python_paths)
diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml
new file mode 100644
index 0000000000..37d4669903
--- /dev/null
+++ b/openpype/modules/slack/manifest.yml
@@ -0,0 +1,23 @@
+_metadata:
+ major_version: 1
+ minor_version: 1
+display_information:
+ name: OpenPypeNotifier
+features:
+ app_home:
+ home_tab_enabled: false
+ messages_tab_enabled: true
+ messages_tab_read_only_enabled: true
+ bot_user:
+ display_name: OpenPypeNotifier
+ always_online: false
+oauth_config:
+ scopes:
+ bot:
+ - chat:write
+ - chat:write.public
+ - files:write
+settings:
+ org_deploy_enabled: false
+ socket_mode_enabled: false
+ is_hosted: false
diff --git a/openpype/modules/slack/plugins/publish/collect_slack_family.py b/openpype/modules/slack/plugins/publish/collect_slack_family.py
new file mode 100644
index 0000000000..2110c0703b
--- /dev/null
+++ b/openpype/modules/slack/plugins/publish/collect_slack_family.py
@@ -0,0 +1,53 @@
+from avalon import io
+import pyblish.api
+
+from openpype.lib.profiles_filtering import filter_profiles
+
+
+class CollectSlackFamilies(pyblish.api.InstancePlugin):
+ """Collect family for Slack notification
+
+ Expects configured profile in
+ Project settings > Slack > Publish plugins > Notification to Slack
+
+ Add Slack family to those instance that should be messaged to Slack
+ """
+ order = pyblish.api.CollectorOrder + 0.4999
+ label = 'Collect Slack family'
+
+ profiles = None
+
+ def process(self, instance):
+ task_name = io.Session.get("AVALON_TASK")
+ family = self.main_family_from_instance(instance)
+
+ key_values = {
+ "families": family,
+ "tasks": task_name,
+ "hosts": instance.data["anatomyData"]["app"],
+ }
+
+ profile = filter_profiles(self.profiles, key_values,
+ logger=self.log)
+
+ # make slack publishable
+ if profile:
+ if instance.data.get('families'):
+ instance.data['families'].append('slack')
+ else:
+ instance.data['families'] = ['slack']
+
+ instance.data["slack_channel_message_profiles"] = \
+ profile["channel_messages"]
+
+ slack_token = (instance.context.data["project_settings"]
+ ["slack"]
+ ["token"])
+ instance.data["slack_token"] = slack_token
+
+ def main_family_from_instance(self, instance): # TODO yank from integrate
+ """Returns main family of entered instance."""
+ family = instance.data.get("family")
+ if not family:
+ family = instance.data["families"][0]
+ return family
diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py
new file mode 100644
index 0000000000..7b81d3c364
--- /dev/null
+++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py
@@ -0,0 +1,155 @@
+import os
+import six
+import pyblish.api
+import copy
+
+from openpype.lib.plugin_tools import prepare_template_data
+
+
+class IntegrateSlackAPI(pyblish.api.InstancePlugin):
+ """ Send message notification to a channel.
+ Triggers on instances with "slack" family, filled by
+ 'collect_slack_family'.
+ Expects configured profile in
+ Project settings > Slack > Publish plugins > Notification to Slack.
+ If instance contains 'thumbnail' it uploads it. Bot must be present
+ in the target channel.
+ Message template can contain {} placeholders from anatomyData.
+ """
+ order = pyblish.api.IntegratorOrder + 0.499
+ label = "Integrate Slack Api"
+ families = ["slack"]
+
+ optional = True
+
+ def process(self, instance):
+ published_path = self._get_thumbnail_path(instance)
+
+ for message_profile in instance.data["slack_channel_message_profiles"]:
+ message = self._get_filled_message(message_profile["message"],
+ instance)
+ if not message:
+ return
+
+ for channel in message_profile["channels"]:
+ if six.PY2:
+ self._python2_call(instance.data["slack_token"],
+ channel,
+ message,
+ published_path,
+ message_profile["upload_thumbnail"])
+ else:
+ self._python3_call(instance.data["slack_token"],
+ channel,
+ message,
+ published_path,
+ message_profile["upload_thumbnail"])
+
+ def _get_filled_message(self, message_templ, instance):
+ """Use message_templ and data from instance to get message content."""
+ fill_data = copy.deepcopy(instance.context.data["anatomyData"])
+
+ fill_pairs = (
+ ("asset", instance.data.get("asset", fill_data.get("asset"))),
+ ("subset", instance.data.get("subset", fill_data.get("subset"))),
+ ("task", instance.data.get("task", fill_data.get("task"))),
+ ("username", instance.data.get("username",
+ fill_data.get("username"))),
+ ("app", instance.data.get("app", fill_data.get("app"))),
+ ("family", instance.data.get("family", fill_data.get("family"))),
+ ("version", str(instance.data.get("version",
+ fill_data.get("version"))))
+ )
+
+ multiple_case_variants = prepare_template_data(fill_pairs)
+ fill_data.update(multiple_case_variants)
+
+ message = None
+ try:
+ message = message_templ.format(**fill_data)
+ except Exception:
+ self.log.warning(
+ "Some keys are missing in {}".format(message_templ),
+ exc_info=True)
+
+ return message
+
+ def _get_thumbnail_path(self, instance):
+ """Returns abs url for thumbnail if present in instance repres"""
+ published_path = None
+ for repre in instance.data['representations']:
+ if repre.get('thumbnail') or "thumbnail" in repre.get('tags', []):
+ repre_files = repre["files"]
+ if isinstance(repre_files, (tuple, list, set)):
+ filename = repre_files[0]
+ else:
+ filename = repre_files
+
+ published_path = os.path.join(
+ repre['stagingDir'], filename
+ )
+ break
+ return published_path
+
+ def _python2_call(self, token, channel, message,
+ published_path, upload_thumbnail):
+ from slackclient import SlackClient
+ try:
+ client = SlackClient(token)
+ if upload_thumbnail and \
+ published_path and os.path.exists(published_path):
+ with open(published_path, 'rb') as pf:
+ response = client.api_call(
+ "files.upload",
+ channels=channel,
+ initial_comment=message,
+ file=pf,
+ title=os.path.basename(published_path)
+ )
+ else:
+ response = client.api_call(
+ "chat.postMessage",
+ channel=channel,
+ text=message
+ )
+
+ if response.get("error"):
+ error_str = self._enrich_error(str(response.get("error")),
+ channel)
+ self.log.warning("Error happened: {}".format(error_str))
+ except Exception as e:
+ # You will get a SlackApiError if "ok" is False
+ error_str = self._enrich_error(str(e), channel)
+ self.log.warning("Error happened: {}".format(error_str))
+
+ def _python3_call(self, token, channel, message,
+ published_path, upload_thumbnail):
+ from slack_sdk import WebClient
+ from slack_sdk.errors import SlackApiError
+ try:
+ client = WebClient(token=token)
+ if upload_thumbnail and \
+ published_path and os.path.exists(published_path):
+ _ = client.files_upload(
+ channels=channel,
+ initial_comment=message,
+ file=published_path,
+ )
+ else:
+ _ = client.chat_postMessage(
+ channel=channel,
+ text=message
+ )
+ except SlackApiError as e:
+ # You will get a SlackApiError if "ok" is False
+ error_str = self._enrich_error(str(e.response["error"]), channel)
+ self.log.warning("Error happened {}".format(error_str))
+
+ def _enrich_error(self, error_str, channel):
+ """Enhance known errors with more helpful notations."""
+ if 'not_in_channel' in error_str:
+ # there is no file.write.public scope, app must be explicitly in
+ # the channel
+ msg = " - application must added to channel '{}'.".format(channel)
+ error_str += msg + " Ask Slack admin."
+ return error_str
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml
new file mode 100644
index 0000000000..79475caa47
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.appveyor.yml
@@ -0,0 +1,32 @@
+# credit: https://packaging.python.org/guides/supporting-windows-using-appveyor/
+
+environment:
+ matrix:
+ - PYTHON: "C:\\Python27"
+ PYTHON_VERSION: "py27-x86"
+ - PYTHON: "C:\\Python34"
+ PYTHON_VERSION: "py34-x86"
+ - PYTHON: "C:\\Python35"
+ PYTHON_VERSION: "py35-x86"
+ - PYTHON: "C:\\Python27-x64"
+ PYTHON_VERSION: "py27-x64"
+ - PYTHON: "C:\\Python34-x64"
+ PYTHON_VERSION: "py34-x64"
+ - PYTHON: "C:\\Python35-x64"
+ PYTHON_VERSION: "py35-x64"
+
+install:
+ - "%PYTHON%\\python.exe -m pip install wheel"
+ - "%PYTHON%\\python.exe -m pip install -r requirements.txt"
+ - "%PYTHON%\\python.exe -m pip install flake8"
+ - "%PYTHON%\\python.exe -m pip install -r test_requirements.txt"
+
+build: off
+
+test_script:
+ - "%PYTHON%\\python.exe -m flake8 slackclient"
+ - "%PYTHON%\\python.exe -m pytest --cov-report= --cov=slackclient tests"
+
+# maybe `after_test:`?
+on_success:
+ - "%PYTHON%\\python.exe -m codecov -e win-%PYTHON_VERSION%"
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc
new file mode 100644
index 0000000000..8d395b7e3b
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.coveragerc
@@ -0,0 +1,13 @@
+[run]
+branch = True
+source = slackclient
+
+[report]
+exclude_lines =
+ if self.debug:
+ pragma: no cover
+ raise NotImplementedError
+ if __name__ == .__main__.:
+ignore_errors = True
+omit =
+ tests/*
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8 b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8
new file mode 100644
index 0000000000..51b50a0465
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 100
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md
new file mode 100644
index 0000000000..614140b037
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/contributing.md
@@ -0,0 +1,60 @@
+# Contributors Guide
+
+Interested in contributing? Awesome! Before you do though, please read our
+[Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as
+well.
+
+There are many ways you can contribute! :heart:
+
+### Bug Reports and Fixes :bug:
+- If you find a bug, please search for it in the [Issues](https://github.com/slackapi/python-slackclient/issues), and if it isn't already tracked,
+ [create a new issue](https://github.com/slackapi/python-slackclient/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still
+ be reviewed.
+- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`.
+- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number.
+ - Include tests that isolate the bug and verifies that it was fixed.
+
+### New Features :bulb:
+- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackapi/python-slackclient/issues/new).
+- Issues that have been identified as a feature request will be labelled `enhancement`.
+- If you'd like to implement the new feature, please wait for feedback from the project
+ maintainers before spending too much time writing the code. In some cases, `enhancement`s may
+ not align well with the project objectives at the time.
+
+### Tests :mag:, Documentation :books:, Miscellaneous :sparkles:
+- If you'd like to improve the tests, you want to make the documentation clearer, you have an
+ alternative implementation of something that may have advantages over the way its currently
+ done, or you have any other change, we would be happy to hear about it!
+ - If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind.
+ - If not, [open an Issue](https://github.com/slackapi/python-slackclient/issues/new) to discuss the idea first.
+
+If you're new to our project and looking for some way to make your first contribution, look for
+Issues labelled `good first contribution`.
+
+## Requirements
+
+For your contribution to be accepted:
+
+- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla-assistant.io/slackapi/python-slackclient).
+- [x] The test suite must be complete and pass.
+- [x] The changes must be approved by code review.
+- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number.
+
+If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created.
+
+[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z)
+
+## Creating a Pull Request
+
+1. :fork_and_knife: Fork the repository on GitHub.
+2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just
+ to make sure everything is in order.
+3. :herb: Create a new branch and check it out.
+4. :crystal_ball: Make your changes and commit them locally. Magic happens here!
+5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`).
+6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `master` in this
+ repository.
+
+## Maintainers
+
+There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md).
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md
new file mode 100644
index 0000000000..a39638a658
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/issue_template.md
@@ -0,0 +1,48 @@
+### Description
+
+Describe your issue here.
+
+### What type of issue is this? (place an `x` in one of the `[ ]`)
+- [ ] bug
+- [ ] enhancement (feature request)
+- [ ] question
+- [ ] documentation related
+- [ ] testing related
+- [ ] discussion
+
+### Requirements (place an `x` in each of the `[ ]`)
+* [ ] I've read and understood the [Contributing guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.
+* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
+* [ ] I've searched for any related issues and avoided creating a duplicate issue.
+
+---
+
+### Bug Report
+
+Filling out the following details about bugs will help us solve your issue sooner.
+
+#### Reproducible in:
+
+slackclient version:
+
+python version:
+
+OS version(s):
+
+#### Steps to reproduce:
+
+1.
+2.
+3.
+
+#### Expected result:
+
+What you expected to happen
+
+#### Actual result:
+
+What actually happened
+
+#### Attachments:
+
+Logs, screenshots, screencast, sample project, funny gif, etc.
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md
new file mode 100644
index 0000000000..c1f6a22232
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/maintainers_guide.md
@@ -0,0 +1,100 @@
+# Maintainers Guide
+
+This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain
+this project. If you use this package within your own software as is but don't plan on modifying it, this guide is
+**not** for you.
+
+## Tools
+
+### Python (and friends)
+
+Not surprisingly, you will need to have Python installed on your system to work on this package. We support non-EOL,
+stable versions of CPython. The current supported versions are listed in the CI configurations (e.g. `.travis.yml`).
+At a minimum, you should have the latest version of Python 2 and the latest version of Python 3 to develop against.
+It's tricky to set up a system that has more than that, so you can lean on the CI servers to test changes on the
+in-between versions for you.
+
+You should also make sure you have the latest versions of `pip`, `setuptools`, `virtualenv`, `wheel`, `twine` and
+[`tox`](https://tox.readthedocs.io/en/latest/) installed with your version of Python.
+
+On macOS, the easiest way to install these tools is by using [Homebrew](https://brew.sh/) and installing the `python`
+and `python3` packages. Some of the above packages are preinstalled and you can install the remaining on your own:
+`pip install virtualenv wheel twine tox && pip3 install virtualenv twine tox`.
+
+## Tasks
+
+### Testing
+
+Tox is used to run the test suite across multiple isolated versions of Python. It is configured in `tox.ini` to
+run all the supported versions of Python, but when you invoke it, you should only select the versions you have on your
+system. For example, on a system with Python 2.7.13 and Python 3.6.1, you would run the tests using the following
+command: `tox -e flake8,py27,py36` (flake8 is a quality analysis tool).
+
+### Generating Documentation
+
+The documentation is generated from the source and templates in the `docs-src` directory. The generated documentation
+gets committed to the repo in `docs` and also published to a GitHub Pages website.
+
+You can generate the documentation by running `tox -e docs`.
+
+### Releasing
+
+1. Create the commit for the release:
+ * Bump the version number in adherence to [Semantic Versioning](http://semver.org/) in `slackclient/version.py`.
+ * Commit with a message including the new version number. For example `1.0.6`.
+
+2. Distribute the release
+ * Build the distribtuions: `python setup.py sdist bdist_wheel`. This will create artifacts in the `dist` directory.
+ * Publish to PyPI: `twine upload dist/*`. You must have access to the credentials to publish.
+ * Create a GitHub Release. You will select the commit with updated version number (e.g. `1.0.6`) to assiociate with
+ the tag, and name the tag after this version (e.g. `1.0.6`). This will also serve as a Changelog for the project.
+ Add a description of changes to the Release. Mention Issue and PR #'s and @-mention contributors.
+
+3. (Slack Internal) Communicate the release internally. Include a link to the GitHub Release.
+
+4. Announce on Slack Team dev4slack in #slack-api
+
+5. (Slack Internal) Tweet? Not necessary for patch updates, might be needed for minor updates, definitely needed for
+ major updates. Include a link to the GitHub Release.
+
+## Workflow
+
+### Versioning and Tags
+
+This project uses semantic versioning, expressed through the numbering scheme of
+[PEP-0440](https://www.python.org/dev/peps/pep-0440/).
+
+### Branches
+
+`master` is where active development occurs. Long running named feature branches are occasionally created for
+collaboration on a feature that has a large scope (because everyone cannot push commits to another person's open Pull
+Request). At some point in the future after a major version increment, there may be maintenance branches for older major
+versions.
+
+### Issue Management
+
+Labels are used to run issues through an organized workflow. Here are the basic definitions:
+
+* `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been
+ documented and the issue has been reproduced.
+* `enhancement`: A feature request for something this package might not already do.
+* `docs`: An issue that is purely about documentation work.
+* `tests`: An issue that is purely about testing work.
+* `needs feedback`: An issue that may have claimed to be a bug but was not reproducible, or was otherwise missing some information.
+* `discussion`: An issue that is purely meant to hold a discussion. Typically the maintainers are looking for feedback in this issues.
+* `question`: An issue that is like a support request because the user's usage was not correct.
+* `semver:major|minor|patch`: Metadata about how resolving this issue would affect the version number.
+* `security`: An issue that has special consideration for security reasons.
+* `good first contribution`: An issue that has a well-defined relatively-small scope, with clear expectations. It helps when the testing approach is also known.
+* `duplicate`: An issue that is functionally the same as another issue. Apply this only if you've linked the other issue by number.
+
+**Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information
+with labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`,
+`needs feedback`, `docs`, `tests`, or `discussion`.
+
+Issues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again,
+reopening is great and better than creating a duplicate issue.
+
+## Everything else
+
+When in doubt, find the other maintainers and ask.
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md
new file mode 100644
index 0000000000..ce8640a6bf
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.github/pull_request_template.md
@@ -0,0 +1,8 @@
+### Summary
+
+Describe the goal of this PR. Mention any related Issue numbers.
+
+### Requirements (place an `x` in each `[ ]`)
+
+* [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.
+* [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore
new file mode 100644
index 0000000000..7290c13d39
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.gitignore
@@ -0,0 +1,26 @@
+# general things to ignore
+build/
+dist/
+docs/_sources/
+docs/.doctrees
+*.egg-info/
+*.egg
+*.py[cod]
+__pycache__/
+*.so
+*~
+
+# virtualenv
+env/
+venv/
+
+# codecov / coverage
+.coverage
+cov_*
+
+# due to using tox and pytest
+.tox
+.cache
+.pytest_cache/
+.python-version
+pip
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml
new file mode 100644
index 0000000000..0ce6c9b039
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/.travis.yml
@@ -0,0 +1,17 @@
+sudo: false
+dist: trusty
+language: python
+python:
+ - "2.7"
+ - "3.4"
+ - "3.5"
+ - "3.6"
+install:
+ - travis_retry pip install -r requirements.txt
+ - travis_retry pip install flake8
+ - travis_retry pip install -r test_requirements.txt
+script:
+ - flake8 slackclient
+ - py.test --cov-report= --cov=slackclient tests
+after_success:
+ - codecov -e $TRAVIS_PYTHON_VERSION
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE
new file mode 100644
index 0000000000..73da6e9751
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2016 Slack Technologies, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in
new file mode 100644
index 0000000000..1aba38f67a
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst
new file mode 100644
index 0000000000..3c7c031f9a
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/README.rst
@@ -0,0 +1,355 @@
+python-slackclient
+===================
+
+A client for Slack, which supports the Slack Web API and Real Time Messaging (RTM) API.
+
+|build-status| |windows-build-status| |codecov| |doc-status| |pypi-version| |python-version|
+
+.. |build-status| image:: https://travis-ci.org/slackapi/python-slackclient.svg?branch=master
+ :target: https://travis-ci.org/slackapi/python-slackclient
+.. |windows-build-status| image:: https://ci.appveyor.com/api/projects/status/rif04t60ptslj32x/branch/master?svg=true
+ :target: https://ci.appveyor.com/project/slackapi/python-slackclient
+.. |codecov| image:: https://codecov.io/gh/slackapi/python-slackclient/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/slackapi/python-slackclient
+.. |doc-status| image:: https://readthedocs.org/projects/python-slackclient/badge/?version=latest
+ :target: http://python-slackclient.readthedocs.io/en/latest/?badge=latest
+.. |pypi-version| image:: https://badge.fury.io/py/slackclient.svg
+ :target: https://pypi.python.org/pypi/slackclient
+.. |python-version| image:: https://img.shields.io/pypi/pyversions/slackclient.svg
+ :target: https://pypi.python.org/pypi/slackclient
+
+Overview
+--------
+
+Whether you're building a custom app for your team, or integrating a third party
+service into your Slack workflows, Slack Developer Kit for Python allows you to leverage the flexibility
+of Python to get your project up and running as quickly as possible.
+
+Documentation
+***************
+
+For comprehensive method information and usage examples, see the `full documentation `_.
+
+If you're building a project to receive content and events from Slack, check out the `Python Slack Events API Adapter `_ library.
+
+You may also review our `Development Roadmap `_ in the project wiki.
+
+
+Requirements and Installation
+******************************
+
+We recommend using `PyPI `_ to install Slack Developer Kit for Python
+
+.. code-block:: bash
+
+ pip install slackclient
+
+Of course, if you prefer doing things the hard way, you can always implement Slack Developer Kit for Python
+by pulling down the source code directly into your project:
+
+.. code-block:: bash
+
+ git clone https://github.com/slackapi/python-slackclient.git
+ pip install -r requirements.txt
+
+Getting Help
+*************
+
+If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue:
+
+- Use our `Github Issue Tracker `_ for reporting bugs or requesting features.
+- Visit the `Bot Developer Hangout `_ for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers.
+
+Basic Usage
+------------
+The Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations
+we provide out of the box.
+
+This package is a modular wrapper designed to make Slack `Web API `_ calls simpler and easier for your
+app. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means
+a complete list. Review the full list of available methods `here `_.
+
+
+Sending a message
+********************
+The primary use of Slack is sending messages. Whether you're sending a message
+to a user or to a channel, this method handles both.
+
+To send a message to a channel, use the channel's ID. For IMs, use the user's ID.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:"
+ )
+
+There are some unique options specific to sending IMs, so be sure to read the **channels**
+section of the `chat.postMessage `_
+page for a full list of formatting and authorship options.
+
+Sending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same
+as sending a regular message, but with an additional ``user`` parameter.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postEphemeral",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ user="U0XXXXXXX"
+ )
+
+See `chat.postEphemeral `_ for more info.
+
+
+Replying to messages and creating threads
+*****************************************
+Threaded messages are just like regular messages, except thread replies are grouped together to provide greater context
+to the user. You can reply to a thread or start a new threaded conversation by simply passing the original message's ``ts``
+ID in the ``thread_ts`` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts`
+ID of the message you're replying to.
+
+A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps.
+When one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not
+appear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ thread_ts="1476746830.000003"
+ )
+
+
+By default, ``reply_broadcast`` is set to ``False``. To indicate your reply is germane to all members of a channel,
+set the ``reply_broadcast`` boolean parameter to ``True``.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ thread_ts="1476746830.000003",
+ reply_broadcast=True
+ )
+
+
+**Note:** While threaded messages may contain attachments and message buttons, when your reply is broadcast to the
+channel, it'll actually be a reference to your reply, not the reply itself.
+So, when appearing in the channel, it won't contain any attachments or message buttons. Also note that updates and
+deletion of threaded replies works the same as regular messages.
+
+See the `Threading messages together `_
+article for more information.
+
+
+Deleting a message
+********************
+Sometimes you need to delete things.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.delete",
+ channel="C0XXXXXX",
+ ts="1476745373.000002"
+ )
+
+See `chat.delete `_ for more info.
+
+Adding or removing an emoji reaction
+****************************************
+You can quickly respond to any message on Slack with an emoji reaction. Reactions
+can be used for any purpose: voting, checking off to-do items, showing excitement — and just for fun.
+
+This method adds a reaction (emoji) to an item (``file``, ``file comment``, ``channel message``, ``group message``, or ``direct message``). One of file, file_comment, or the combination of channel and timestamp must be specified.
+
+.. code-block:: python
+
+ import os
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "reactions.add",
+ channel="C0XXXXXXX",
+ name="thumbsup",
+ timestamp="1234567890.123456"
+ )
+
+Removing an emoji reaction is basically the same format, but you'll use ``reactions.remove`` instead of ``reactions.add``
+
+.. code-block:: python
+
+ sc.api_call(
+ "reactions.remove",
+ channel="C0XXXXXXX",
+ name="thumbsup",
+ timestamp="1234567890.123456"
+ )
+
+
+See `reactions.add `_ and `reactions.remove `_ for more info.
+
+Getting a list of channels
+******************************
+At some point, you'll want to find out what channels are available to your app. This is how you get that list.
+
+**Note:** This call requires the ``channels:read`` scope.
+
+.. code-block:: python
+
+ sc.api_call("channels.list")
+
+Archived channels are included by default. You can exclude them by passing ``exclude_archived=1`` to your request.
+
+.. code-block:: python
+
+ sc.api_call(
+ "channels.list",
+ exclude_archived=1
+ )
+
+See `channels.list `_ for more info.
+
+Getting a channel's info
+*************************
+Once you have the ID for a specific channel, you can fetch information about that channel.
+
+.. code-block:: python
+
+ sc.api_call(
+ "channels.info",
+ channel="C0XXXXXXX"
+ )
+
+See `channels.info `_ for more info.
+
+Joining a channel
+********************
+Channels are the social hub of most Slack teams. Here's how you hop into one:
+
+.. code-block:: python
+
+ sc.api_call(
+ "channels.join",
+ channel="C0XXXXXXY"
+ )
+
+If you are already in the channel, the response is slightly different.
+``already_in_channel`` will be true, and a limited ``channel`` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.
+
+See `channels.join `_ for more info.
+
+Leaving a channel
+********************
+Maybe you've finished up all the business you had in a channel, or maybe you
+joined one by accident. This is how you leave a channel.
+
+.. code-block:: python
+
+ sc.api_call(
+ "channels.leave",
+ channel="C0XXXXXXX"
+ )
+
+See `channels.leave `_ for more info.
+
+
+Tokens and Authentication
+**************************
+
+The simplest way to create an instance of the client, as shown in the samples above, is to use a bot (xoxb) access token:
+
+.. code-block:: python
+
+ # Get the access token from environmental variable
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+
+The SlackClient library allows you to use a variety of Slack authentication tokens.
+
+To take advantage of automatic token refresh, you'll need to instantiate the client a little differently than when using
+a bot access token. With a bot token, you have the access (xoxb) token when you create the client, when using refresh tokens,
+you won't know the access token when the client is created.
+
+Upon the first request, the SlackClient will request a new access (xoxa) token on behalf of your application, using your app's
+refresh token, client ID, and client secret.
+
+.. code-block:: python
+
+ # Get the access token from environmental variable
+ slack_refresh_token = os.environ["SLACK_REFRESH_TOKEN"]
+ slack_client_id = os.environ["SLACK_CLIENT_ID"]
+ slack_client_secret = os.environ["SLACK_CLIENT_SECRET"]
+
+
+Since your app's access tokens will be expiring and refreshed, the client requires a callback method to be passed in on creation of the client.
+Once Slack returns an access token for your app, the SlackClient will call your provided callback to update the access token in your datastore.
+
+.. code-block:: python
+
+ # This is where you'll add your data store update logic
+ def token_update_callback(update_data):
+ print("Enterprise ID: {}".format(update_data["enterprise_id"]))
+ print("Workspace ID: {}".format(update_data["team_id"]))
+ print("Access Token: {}".format(update_data["access_token"]))
+ print("Access Token expires in (ms): {}".format(update_data["expires_in"]))
+
+ # When creating an instance of the client, pass the client details and token update callback
+ sc = SlackClient(
+ refresh_token=slack_refresh_token,
+ client_id=slack_client_id,
+ client_secret=slack_client_secret,
+ token_update_callback=token_update_callback
+ )
+
+
+Slack will send your callback function the **app's access token**, **token expiration TTL**, **team ID**, and **enterprise ID** (for enterprise workspaces)
+
+
+See `Tokens & Authentication `_ for API token handling best practices.
+
+
+
+Additional Information
+********************************************************************************************
+For comprehensive method information and usage examples, see the `full documentation`_.
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore
new file mode 100644
index 0000000000..e35d8850c9
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/.gitignore
@@ -0,0 +1 @@
+_build
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile
new file mode 100644
index 0000000000..ecbc9e80a7
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/Makefile
@@ -0,0 +1,225 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = ../docs/
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help
+help:
+ @echo "Please use \`make ' where is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " applehelp to make an Apple Help Book"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " epub3 to make an epub3"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+ @echo " coverage to run coverage check of the documentation (if enabled)"
+ @echo " dummy to check syntax errors of document sources"
+
+.PHONY: clean
+clean:
+ rm -rf $(BUILDDIR)/*
+
+.PHONY: html
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+.PHONY: dirhtml
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+.PHONY: singlehtml
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+.PHONY: pickle
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+.PHONY: json
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+.PHONY: htmlhelp
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+.PHONY: qthelp
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-slackclient.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-slackclient.qhc"
+
+.PHONY: applehelp
+applehelp:
+ $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
+ @echo
+ @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
+ @echo "N.B. You won't be able to view it unless you put it in" \
+ "~/Library/Documentation/Help or install it in your application" \
+ "bundle."
+
+.PHONY: devhelp
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/python-slackclient"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-slackclient"
+ @echo "# devhelp"
+
+.PHONY: epub
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+.PHONY: epub3
+epub3:
+ $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
+ @echo
+ @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
+
+.PHONY: latex
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+.PHONY: latexpdf
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: latexpdfja
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+.PHONY: text
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+.PHONY: man
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+.PHONY: texinfo
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+.PHONY: info
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+.PHONY: gettext
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+.PHONY: changes
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+.PHONY: linkcheck
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+.PHONY: doctest
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+.PHONY: coverage
+coverage:
+ $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
+ @echo "Testing of coverage in the sources finished, look at the " \
+ "results in $(BUILDDIR)/coverage/python.txt."
+
+.PHONY: xml
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+.PHONY: pseudoxml
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
+
+.PHONY: dummy
+dummy:
+ $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
+ @echo
+ @echo "Build finished. Dummy builder generates no files."
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py
new file mode 100644
index 0000000000..bc81579bbe
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/conf.py
@@ -0,0 +1,342 @@
+# -*- coding: utf-8 -*-
+#
+# python-slackclient documentation build configuration file, created by
+# sphinx-quickstart on Mon Jun 27 17:36:09 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.coverage',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['../../_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Slack Developer Kit for Python'
+copyright = u'2015–2016 Slack Technologies, Inc. and contributors'
+author = u'Slack Technologies, Inc. and contributors'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'1.0'
+# The full version, including alpha/beta/rc tags.
+release = u'1.0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'emacs'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "slack"
+html_theme_path = ["../../_themes", ]
+
+highlight_language = "python"
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# " v documentation" by default.
+#
+# html_title = u'python-slackclient v1.0.1'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['static']
+
+html_context = {
+ 'css_files': ['static/pygments.css'],
+}
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'python-slackclientdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'python-slackclient.tex', u'python-slackclient Documentation',
+ u'Ryan Huber, Jeff Ammons', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'python-slackclient', u'python-slackclient Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'python-slackclient', u'python-slackclient Documentation',
+ author, 'python-slackclient', 'A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html
new file mode 100644
index 0000000000..e0e77d1eb5
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/layout.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+ {{ metatags }}
+
+ {%- block htmltitle %}
+ {{ title|striptags|e + " — "|safe + project|e }}
+ {%- endblock %}
+
+ {%- macro css() %}
+
+
+
+
+
+ {%- endmacro %}
+
+
+
+
+ {{ css() }}
+ {%- block linktags %}
+
+
+ {%- endblock %}
+
+
+
+
+
+
+
+
+
+
+ {%- block header %}
+
+
+
+
+
+
+
+
+
+ {{ project }}
+
+
+{%- endif %}
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html
new file mode 100644
index 0000000000..d92ca908f7
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/sidebar.html
@@ -0,0 +1,15 @@
+{{ toctree(maxdepth=-1, collapse=False,includehidden=True) }}
+
+
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t
new file mode 100644
index 0000000000..93fc2c66b5
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/default.css_t
@@ -0,0 +1,74 @@
+a.headerlink {
+ display: none !important;
+}
+
+.section-title {
+ font-size: 2rem;
+ line-height: 2.5rem;
+ letter-spacing: -1px;
+ font-weight: 700;
+ margin: 0 0 1rem;
+}
+
+nav#api_nav .toctree-l1 {
+ margin-bottom: 1.5rem;
+}
+
+nav#api_nav #api_sections ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l1>a {
+ color: #1264a3;
+ letter-spacing: 0;
+ font-size: .8rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ border: none;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 {
+ margin: 0;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 a {
+ color: #1d1c1d;
+ text-transform: none;
+ font-weight: inherit;
+ padding: 0;
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 15px!important;
+ line-height:15px;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 a:hover {
+ cursor: pointer;
+ text-decoration: none;
+ background-color:#e8f5fa;
+ border-color:#dcf0fb;
+}
+
+nav#api_nav #footer #footer_nav {
+ font-size: .9375rem;
+}
+
+nav#api_nav #footer #footer_nav a {
+ border: none;
+ padding: 0;
+ color: #616061;
+}
+
+nav#api_nav #footer #footer_nav a:hover {
+ text-decoration: none;
+ color: #1c1c1c;
+}
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t
new file mode 100644
index 0000000000..7f360ac666
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/docs.css_t
@@ -0,0 +1,34 @@
+/* Updates body font */
+body {
+ font-family: Slack-Lato,appleLogo,sans-serif;
+}
+
+/* Replaces old sidebar styled links */
+.sidebar_menu h5 {
+ font-size: 0.8rem;
+ font-weight: 800;
+ margin-bottom: 3px;
+}
+
+/* Aligns footer navigation to the left of the sidebar */
+.footer_nav {
+ margin: 0 !important;
+}
+
+/* Styles the signature all nice and pretty <3 */
+#footer_signature {
+ color:#e01e5a;
+ font-size:.9rem;
+ margin-top: 10px;
+}
+
+/* Fixes link hover state */
+a:hover {
+ text-decoration: underline;
+}
+
+/* Makes footer consistent */
+footer {
+ background-color: transparent;
+ border: 0;
+}
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t
new file mode 100644
index 0000000000..a94b170ffb
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/static/pygments.css_t
@@ -0,0 +1,60 @@
+.highlight .hll { background-color: #ffffcc }
+.highlight { background: #ffffff; }
+.highlight .c { color: #999988; font-style: italic } /* Comment */
+.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
+.highlight .k { font-weight: bold } /* Keyword */
+.highlight .o { font-weight: bold } /* Operator */
+.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */
+.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */
+.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
+.highlight .ge { font-style: italic } /* Generic.Emph */
+.highlight .gr { color: #aa0000 } /* Generic.Error */
+.highlight .gh { color: #999999 } /* Generic.Heading */
+.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
+.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gp { color: #555555 } /* Generic.Prompt */
+.highlight .gs { font-weight: bold } /* Generic.Strong */
+.highlight .gu { color: #aaaaaa } /* Generic.Subheading */
+.highlight .gt { color: #aa0000 } /* Generic.Traceback */
+.highlight .kc { font-weight: bold } /* Keyword.Constant */
+.highlight .kd { font-weight: bold } /* Keyword.Declaration */
+.highlight .kn { font-weight: bold } /* Keyword.Namespace */
+.highlight .kp { font-weight: bold } /* Keyword.Pseudo */
+.highlight .kr { font-weight: bold } /* Keyword.Reserved */
+.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */
+.highlight .m { color: #009999 } /* Literal.Number */
+.highlight .s { color: #bb8844 } /* Literal.String */
+.highlight .na { color: #008080 } /* Name.Attribute */
+.highlight .nb { color: #999999 } /* Name.Builtin */
+.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */
+.highlight .no { color: #008080 } /* Name.Constant */
+.highlight .ni { color: #800080 } /* Name.Entity */
+.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */
+.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */
+.highlight .nn { color: #555555 } /* Name.Namespace */
+.highlight .nt { color: #000080 } /* Name.Tag */
+.highlight .nv { color: #008080 } /* Name.Variable */
+.highlight .ow { font-weight: bold } /* Operator.Word */
+.highlight .w { color: #bbbbbb } /* Text.Whitespace */
+.highlight .mf { color: #009999 } /* Literal.Number.Float */
+.highlight .mh { color: #009999 } /* Literal.Number.Hex */
+.highlight .mi { color: #009999 } /* Literal.Number.Integer */
+.highlight .mo { color: #009999 } /* Literal.Number.Oct */
+.highlight .sb { color: #bb8844 } /* Literal.String.Backtick */
+.highlight .sc { color: #bb8844 } /* Literal.String.Char */
+.highlight .sd { color: #bb8844 } /* Literal.String.Doc */
+.highlight .s2 { color: #bb8844 } /* Literal.String.Double */
+.highlight .se { color: #bb8844 } /* Literal.String.Escape */
+.highlight .sh { color: #bb8844 } /* Literal.String.Heredoc */
+.highlight .si { color: #bb8844 } /* Literal.String.Interpol */
+.highlight .sx { color: #bb8844 } /* Literal.String.Other */
+.highlight .sr { color: #808000 } /* Literal.String.Regex */
+.highlight .s1 { color: #bb8844 } /* Literal.String.Single */
+.highlight .ss { color: #bb8844 } /* Literal.String.Symbol */
+.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */
+.highlight .vc { color: #008080 } /* Name.Variable.Class */
+.highlight .vg { color: #008080 } /* Name.Variable.Global */
+.highlight .vi { color: #008080 } /* Name.Variable.Instance */
+.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf
new file mode 100644
index 0000000000..b13de3cbb4
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/_themes/slack/theme.conf
@@ -0,0 +1,6 @@
+[theme]
+inherit = default
+stylesheet = default.css
+show_sphinx = False
+
+[options]
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst
new file mode 100644
index 0000000000..17fad157f5
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/about.rst
@@ -0,0 +1,18 @@
+==============================================
+About
+==============================================
+
+|product_name|
+**************
+
+
+Access the Slack Platform from your Python app. |product_name| lets you build on the Slack Web APIs pythonically.
+
+|product_name| is proudly maintained with 💖 by the Slack Developer Tools team
+
+- `License`_
+- `Code of Conduct`_
+- `Contributing`_
+- `Contributor License Agreement`_
+
+.. include:: metadata.rst
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst
new file mode 100644
index 0000000000..9a587c3f21
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/auth.rst
@@ -0,0 +1,150 @@
+==============================================
+Tokens & Authentication
+==============================================
+.. _handling-tokens:
+
+Handling tokens and other sensitive data
+----------------------------------------
+⚠️ **Slack tokens are the keys to your—or your customers’—data.Keep them secret. Keep them safe.**
+
+One way to do that is to never explicitly hardcode them.
+
+Try to avoid this when possible:
+
+.. code-block:: python
+
+ token = 'xoxb-abc-1232'
+
+⚠️ **Never share test tokens with other users or applications. Do not publish test tokens in public code repositories.**
+
+We recommend you pass tokens in as environment variables, or persist them in a database that is accessed at runtime. You can add a token to the environment by starting your app as:
+
+.. code-block:: python
+
+ SLACK_BOT_TOKEN="xoxb-abc-1232" python myapp.py
+
+Then retrieve the key with:
+
+.. code-block:: python
+
+ import os
+ SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
+
+You can use the same technique for other kinds of sensitive data that ne’er-do-wells could use in nefarious ways, including
+
+- Incoming webhook URLs
+- Slash command verification tokens
+- App client ids and client secrets
+
+For additional information, please see our `Safely Storing Credentials `_ page.
+
+Single-Workspace Apps
+-----------------------
+If you're building an application for a single Slack workspace, there's no need to build out the entire OAuth flow.
+
+Once you've setup your features, click on the **Install App to Team** button found on the **Install App** page.
+If you add new permission scopes or Slack app features after an app has been installed, you must reinstall the app to
+your workspace for changes to take effect.
+
+For additional information, see the `Installing Apps `_ of our `Building Slack apps `_ page.
+
+The OAuth flow
+-------------------------
+Authentication for Slack's APIs is done using OAuth, so you'll want to read up on `OAuth `_.
+
+In order to implement OAuth in your app, you will need to include a web server. In this example, we'll use `Flask `_.
+
+As mentioned above, we're setting the app tokens and other configs in environment variables and pulling them into global variables.
+
+Depending on what actions your app will need to perform, you'll need different OAuth permission scopes. Review the available scopes `here `_.
+
+.. code-block:: python
+
+ import os
+ from flask import Flask, request
+ from slackclient import SlackClient
+
+ client_id = os.environ["SLACK_CLIENT_ID"]
+ client_secret = os.environ["SLACK_CLIENT_SECRET"]
+ oauth_scope = os.environ["SLACK_BOT_SCOPE"]
+
+ app = Flask(__name__)
+
+**The OAuth initiation link:**
+
+To begin the OAuth flow, you'll need to provide the user with a link to Slack's OAuth page.
+This directs the user to Slack's OAuth acceptance page, where the user will review and accept
+or refuse the permissions your app is requesting as defined by the requested scope(s).
+
+For the best user experience, use the `Add to Slack button `_ to direct users to approve your application for access or `Sign in with Slack `_ to log users in.
+
+.. code-block:: python
+
+ @app.route("/begin_auth", methods=["GET"])
+ def pre_install():
+ return '''
+
+ Add to Slack
+
+ '''.format(oauth_scope, client_id)
+
+**The OAuth completion page**
+
+Once the user has agreed to the permissions you've requested on Slack's OAuth
+page, Slack will redirect the user to your auth completion page. Included in this
+redirect is a ``code`` query string param which you’ll use to request access
+tokens from the ``oauth.access`` endpoint.
+
+Generally, Web API requests require a valid OAuth token, but there are a few endpoints
+which do not require a token. ``oauth.access`` is one example of this. Since this
+is the endpoint you'll use to retrieve the tokens for later API requests,
+an empty string ``""`` is acceptable for this request.
+
+.. code-block:: python
+
+ @app.route("/finish_auth", methods=["GET", "POST"])
+ def post_install():
+
+ # Retrieve the auth code from the request params
+ auth_code = request.args['code']
+
+ # An empty string is a valid token for this request
+ sc = SlackClient("")
+
+ # Request the auth tokens from Slack
+ auth_response = sc.api_call(
+ "oauth.access",
+ client_id=client_id,
+ client_secret=client_secret,
+ code=auth_code
+ )
+
+A successful request to ``oauth.access`` will yield two tokens: A ``user``
+token and a ``bot`` token. The ``user`` token ``auth_response['access_token']``
+is used to make requests on behalf of the authorizing user and the ``bot``
+token ``auth_response['bot']['bot_access_token']`` is for making requests
+on behalf of your app's bot user.
+
+If your Slack app includes a bot user, upon approval the JSON response will contain
+an additional node containing an access token to be specifically used for your bot
+user, within the context of the approving team.
+
+When you use Web API methods on behalf of your bot user, you should use this bot
+user access token instead of the top-level access token granted to your application.
+
+.. code-block:: python
+
+ # Save the bot token to an environmental variable or to your data store
+ # for later use
+ os.environ["SLACK_USER_TOKEN"] = auth_response['access_token']
+ os.environ["SLACK_BOT_TOKEN"] = auth_response['bot']['bot_access_token']
+
+ # Don't forget to let the user know that auth has succeeded!
+ return "Auth complete!"
+
+Once your user has completed the OAuth flow, you'll be able to use the provided
+tokens to make a variety of Web API calls on behalf of the user and your app's bot user.
+
+See the :ref:`Web API usage ` section of this documentation for usage examples.
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst
new file mode 100644
index 0000000000..e61365c8a4
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/basic_usage.rst
@@ -0,0 +1,435 @@
+.. _web-api-examples:
+
+==============================================
+Basic Usage
+==============================================
+The Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations
+we provide out of the box.
+
+This package is a modular wrapper designed to make Slack `Web API`_ calls simpler and easier for your
+app. Provided below are examples of how to interact with commonly used API endpoints, but this is by no means
+a complete list. Review the full list of available methods `here `_.
+
+See :ref:`Tokens & Authentication ` for API token handling best practices.
+
+--------
+
+Sending a message
+-----------------------
+The primary use of Slack is sending messages. Whether you're sending a message
+to a user or to a channel, this method handles both.
+
+To send a message to a channel, use the channel's ID. For IMs, use the user's ID.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:"
+ )
+
+There are some unique options specific to sending IMs, so be sure to read the **channels**
+section of the `chat.postMessage `_
+page for a full list of formatting and authorship options.
+
+Sending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same
+as sending a regular message, but with an additional ``user`` parameter.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postEphemeral",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ user="U0XXXXXXX"
+ )
+
+See `chat.postEphemeral `_ for more info.
+
+--------
+
+Customizing a message's layout
+-----------------------
+The chat.postMessage method takes an optional blocks argument that allows you to customize the layout of a message.
+Blocks for Web API methods are all specified in a single object literal, so just add additional keys for any optional argument.
+
+To send a message to a channel, use the channel's ID. For IMs, use the user's ID.
+
+.. code-block:: python
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ blocks=[
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "Danny Torrence left the following review for your property:"
+ }
+ },
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": " \n :star: \n Doors had too many axe holes, guest in room " +
+ "237 was far too rowdy, whole place felt stuck in the 1920s."
+ },
+ "accessory": {
+ "type": "image",
+ "image_url": "https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg",
+ "alt_text": "Haunted hotel image"
+ }
+ },
+ {
+ "type": "section",
+ "fields": [
+ {
+ "type": "mrkdwn",
+ "text": "*Average Rating*\n1.0"
+ }
+ ]
+ }
+ ]
+ )
+
+**Note:** You can use the `Block Kit Builder `for a playground where you can prototype your message's look and feel.
+
+--------
+
+Replying to messages and creating threads
+------------------------------------------
+Threaded messages are just like regular messages, except thread replies are grouped together to provide greater context
+to the user. You can reply to a thread or start a new threaded conversation by simply passing the original message's ``ts``
+ID in the ``thread_ts`` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts`
+ID of the message you're replying to.
+
+A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps.
+When one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not
+appear directly in the channel, instead relegated to a kind of forked timeline descending from the parent message.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ thread_ts="1476746830.000003"
+ )
+
+
+By default, ``reply_broadcast`` is set to ``False``. To indicate your reply is germane to all members of a channel,
+set the ``reply_broadcast`` boolean parameter to ``True``.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.postMessage",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:",
+ thread_ts="1476746830.000003",
+ reply_broadcast=True
+ )
+
+
+**Note:** While threaded messages may contain attachments and message buttons, when your reply is broadcast to the
+channel, it'll actually be a reference to your reply, not the reply itself.
+So, when appearing in the channel, it won't contain any attachments or message buttons. Also note that updates and
+deletion of threaded replies works the same as regular messages.
+
+See the `Threading messages together `_
+article for more information.
+
+
+--------
+
+Updating the content of a message
+----------------------------------
+Let's say you have a bot which posts the status of a request. When that request
+is updated, you'll want to update the message to reflect it's state. Or your user
+might want to fix a typo or change some wording. This is how you'll make those changes.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.update",
+ ts="1476746830.000003",
+ channel="C0XXXXXX",
+ text="Hello from Python! :tada:"
+ )
+
+See `chat.update `_ for formatting options
+and some special considerations when calling this with a bot user.
+
+--------
+
+Deleting a message
+-------------------
+Sometimes you need to delete things.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "chat.delete",
+ channel="C0XXXXXX",
+ ts="1476745373.000002"
+ )
+
+See `chat.delete `_ for more info.
+
+--------
+
+Adding or removing an emoji reaction
+---------------------------------------
+You can quickly respond to any message on Slack with an emoji reaction. Reactions
+can be used for any purpose: voting, checking off to-do items, showing excitement — and just for fun.
+
+This method adds a reaction (emoji) to an item (``file``, ``file comment``, ``channel message``, ``group message``, or ``direct message``). One of file, file_comment, or the combination of channel and timestamp must be specified.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "reactions.add",
+ channel="C0XXXXXXX",
+ name="thumbsup",
+ timestamp="1234567890.123456"
+ )
+
+Removing an emoji reaction is basically the same format, but you'll use ``reactions.remove`` instead of ``reactions.add``
+
+.. code-block:: python
+
+ sc.api_call(
+ "reactions.remove",
+ channel="C0XXXXXXX",
+ name="thumbsup",
+ timestamp="1234567890.123456"
+ )
+
+
+See `reactions.add `_ and `reactions.remove `_ for more info.
+
+--------
+
+Getting a list of channels
+---------------------------
+At some point, you'll want to find out what channels are available to your app. This is how you get that list.
+
+**Note:** This call requires the ``channels:read`` scope.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call("channels.list")
+
+Archived channels are included by default. You can exclude them by passing ``exclude_archived=1`` to your request.
+
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "channels.list",
+ exclude_archived=1
+ )
+
+See `channels.list `_ for more info.
+
+--------
+
+Getting a channel's info
+-------------------------
+Once you have the ID for a specific channel, you can fetch information about that channel.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "channels.info",
+ channel="C0XXXXXXX"
+ )
+
+See `channels.info `_ for more info.
+
+--------
+
+Joining a channel
+------------------
+Channels are the social hub of most Slack teams. Here's how you hop into one:
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "channels.join",
+ channel="C0XXXXXXY"
+ )
+
+If you are already in the channel, the response is slightly different.
+``already_in_channel`` will be true, and a limited ``channel`` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.
+
+See `channels.join `_ for more info.
+
+--------
+
+Leaving a channel
+------------------
+Maybe you've finished up all the business you had in a channel, or maybe you
+joined one by accident. This is how you leave a channel.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "channels.leave",
+ channel="C0XXXXXXX"
+ )
+
+See `channels.leave `_ for more info.
+
+--------
+
+Get a list of team members
+------------------------------
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call("users.list")
+
+See `users.list `_ for more info.
+
+
+--------
+
+Uploading files
+------------------------------
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ with open('thinking_very_much.png') as file_content:
+ sc.api_call(
+ "files.upload",
+ channels="C3UKJTQAC",
+ file=file_content,
+ title="Test upload"
+ )
+
+See `files.upload `_ for more info.
+
+
+--------
+
+Web API Rate Limits
+--------------------
+Slack allows applications to send no more than one message per second. We allow bursts over that
+limit for short periods. However, if your app continues to exceed the limit over a longer period
+of time it will be rate limited.
+
+Here's a very basic example of how one might deal with rate limited requests.
+
+If you go over these limits, Slack will start returning a HTTP 429 Too Many Requests error,
+a JSON object containing the number of calls you have been making, and a Retry-After header
+containing the number of seconds until you can retry.
+
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+ import time
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ # Simple wrapper for sending a Slack message
+ def send_slack_message(channel, message):
+ return sc.api_call(
+ "chat.postMessage",
+ channel=channel,
+ text=message
+ )
+
+ # Make the API call and save results to `response`
+ response = send_slack_message("C0XXXXXX", "Hello, from Python!")
+
+ # Check to see if the message sent successfully.
+ # If the message succeeded, `response["ok"]`` will be `True`
+ if response["ok"]:
+ print("Message posted successfully: " + response["message"]["ts"])
+ # If the message failed, check for rate limit headers in the response
+ elif response["ok"] is False and response["headers"]["Retry-After"]:
+ # The `Retry-After` header will tell you how long to wait before retrying
+ delay = int(response["headers"]["Retry-After"])
+ print("Rate limited. Retrying in " + str(delay) + " seconds")
+ time.sleep(delay)
+ send_slack_message(message, channel)
+
+See the documentation on `Rate Limiting `_ for more info.
+
+.. include:: metadata.rst
+
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst
new file mode 100644
index 0000000000..74300a43a0
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/changelog.rst
@@ -0,0 +1,156 @@
+==============================================
+Changelog
+==============================================
+
+v1.3.2 (2019-05-30)
+-------------------
+- Fixing an issue where reconnects used rtm.start istead of rtm.connect. #422
+
+
+v1.3.1 (2019-02-28)
+-------------------
+
+- Lock websocket-client version to < 0.55.0: temp fix for #385
+
+
+v1.3.0 (2018-09-11)
+-------------------
+
+## New Features
+- Adds support for short lived tokens and automatic token refresh #347 (Thanks @roach!)
+
+## Other
+- update RTM rate limiting comment and error message #308 (Thanks @benoitlavigne!)
+- Use logging instead of traceback #309 (Thanks @harlowja!)
+- Remove Python 3.3 from test environments #346 (Thanks @roach!)
+- Enforced linting when using VSCode. #347 (Thanks @roach!)
+
+
+v1.2.1 (2018-03-26)
+-------------------
+
+- Added rate limit handling for rtm connections (thanks @jayalane!)
+
+
+v1.2.0 (2018-03-20)
+-------------------
+
+- You can now tell the RTM client to automatically reconnect by passing `auto_reconnect=True`
+
+v1.1.3 (2018-03-01)
+-------------------
+
+- Fixed another API param encoding bug. It encodes things properly now.
+
+v1.1.2 (2018-01-31)
+-------------------
+
+- Fixed an encoding issue which was encoding some Web API params incorrectly (sorry)
+
+v1.1.1 (2018-01-30)
+-------------------
+
+ - Adds HTTP response headers to `api_call` responses to expose things like rate limit info
+ - Moves `token` into auth header rather than request params
+
+v1.1.0 (2017-11-21)
+-------------------
+
+ - Aadds new SlackClientError and ResponseParseError types to describe errors - thanks @aoberoi!
+ - Fix Build Error (#245) - thanks @stasfilin!
+ - include email as user property (#173) - thanks @acaire!
+ - Add http reply into slack login and slack connection error (#216) - thanks @harlowja!
+ - Removed unused exception class (#233)
+ - Fix rtm_send_message bug (#225) - thanks @kt5356!
+ - Allow use of custom parameters on rtm_connect() (#210) - thanks @kamushadenes!
+ - Fix link to rtm.connect docs (#223) - @sampart!
+
+v1.0.9 (2017-08-31)
+-------------------
+
+ - Fixed rtm_send_message ID bug introduced in 1.0.8
+
+v1.0.8 (2017-08-31)
+-------------------
+
+ - Added rtm.connect support
+
+v1.0.7 (2017-08-02)
+-------------------
+
+ - Fixes an issue where connecting over RTM to large teams may result in “Websocket URL expired” errors
+ - A load of packaging improvements
+
+v1.0.6 (2017-06-12)
+-------------------
+
+ - Added proxy support (thanks @timfeirg!)
+ - Tidied up docs (thanks @schlueter!)
+ - Added tox settings for Python 3 testing (thanks @cclauss!)
+
+v1.0.5 (2017-01-23)
+-------------------
+
+ - Allow RTM Channel.send_message to reply to a thread
+ - Index users by ID instead of Name (non-breaking change)
+ - Added timeout to api calls.
+ - Fixed a typo about token access in auth.rst, thanks @kelvintaywl!
+ - Added Message Threads to the docs
+
+v1.0.4 (2016-12-15)
+-------------------
+
+ - fixed the ability to search for a user by ID
+
+v1.0.3 (2016-12-13)
+-------------------
+
+ - fixed an issue causing RTM connections to fail for large teams
+
+v1.0.2 (2016-09-22)
+-------------------
+
+ - removed unused ping counter
+ - fixed contributor guidelines links
+ - updated documentation
+ - Fix bug preventing API calls requiring a file ID
+ - Removes files from api_calls before JSON encoding, so the request is properly formatted
+
+v1.0.1 (2016-03-25)
+-------------------
+
+ - fix for __eq__ comparison in channels using '#' in channel name
+ - added copyright info to the LICENSE file
+
+v1.0.0 (2016-02-28)
+-------------------
+
+ - the ``api_call`` function now returns a decoded JSON object, rather than a JSON encoded string
+ - some ``api_call`` calls now call actions on the parent server object:
+ - ``im.open``
+ - ``mpim.open``, ``groups.create``, ``groups.createChild``
+ - ``channels.create``, `channels.join``
+
+v0.18.0 (2016-02-21)
+--------------------
+
+ - Moves to use semver for versioning
+ - Adds support for private groups and MPDMs
+ - Switches to use requests instead of urllib
+ - Gets Travis CI integration working
+ - Fixes some formatting issues so the code will work for python 2.6
+ - Cleans up some unused imports, some PEP-8 fixes and a couple bad default args fixes
+
+v0.17.0 (2016-02-15)
+--------------------
+
+ - Fixes the server so that it doesn't add duplicate users or channels to its internal lists, https://github.com/slackapi/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa
+ - README updates:
+ - Updates the URLs pointing to Slack docs for configuring authentication, https://github.com/slackapi/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9
+ - s/channnels/channels, https://github.com/slackapi/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb
+ - Adds users to the local cache when they join the team, https://github.com/slackapi/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23
+ - Fixes urllib py 2/3 compatibility, https://github.com/slackapi/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886
+
+
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py
new file mode 100644
index 0000000000..37a4011949
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conf.py
@@ -0,0 +1,338 @@
+# -*- coding: utf-8 -*-
+#
+# python-slackclient documentation build configuration file, created by
+# sphinx-quickstart on Mon Jun 27 17:36:09 2016.
+#
+# This file is execfile()d with the current directory set to its
+# containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+import os
+import sys
+sys.path.insert(0, os.path.abspath('../'))
+
+# -- General configuration ------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.coverage',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The encoding of source files.
+#
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'python-slackclient'
+copyright = u'2016, Ryan Huber, Jeff Ammons'
+author = u'Ryan Huber, Jeff Ammons'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = u'1.0'
+# The full version, including alpha/beta/rc tags.
+release = u'1.0.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#
+# today = ''
+#
+# Else, today_fmt is used as the format for a strftime call.
+#
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The reST default role (used for this markup: `text`) to use for all
+# documents.
+#
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# If true, keep warnings as "system message" paragraphs in the built documents.
+# keep_warnings = False
+
+# If true, `todo` and `todoList` produce output, else they produce nothing.
+todo_include_todos = False
+
+
+# -- Options for HTML output ----------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+import sphinx_rtd_theme # noqa
+html_theme = "sphinx_rtd_theme"
+
+html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents.
+# " v documentation" by default.
+#
+# html_title = u'python-slackclient v1.0.1'
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+#
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+#
+# html_logo = None
+
+# The name of an image file (relative to this directory) to use as a favicon of
+# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['_static']
+
+# Add any extra paths that contain custom files (such as robots.txt or
+# .htaccess) here, relative to this directory. These files are copied
+# directly to the root of the documentation.
+#
+# html_extra_path = []
+
+# If not None, a 'Last updated on:' timestamp is inserted at every page
+# bottom, using the given strftime format.
+# The empty string is equivalent to '%b %d, %Y'.
+#
+# html_last_updated_fmt = None
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+#
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+#
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+#
+# html_domain_indices = True
+
+# If false, no index is generated.
+#
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+#
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Language to be used for generating the HTML full-text search index.
+# Sphinx supports the following languages:
+# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
+# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
+#
+# html_search_language = 'en'
+
+# A dictionary with options for the search language support, empty by default.
+# 'ja' uses this config value.
+# 'zh' user can custom change `jieba` dictionary path.
+#
+# html_search_options = {'type': 'default'}
+
+# The name of a javascript file (relative to the configuration directory) that
+# implements a search results scorer. If empty, the default will be used.
+#
+# html_search_scorer = 'scorer.js'
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'python-slackclientdoc'
+
+# -- Options for LaTeX output ---------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ #
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ #
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ #
+ # 'preamble': '',
+
+ # Latex figure (float) alignment
+ #
+ # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+# author, documentclass [howto, manual, or own class]).
+latex_documents = [
+ (master_doc, 'python-slackclient.tex', u'python-slackclient Documentation',
+ u'Ryan Huber, Jeff Ammons', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+#
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#
+# latex_appendices = []
+
+# If false, no module index is generated.
+#
+# latex_domain_indices = True
+
+
+# -- Options for manual page output ---------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ (master_doc, 'python-slackclient', u'python-slackclient Documentation',
+ [author], 1)
+]
+
+# If true, show URL addresses after external links.
+#
+# man_show_urls = False
+
+
+# -- Options for Texinfo output -------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ (master_doc, 'python-slackclient', u'python-slackclient Documentation',
+ author, 'python-slackclient', 'A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+#
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#
+# texinfo_show_urls = 'footnote'
+
+# If true, do not generate a @detailmenu in the "Top" node's menu.
+#
+# texinfo_no_detailmenu = False
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst
new file mode 100644
index 0000000000..77b0a25bb1
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/conversations.rst
@@ -0,0 +1,166 @@
+.. _conversations_api:
+
+==============================================
+Conversations API
+==============================================
+The Slack Conversations API provides your app with a unified interface to work with all the
+channel-like things encountered in Slack; public channels, private channels, direct messages, group
+direct messages, and our newest channel type, Shared Channels.
+
+
+See `Conversations API `_ docs for more info.
+
+--------
+
+Creating a direct message or multi-person direct message
+---------------------------------------------------------
+This Conversations API method opens a multi-person direct message or just a 1:1 direct message.
+
+*Use conversations.create for public or private channels.*
+
+Provide 1 to 8 user IDs in the ``user`` parameter to open or resume a conversation. Providing only
+1 ID will create a direct message. Providing more will create an ``mpim``.
+
+If there are no conversations already in progress including that exact set of members, a new
+multi-person direct message conversation begins.
+
+Subsequent calls to ``conversations.open`` with the same set of users will return the already
+existing conversation.
+
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "conversations.open",
+ users=["W1234567890","U2345678901","U3456789012"]
+ )
+
+See `conversations.open `_ additional info.
+
+--------
+
+Creating a public or private channel
+-------------------------------------
+Initiates a public or private channel-based conversation
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "conversations.create",
+ name="myprivatechannel",
+ is_private=True
+ )
+
+See `conversations.create `_ additional info.
+
+--------
+
+Getting information about a conversation
+-----------------------------------------
+This Conversations API method returns information about a workspace conversation.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "conversations.info",
+ channel="C0XXXXXX",
+ )
+
+See `conversations.info `_ for more info.
+
+
+--------
+
+Getting a list of conversations
+--------------------------------
+This Conversations API method returns a list of all channel-like conversations in a workspace.
+The "channels" returned depend on what the calling token has access to and the directives placed
+in the ``types`` parameter.
+
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call("conversations.list")
+
+Only public conversations are included by default. You may include additional conversations types
+by passing ``types`` (as a string) into your list request. Additional conversation types include
+``public_channel`` and ``private_channel``.
+
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ # Note that `types` is a string
+ sc.api_call(
+ "conversations.list",
+ types="public_channel, private_channel"
+ )
+
+See `conversations.list `_ for more info.
+
+
+--------
+
+Leaving a conversation
+-----------------------
+Maybe you've finished up all the business you had in a conversation, or maybe you
+joined one by accident. This is how you leave a conversation.
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call(
+ "conversations.leave",
+ channel="C0XXXXXXX"
+ )
+
+See `conversations.leave `_ for more info.
+
+--------
+
+Get conversation members
+------------------------------
+Get a list fo the members of a conversation
+
+.. code-block:: python
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.api_call("conversations.members",
+ channel="C0XXXXXXX"
+ )
+
+See `users.list `_ for more info.
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst
new file mode 100644
index 0000000000..79621351c7
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/faq.rst
@@ -0,0 +1,63 @@
+==============================================
+Frequently Asked Questions
+==============================================
+
+What even is |product_name| and why should I care?
+**************************************************
+
+|product_name| is a wrapper around commonly accessed parts of the Slack Platform. It provides basic mechanisms for
+using the Slack Web API from within your Python app.
+
+On the other hand, |product_name| does not provide access to the Events bot-building API, but
+[this adapter](https://github.com/slackapi/python-slack-events-api) does.
+
+OMG I found a bug!
+******************
+
+Well, poop. Take a deep breath, and then let us know on the `Issue Tracker`_. If you're feeling particularly ambitious,
+why not submit a `pull request`_ with a bug fix?
+
+Hey, there's a feature missing!
+*******************************
+
+There's always something more that could be added! You can let us know in the `Issue Tracker`_ to start a discussion
+around the proposed feature, that's a good start. If you're feeling particularly ambitious, why not write the feature
+yourself, and submit a `pull request`_! We love feedback and we love help and we don't bite. Much.
+
+I'd like to contribute...but how?
+*********************************
+
+What an excellent question. First of all, please have a look at our general `contributing guidelines`_. We'll wait for
+you here.
+
+All done? Great! While we're super excited to incorporate your new feature into |product_name|, there are a
+couple of things we want to make sure you've given thought to.
+
+- Please write unit tests for your new code. But don't **just** aim to increase the test coverage, rather, we expect you
+ to have written **thoughtful** tests that ensure your new feature will continue to work as expected, and to help future
+ contributors to ensure they don't break it!
+
+- Please document your new feature. Think about **concrete use cases** for your feature, and add a section to the
+ appropriate document, including a **complete** sample program that demonstrates your feature. Don't forget to update
+ the changelog in ``changelog.rst``!
+
+Including these two items with your pull request will totally make our day—and, more importantly, your future users' days!
+
+On that note...
+
+How do I compile the documentation?
+***********************************
+
+This project's documentation is generated with `Sphinx `_. If you are editing one of the many
+reStructuredText files in the ``docs-src`` folder, you'll need to rebuild the documentation. It is recommended to run
+the following steps inside a ``virtualenv`` environment.
+
+.. code-block:: bash
+
+ tox -e docs
+
+
+Do be sure to add the ``docs`` folder and its contents to your pull request!
+
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst
new file mode 100644
index 0000000000..30c9969730
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/index.rst
@@ -0,0 +1,46 @@
+.. toctree::
+ :hidden:
+
+ self
+ auth
+ basic_usage
+ conversations
+ real_time_messaging
+ faq
+ changelog
+ about
+
+==============================================
+|product_name|
+==============================================
+
+Whether you're building a custom app for your team, or integrating a third party
+service into your Slack workflows, |product_name| allows you to leverage the flexibility
+of Python to get your project up and running as quickly as possible.
+
+Requirements and Installation
+******************************
+
+We recommend using `PyPI `_ to install |product_name|
+
+.. code-block:: bash
+
+ pip install slackclient
+
+Of course, if you prefer doing things the hard way, you can always implement |product_name|
+by pulling down the source code directly into your project:
+
+.. code-block:: bash
+
+ git clone https://github.com/slackapi/python-slackclient.git
+ pip install -r requirements.txt
+
+Getting Help
+************
+
+If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue:
+
+- Use our `Github Issue Tracker `_ for reporting bugs or requesting features.
+- Visit the `Bot Developer Hangout `_ for getting help using |product_name| or just generally bond with your fellow Slack developers.
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat
new file mode 100644
index 0000000000..5a08728349
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/make.bat
@@ -0,0 +1,281 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^` where ^ is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. epub3 to make an epub3
+ echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if enabled
+ echo. coverage to run coverage check of the documentation if enabled
+ echo. dummy to check syntax errors of document sources
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+REM Check if sphinx-build is available and fallback to Python version if any
+%SPHINXBUILD% 1>NUL 2>NUL
+if errorlevel 9009 goto sphinx_python
+goto sphinx_ok
+
+:sphinx_python
+
+set SPHINXBUILD=python -m sphinx.__init__
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+:sphinx_ok
+
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-slackclient.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-slackclient.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "epub3" (
+ %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub3 file is in %BUILDDIR%/epub3.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %~dp0
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "coverage" (
+ %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of coverage in the sources finished, look at the ^
+results in %BUILDDIR%/coverage/python.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+if "%1" == "dummy" (
+ %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. Dummy builder generates no files.
+ goto end
+)
+
+:end
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst
new file mode 100644
index 0000000000..75d613470c
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/metadata.rst
@@ -0,0 +1,19 @@
+.. Site settings
+.. |product_name| replace:: Slack Developer Kit for Python
+.. |email| replace:: opensource@slack.com
+.. |repo_name| replace:: python-slackclient
+.. |github_username| replace:: SlackAPI
+.. |twitter_username| replace:: SlackAPI
+
+.. _Bot Developer Hangout: https://dev4slack.slack.com/archives/sdk-python-slackclient
+.. _Issue Tracker: http://github.com/SlackAPI/python-slackclient/issues
+.. _pull request: http://github.com/SlackAPI/python-slackclient/pulls
+.. _Python RTMBot: https://slackapi.github.io/python-rtmbot
+.. _License: https://github.com/SlackAPI/python-slackclient/blob/master/LICENSE
+.. _Code of Conduct: https://slackhq.github.io/code-of-conduct
+.. _Contributing: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md
+.. _contributing guidelines: https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md
+.. _Contributor License Agreement: https://docs.google.com/a/slack-corp.com/forms/d/e/1FAIpQLSfzjVoCM7ohBnjWf7eDYQxzti1EPpinsIJQA5RAUBwJKRUQHg/viewform
+.. _Real Time Messaging (RTM) API: https://api.slack.com/rtm
+.. _Web API: https://api.slack.com/web
+
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst
new file mode 100644
index 0000000000..e651ea6875
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs-src/real_time_messaging.rst
@@ -0,0 +1,130 @@
+.. _real-time-messaging:
+
+==============================================
+Real Time Messaging (RTM)
+==============================================
+The `Real Time Messaging (RTM) API`_ is a WebSocket-based API that allows you to
+receive events from Slack in real time and send messages as users.
+
+If you prefer events to be pushed to you instead, we recommend using the
+HTTP-based `Events API `_ instead.
+Most event types supported by the RTM API are also available
+in `the Events API `_.
+
+See :ref:`Tokens & Authentication ` for API token handling best practices.
+
+Connecting to the RTM API
+------------------------------------------
+::
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ if sc.rtm_connect():
+ while sc.server.connected is True:
+ print sc.rtm_read()
+ time.sleep(1)
+ else:
+ print "Connection Failed"
+
+If you connect successfully the first event received will be a hello:
+::
+
+ {
+ u'type': u'hello'
+ }
+
+If there was a problem connecting an error will be returned, including a descriptive error message:
+::
+
+ {
+ u'type': u'error',
+ u'error': {
+ u'code': 1,
+ u'msg': u'Socket URL has expired'
+ }
+ }
+
+rtm.start vs rtm.connect
+---------------------------
+
+If you expect your app to be used on large teams, we recommend starting the RTM client with `rtm.connect` rather than the default connection method for this client, `rtm.start`.
+`rtm.connect` provides a lighter initial connection payload, without the team's channel and user information included. You'll need to request channel and user info via
+the Web API separately.
+
+To do this, simply pass `with_team_state=False` into the `rtm_connect` call, like so:
+::
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ if sc.rtm_connect(with_team_state=False):
+ while True:
+ print sc.rtm_read()
+ time.sleep(1)
+ else:
+ print "Connection Failed"
+
+
+Passing `auto_reconnect=True` will tell the websocket client to automatically reconnect if the connection gets dropped.
+
+
+See the `rtm.start docs `_ and the `rtm.connect docs `_
+for more details.
+
+
+RTM Events
+-------------
+::
+
+ {
+ u'type': u'message',
+ u'ts': u'1358878749.000002',
+ u'user': u'U023BECGF',
+ u'text': u'Hello'
+ }
+
+See `RTM Events `_ for a complete list of events.
+
+Sending messages via the RTM API
+---------------------------------
+You can send a message to Slack by sending JSON over the websocket connection.
+
+::
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.rtm_send_message("welcome-test", "test")
+
+You can send a message to a private group or direct message channel in the same
+way, but using a Group ID (``C024BE91L``) or DM channel ID (``D024BE91L``).
+
+You can send a message in reply to a thread using the ``thread`` argument, and
+optionally broadcast that message back to the channel by setting
+``reply_broadcast`` to ``True``.
+
+::
+
+ from slackclient import SlackClient
+
+ slack_token = os.environ["SLACK_API_TOKEN"]
+ sc = SlackClient(slack_token)
+
+ sc.rtm_send_message("welcome-test", "test", "1482960137.003543", True)
+
+See `Threading messages `_
+for more details on using threads.
+
+The RTM API only supports posting messages with `basic formatting `_.
+It does not support attachments or other message formatting modes.
+
+ To post a more complex message as a user, see :ref:`Web API usage `.
+
+.. include:: metadata.rst
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh
new file mode 100644
index 0000000000..daf352aeb5
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+sphinx-build -c ./docs-src/_themes/slack/ -b html docs-src docs && touch ./docs/.nojekyll
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo
new file mode 100644
index 0000000000..1c0a8af899
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.buildinfo
@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 6abbc33d255b00e789666fcb765fbf2d
+tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/.nojekyll
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif
new file mode 100644
index 0000000000..61faf8cab2
Binary files /dev/null and b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/ajax-loader.gif differ
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css
new file mode 100644
index 0000000000..104f076ae8
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/basic.css
@@ -0,0 +1,676 @@
+/*
+ * basic.css
+ * ~~~~~~~~~
+ *
+ * Sphinx stylesheet -- basic theme.
+ *
+ * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/* -- main layout ----------------------------------------------------------- */
+
+div.clearer {
+ clear: both;
+}
+
+/* -- relbar ---------------------------------------------------------------- */
+
+div.related {
+ width: 100%;
+ font-size: 90%;
+}
+
+div.related h3 {
+ display: none;
+}
+
+div.related ul {
+ margin: 0;
+ padding: 0 0 0 10px;
+ list-style: none;
+}
+
+div.related li {
+ display: inline;
+}
+
+div.related li.right {
+ float: right;
+ margin-right: 5px;
+}
+
+/* -- sidebar --------------------------------------------------------------- */
+
+div.sphinxsidebarwrapper {
+ padding: 10px 5px 0 10px;
+}
+
+div.sphinxsidebar {
+ float: left;
+ width: 230px;
+ margin-left: -100%;
+ font-size: 90%;
+ word-wrap: break-word;
+ overflow-wrap : break-word;
+}
+
+div.sphinxsidebar ul {
+ list-style: none;
+}
+
+div.sphinxsidebar ul ul,
+div.sphinxsidebar ul.want-points {
+ margin-left: 20px;
+ list-style: square;
+}
+
+div.sphinxsidebar ul ul {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+div.sphinxsidebar form {
+ margin-top: 10px;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #98dbcc;
+ font-family: sans-serif;
+ font-size: 1em;
+}
+
+div.sphinxsidebar #searchbox form.search {
+ overflow: hidden;
+}
+
+div.sphinxsidebar #searchbox input[type="text"] {
+ float: left;
+ width: 80%;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+div.sphinxsidebar #searchbox input[type="submit"] {
+ float: left;
+ width: 20%;
+ border-left: none;
+ padding: 0.25em;
+ box-sizing: border-box;
+}
+
+
+img {
+ border: 0;
+ max-width: 100%;
+}
+
+/* -- search page ----------------------------------------------------------- */
+
+ul.search {
+ margin: 10px 0 0 20px;
+ padding: 0;
+}
+
+ul.search li {
+ padding: 5px 0 5px 20px;
+ background-image: url(file.png);
+ background-repeat: no-repeat;
+ background-position: 0 7px;
+}
+
+ul.search li a {
+ font-weight: bold;
+}
+
+ul.search li div.context {
+ color: #888;
+ margin: 2px 0 0 30px;
+ text-align: left;
+}
+
+ul.keywordmatches li.goodmatch a {
+ font-weight: bold;
+}
+
+/* -- index page ------------------------------------------------------------ */
+
+table.contentstable {
+ width: 90%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table.contentstable p.biglink {
+ line-height: 150%;
+}
+
+a.biglink {
+ font-size: 1.3em;
+}
+
+span.linkdescr {
+ font-style: italic;
+ padding-top: 5px;
+ font-size: 90%;
+}
+
+/* -- general index --------------------------------------------------------- */
+
+table.indextable {
+ width: 100%;
+}
+
+table.indextable td {
+ text-align: left;
+ vertical-align: top;
+}
+
+table.indextable ul {
+ margin-top: 0;
+ margin-bottom: 0;
+ list-style-type: none;
+}
+
+table.indextable > tbody > tr > td > ul {
+ padding-left: 0em;
+}
+
+table.indextable tr.pcap {
+ height: 10px;
+}
+
+table.indextable tr.cap {
+ margin-top: 10px;
+ background-color: #f2f2f2;
+}
+
+img.toggler {
+ margin-right: 3px;
+ margin-top: 3px;
+ cursor: pointer;
+}
+
+div.modindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+div.genindex-jumpbox {
+ border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
+ margin: 1em 0 1em 0;
+ padding: 0.4em;
+}
+
+/* -- domain module index --------------------------------------------------- */
+
+table.modindextable td {
+ padding: 2px;
+ border-collapse: collapse;
+}
+
+/* -- general body styles --------------------------------------------------- */
+
+div.body {
+ min-width: 450px;
+ max-width: 800px;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+ -moz-hyphens: auto;
+ -ms-hyphens: auto;
+ -webkit-hyphens: auto;
+ hyphens: auto;
+}
+
+a.headerlink {
+ visibility: hidden;
+}
+
+h1:hover > a.headerlink,
+h2:hover > a.headerlink,
+h3:hover > a.headerlink,
+h4:hover > a.headerlink,
+h5:hover > a.headerlink,
+h6:hover > a.headerlink,
+dt:hover > a.headerlink,
+caption:hover > a.headerlink,
+p.caption:hover > a.headerlink,
+div.code-block-caption:hover > a.headerlink {
+ visibility: visible;
+}
+
+div.body p.caption {
+ text-align: inherit;
+}
+
+div.body td {
+ text-align: left;
+}
+
+.first {
+ margin-top: 0 !important;
+}
+
+p.rubric {
+ margin-top: 30px;
+ font-weight: bold;
+}
+
+img.align-left, .figure.align-left, object.align-left {
+ clear: left;
+ float: left;
+ margin-right: 1em;
+}
+
+img.align-right, .figure.align-right, object.align-right {
+ clear: right;
+ float: right;
+ margin-left: 1em;
+}
+
+img.align-center, .figure.align-center, object.align-center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.align-left {
+ text-align: left;
+}
+
+.align-center {
+ text-align: center;
+}
+
+.align-right {
+ text-align: right;
+}
+
+/* -- sidebars -------------------------------------------------------------- */
+
+div.sidebar {
+ margin: 0 0 0.5em 1em;
+ border: 1px solid #ddb;
+ padding: 7px 7px 0 7px;
+ background-color: #ffe;
+ width: 40%;
+ float: right;
+}
+
+p.sidebar-title {
+ font-weight: bold;
+}
+
+/* -- topics ---------------------------------------------------------------- */
+
+div.topic {
+ border: 1px solid #ccc;
+ padding: 7px 7px 0 7px;
+ margin: 10px 0 10px 0;
+}
+
+p.topic-title {
+ font-size: 1.1em;
+ font-weight: bold;
+ margin-top: 10px;
+}
+
+/* -- admonitions ----------------------------------------------------------- */
+
+div.admonition {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ padding: 7px;
+}
+
+div.admonition dt {
+ font-weight: bold;
+}
+
+div.admonition dl {
+ margin-bottom: 0;
+}
+
+p.admonition-title {
+ margin: 0px 10px 5px 0px;
+ font-weight: bold;
+}
+
+div.body p.centered {
+ text-align: center;
+ margin-top: 25px;
+}
+
+/* -- tables ---------------------------------------------------------------- */
+
+table.docutils {
+ border: 0;
+ border-collapse: collapse;
+}
+
+table.align-center {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+table caption span.caption-number {
+ font-style: italic;
+}
+
+table caption span.caption-text {
+}
+
+table.docutils td, table.docutils th {
+ padding: 1px 8px 1px 5px;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 1px solid #aaa;
+}
+
+table.footnote td, table.footnote th {
+ border: 0 !important;
+}
+
+th {
+ text-align: left;
+ padding-right: 5px;
+}
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px;
+}
+
+table.citation td {
+ border-bottom: none;
+}
+
+/* -- figures --------------------------------------------------------------- */
+
+div.figure {
+ margin: 0.5em;
+ padding: 0.5em;
+}
+
+div.figure p.caption {
+ padding: 0.3em;
+}
+
+div.figure p.caption span.caption-number {
+ font-style: italic;
+}
+
+div.figure p.caption span.caption-text {
+}
+
+/* -- field list styles ----------------------------------------------------- */
+
+table.field-list td, table.field-list th {
+ border: 0 !important;
+}
+
+.field-list ul {
+ margin: 0;
+ padding-left: 1em;
+}
+
+.field-list p {
+ margin: 0;
+}
+
+.field-name {
+ -moz-hyphens: manual;
+ -ms-hyphens: manual;
+ -webkit-hyphens: manual;
+ hyphens: manual;
+}
+
+/* -- hlist styles ---------------------------------------------------------- */
+
+table.hlist td {
+ vertical-align: top;
+}
+
+
+/* -- other body styles ----------------------------------------------------- */
+
+ol.arabic {
+ list-style: decimal;
+}
+
+ol.loweralpha {
+ list-style: lower-alpha;
+}
+
+ol.upperalpha {
+ list-style: upper-alpha;
+}
+
+ol.lowerroman {
+ list-style: lower-roman;
+}
+
+ol.upperroman {
+ list-style: upper-roman;
+}
+
+dl {
+ margin-bottom: 15px;
+}
+
+dd p {
+ margin-top: 0px;
+}
+
+dd ul, dd table {
+ margin-bottom: 10px;
+}
+
+dd {
+ margin-top: 3px;
+ margin-bottom: 10px;
+ margin-left: 30px;
+}
+
+dt:target, span.highlighted {
+ background-color: #fbe54e;
+}
+
+rect.highlighted {
+ fill: #fbe54e;
+}
+
+dl.glossary dt {
+ font-weight: bold;
+ font-size: 1.1em;
+}
+
+.optional {
+ font-size: 1.3em;
+}
+
+.sig-paren {
+ font-size: larger;
+}
+
+.versionmodified {
+ font-style: italic;
+}
+
+.system-message {
+ background-color: #fda;
+ padding: 5px;
+ border: 3px solid red;
+}
+
+.footnote:target {
+ background-color: #ffa;
+}
+
+.line-block {
+ display: block;
+ margin-top: 1em;
+ margin-bottom: 1em;
+}
+
+.line-block .line-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: 1.5em;
+}
+
+.guilabel, .menuselection {
+ font-family: sans-serif;
+}
+
+.accelerator {
+ text-decoration: underline;
+}
+
+.classifier {
+ font-style: oblique;
+}
+
+abbr, acronym {
+ border-bottom: dotted 1px;
+ cursor: help;
+}
+
+/* -- code displays --------------------------------------------------------- */
+
+pre {
+ overflow: auto;
+ overflow-y: hidden; /* fixes display issues on Chrome browsers */
+}
+
+span.pre {
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ -webkit-hyphens: none;
+ hyphens: none;
+}
+
+td.linenos pre {
+ padding: 5px 0px;
+ border: 0;
+ background-color: transparent;
+ color: #aaa;
+}
+
+table.highlighttable {
+ margin-left: 0.5em;
+}
+
+table.highlighttable td {
+ padding: 0 0.5em 0 0.5em;
+}
+
+div.code-block-caption {
+ padding: 2px 5px;
+ font-size: small;
+}
+
+div.code-block-caption code {
+ background-color: transparent;
+}
+
+div.code-block-caption + div > div.highlight > pre {
+ margin-top: 0;
+}
+
+div.code-block-caption span.caption-number {
+ padding: 0.1em 0.3em;
+ font-style: italic;
+}
+
+div.code-block-caption span.caption-text {
+}
+
+div.literal-block-wrapper {
+ padding: 1em 1em 0;
+}
+
+div.literal-block-wrapper div.highlight {
+ margin: 0;
+}
+
+code.descname {
+ background-color: transparent;
+ font-weight: bold;
+ font-size: 1.2em;
+}
+
+code.descclassname {
+ background-color: transparent;
+}
+
+code.xref, a code {
+ background-color: transparent;
+ font-weight: bold;
+}
+
+h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+ background-color: transparent;
+}
+
+.viewcode-link {
+ float: right;
+}
+
+.viewcode-back {
+ float: right;
+ font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+ margin: -1px -10px;
+ padding: 0 10px;
+}
+
+/* -- math display ---------------------------------------------------------- */
+
+img.math {
+ vertical-align: middle;
+}
+
+div.body div.math p {
+ text-align: center;
+}
+
+span.eqno {
+ float: right;
+}
+
+span.eqno a.headerlink {
+ position: relative;
+ left: 0px;
+ z-index: 1;
+}
+
+div.math:hover a.headerlink {
+ visibility: visible;
+}
+
+/* -- printout stylesheet --------------------------------------------------- */
+
+@media print {
+ div.document,
+ div.documentwrapper,
+ div.bodywrapper {
+ margin: 0 !important;
+ width: 100%;
+ }
+
+ div.sphinxsidebar,
+ div.related,
+ div.footer,
+ #top-link {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css
new file mode 100644
index 0000000000..6cfbfb9c3f
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/classic.css
@@ -0,0 +1,261 @@
+/*
+ * classic.css_t
+ * ~~~~~~~~~~~~~
+ *
+ * Sphinx stylesheet -- classic theme.
+ *
+ * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+@import url("basic.css");
+
+/* -- page layout ----------------------------------------------------------- */
+
+body {
+ font-family: sans-serif;
+ font-size: 100%;
+ background-color: #11303d;
+ color: #000;
+ margin: 0;
+ padding: 0;
+}
+
+div.document {
+ background-color: #1c4e63;
+}
+
+div.documentwrapper {
+ float: left;
+ width: 100%;
+}
+
+div.bodywrapper {
+ margin: 0 0 0 230px;
+}
+
+div.body {
+ background-color: #ffffff;
+ color: #000000;
+ padding: 0 20px 30px 20px;
+}
+
+div.footer {
+ color: #ffffff;
+ width: 100%;
+ padding: 9px 0 9px 0;
+ text-align: center;
+ font-size: 75%;
+}
+
+div.footer a {
+ color: #ffffff;
+ text-decoration: underline;
+}
+
+div.related {
+ background-color: #133f52;
+ line-height: 30px;
+ color: #ffffff;
+}
+
+div.related a {
+ color: #ffffff;
+}
+
+div.sphinxsidebar {
+}
+
+div.sphinxsidebar h3 {
+ font-family: 'Trebuchet MS', sans-serif;
+ color: #ffffff;
+ font-size: 1.4em;
+ font-weight: normal;
+ margin: 0;
+ padding: 0;
+}
+
+div.sphinxsidebar h3 a {
+ color: #ffffff;
+}
+
+div.sphinxsidebar h4 {
+ font-family: 'Trebuchet MS', sans-serif;
+ color: #ffffff;
+ font-size: 1.3em;
+ font-weight: normal;
+ margin: 5px 0 0 0;
+ padding: 0;
+}
+
+div.sphinxsidebar p {
+ color: #ffffff;
+}
+
+div.sphinxsidebar p.topless {
+ margin: 5px 10px 10px 10px;
+}
+
+div.sphinxsidebar ul {
+ margin: 10px;
+ padding: 0;
+ color: #ffffff;
+}
+
+div.sphinxsidebar a {
+ color: #98dbcc;
+}
+
+div.sphinxsidebar input {
+ border: 1px solid #98dbcc;
+ font-family: sans-serif;
+ font-size: 1em;
+}
+
+
+
+/* -- hyperlink styles ------------------------------------------------------ */
+
+a {
+ color: #355f7c;
+ text-decoration: none;
+}
+
+a:visited {
+ color: #355f7c;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+
+
+/* -- body styles ----------------------------------------------------------- */
+
+div.body h1,
+div.body h2,
+div.body h3,
+div.body h4,
+div.body h5,
+div.body h6 {
+ font-family: 'Trebuchet MS', sans-serif;
+ background-color: #f2f2f2;
+ font-weight: normal;
+ color: #20435c;
+ border-bottom: 1px solid #ccc;
+ margin: 20px -20px 10px -20px;
+ padding: 3px 0 3px 10px;
+}
+
+div.body h1 { margin-top: 0; font-size: 200%; }
+div.body h2 { font-size: 160%; }
+div.body h3 { font-size: 140%; }
+div.body h4 { font-size: 120%; }
+div.body h5 { font-size: 110%; }
+div.body h6 { font-size: 100%; }
+
+a.headerlink {
+ color: #c60f0f;
+ font-size: 0.8em;
+ padding: 0 4px 0 4px;
+ text-decoration: none;
+}
+
+a.headerlink:hover {
+ background-color: #c60f0f;
+ color: white;
+}
+
+div.body p, div.body dd, div.body li, div.body blockquote {
+ text-align: justify;
+ line-height: 130%;
+}
+
+div.admonition p.admonition-title + p {
+ display: inline;
+}
+
+div.admonition p {
+ margin-bottom: 5px;
+}
+
+div.admonition pre {
+ margin-bottom: 5px;
+}
+
+div.admonition ul, div.admonition ol {
+ margin-bottom: 5px;
+}
+
+div.note {
+ background-color: #eee;
+ border: 1px solid #ccc;
+}
+
+div.seealso {
+ background-color: #ffc;
+ border: 1px solid #ff6;
+}
+
+div.topic {
+ background-color: #eee;
+}
+
+div.warning {
+ background-color: #ffe4e4;
+ border: 1px solid #f66;
+}
+
+p.admonition-title {
+ display: inline;
+}
+
+p.admonition-title:after {
+ content: ":";
+}
+
+pre {
+ padding: 5px;
+ background-color: #eeffcc;
+ color: #333333;
+ line-height: 120%;
+ border: 1px solid #ac9;
+ border-left: none;
+ border-right: none;
+}
+
+code {
+ background-color: #ecf0f3;
+ padding: 0 1px 0 1px;
+ font-size: 0.95em;
+}
+
+th {
+ background-color: #ede;
+}
+
+.warning code {
+ background: #efc2c2;
+}
+
+.note code {
+ background: #d6d6d6;
+}
+
+.viewcode-back {
+ font-family: sans-serif;
+}
+
+div.viewcode-block:target {
+ background-color: #f4debf;
+ border-top: 1px solid #ac9;
+ border-bottom: 1px solid #ac9;
+}
+
+div.code-block-caption {
+ color: #efefef;
+ background-color: #1c4e63;
+}
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png
new file mode 100644
index 0000000000..15e27edb12
Binary files /dev/null and b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-bright.png differ
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png
new file mode 100644
index 0000000000..4d91bcf57d
Binary files /dev/null and b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment-close.png differ
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png
new file mode 100644
index 0000000000..dfbc0cbd51
Binary files /dev/null and b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/comment.png differ
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css
new file mode 100644
index 0000000000..d614fd2cf4
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/default.css
@@ -0,0 +1,74 @@
+a.headerlink {
+ display: none !important;
+}
+
+.section-title {
+ font-size: 2rem;
+ line-height: 2.5rem;
+ letter-spacing: -1px;
+ font-weight: 700;
+ margin: 0 0 1rem;
+}
+
+nav#api_nav .toctree-l1 {
+ margin-bottom: 1.5rem;
+}
+
+nav#api_nav #api_sections ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l1>a {
+ color: #1264a3;
+ letter-spacing: 0;
+ font-size: .8rem;
+ font-weight: 800;
+ text-transform: uppercase;
+ border: none;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 {
+ margin: 0;
+ padding: 0;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 a {
+ color: #1d1c1d;
+ text-transform: none;
+ font-weight: inherit;
+ padding: 0;
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ font-size: 15px!important;
+ line-height:15px;
+ padding: 4px 8px;
+ border: 1px solid transparent;
+ border-radius: 4px;
+}
+
+nav#api_nav #api_sections ul li.toctree-l2 a:hover {
+ cursor: pointer;
+ text-decoration: none;
+ background-color:#e8f5fa;
+ border-color:#dcf0fb;
+}
+
+nav#api_nav #footer #footer_nav {
+ font-size: .9375rem;
+}
+
+nav#api_nav #footer #footer_nav a {
+ border: none;
+ padding: 0;
+ color: #616061;
+}
+
+nav#api_nav #footer #footer_nav a:hover {
+ text-decoration: none;
+ color: #1c1c1c;
+}
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css
new file mode 100644
index 0000000000..7f360ac666
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/docs.css
@@ -0,0 +1,34 @@
+/* Updates body font */
+body {
+ font-family: Slack-Lato,appleLogo,sans-serif;
+}
+
+/* Replaces old sidebar styled links */
+.sidebar_menu h5 {
+ font-size: 0.8rem;
+ font-weight: 800;
+ margin-bottom: 3px;
+}
+
+/* Aligns footer navigation to the left of the sidebar */
+.footer_nav {
+ margin: 0 !important;
+}
+
+/* Styles the signature all nice and pretty <3 */
+#footer_signature {
+ color:#e01e5a;
+ font-size:.9rem;
+ margin-top: 10px;
+}
+
+/* Fixes link hover state */
+a:hover {
+ text-decoration: underline;
+}
+
+/* Makes footer consistent */
+footer {
+ background-color: transparent;
+ border: 0;
+}
\ No newline at end of file
diff --git a/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js
new file mode 100644
index 0000000000..ffadbec11f
--- /dev/null
+++ b/openpype/modules/slack/python2_vendor/python-slack-sdk-1/docs/_static/doctools.js
@@ -0,0 +1,315 @@
+/*
+ * doctools.js
+ * ~~~~~~~~~~~
+ *
+ * Sphinx JavaScript utilities for all documentation.
+ *
+ * :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS.
+ * :license: BSD, see LICENSE for details.
+ *
+ */
+
+/**
+ * select a different prefix for underscore
+ */
+$u = _.noConflict();
+
+/**
+ * make the code below compatible with browsers without
+ * an installed firebug like debugger
+if (!window.console || !console.firebug) {
+ var names = ["log", "debug", "info", "warn", "error", "assert", "dir",
+ "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace",
+ "profile", "profileEnd"];
+ window.console = {};
+ for (var i = 0; i < names.length; ++i)
+ window.console[names[i]] = function() {};
+}
+ */
+
+/**
+ * small helper function to urldecode strings
+ */
+jQuery.urldecode = function(x) {
+ return decodeURIComponent(x).replace(/\+/g, ' ');
+};
+
+/**
+ * small helper function to urlencode strings
+ */
+jQuery.urlencode = encodeURIComponent;
+
+/**
+ * This function returns the parsed url parameters of the
+ * current request. Multiple values per key are supported,
+ * it will always return arrays of strings for the value parts.
+ */
+jQuery.getQueryParameters = function(s) {
+ if (typeof s === 'undefined')
+ s = document.location.search;
+ var parts = s.substr(s.indexOf('?') + 1).split('&');
+ var result = {};
+ for (var i = 0; i < parts.length; i++) {
+ var tmp = parts[i].split('=', 2);
+ var key = jQuery.urldecode(tmp[0]);
+ var value = jQuery.urldecode(tmp[1]);
+ if (key in result)
+ result[key].push(value);
+ else
+ result[key] = [value];
+ }
+ return result;
+};
+
+/**
+ * highlight a given string on a jquery object by wrapping it in
+ * span elements with the given class name.
+ */
+jQuery.fn.highlightText = function(text, className) {
+ function highlight(node, addItems) {
+ if (node.nodeType === 3) {
+ var val = node.nodeValue;
+ var pos = val.toLowerCase().indexOf(text);
+ if (pos >= 0 &&
+ !jQuery(node.parentNode).hasClass(className) &&
+ !jQuery(node.parentNode).hasClass("nohighlight")) {
+ var span;
+ var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg");
+ if (isInSVG) {
+ span = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
+ } else {
+ span = document.createElement("span");
+ span.className = className;
+ }
+ span.appendChild(document.createTextNode(val.substr(pos, text.length)));
+ node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+ document.createTextNode(val.substr(pos + text.length)),
+ node.nextSibling));
+ node.nodeValue = val.substr(0, pos);
+ if (isInSVG) {
+ var bbox = span.getBBox();
+ var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
+ rect.x.baseVal.value = bbox.x;
+ rect.y.baseVal.value = bbox.y;
+ rect.width.baseVal.value = bbox.width;
+ rect.height.baseVal.value = bbox.height;
+ rect.setAttribute('class', className);
+ var parentOfText = node.parentNode.parentNode;
+ addItems.push({
+ "parent": node.parentNode,
+ "target": rect});
+ }
+ }
+ }
+ else if (!jQuery(node).is("button, select, textarea")) {
+ jQuery.each(node.childNodes, function() {
+ highlight(this, addItems);
+ });
+ }
+ }
+ var addItems = [];
+ var result = this.each(function() {
+ highlight(this, addItems);
+ });
+ for (var i = 0; i < addItems.length; ++i) {
+ jQuery(addItems[i].parent).before(addItems[i].target);
+ }
+ return result;
+};
+
+/*
+ * backward compatibility for jQuery.browser
+ * This will be supported until firefox bug is fixed.
+ */
+if (!jQuery.browser) {
+ jQuery.uaMatch = function(ua) {
+ ua = ua.toLowerCase();
+
+ var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
+ /(webkit)[ \/]([\w.]+)/.exec(ua) ||
+ /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
+ /(msie) ([\w.]+)/.exec(ua) ||
+ ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
+ [];
+
+ return {
+ browser: match[ 1 ] || "",
+ version: match[ 2 ] || "0"
+ };
+ };
+ jQuery.browser = {};
+ jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true;
+}
+
+/**
+ * Small JavaScript module for the documentation.
+ */
+var Documentation = {
+
+ init : function() {
+ this.fixFirefoxAnchorBug();
+ this.highlightSearchWords();
+ this.initIndexTable();
+ if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) {
+ this.initOnKeyListeners();
+ }
+ },
+
+ /**
+ * i18n support
+ */
+ TRANSLATIONS : {},
+ PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; },
+ LOCALE : 'unknown',
+
+ // gettext and ngettext don't access this so that the functions
+ // can safely bound to a different name (_ = Documentation.gettext)
+ gettext : function(string) {
+ var translated = Documentation.TRANSLATIONS[string];
+ if (typeof translated === 'undefined')
+ return string;
+ return (typeof translated === 'string') ? translated : translated[0];
+ },
+
+ ngettext : function(singular, plural, n) {
+ var translated = Documentation.TRANSLATIONS[singular];
+ if (typeof translated === 'undefined')
+ return (n == 1) ? singular : plural;
+ return translated[Documentation.PLURALEXPR(n)];
+ },
+
+ addTranslations : function(catalog) {
+ for (var key in catalog.messages)
+ this.TRANSLATIONS[key] = catalog.messages[key];
+ this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')');
+ this.LOCALE = catalog.locale;
+ },
+
+ /**
+ * add context elements like header anchor links
+ */
+ addContextElements : function() {
+ $('div[id] > :header:first').each(function() {
+ $('\u00B6').
+ attr('href', '#' + this.id).
+ attr('title', _('Permalink to this headline')).
+ appendTo(this);
+ });
+ $('dt[id]').each(function() {
+ $('\u00B6').
+ attr('href', '#' + this.id).
+ attr('title', _('Permalink to this definition')).
+ appendTo(this);
+ });
+ },
+
+ /**
+ * workaround a firefox stupidity
+ * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075
+ */
+ fixFirefoxAnchorBug : function() {
+ if (document.location.hash && $.browser.mozilla)
+ window.setTimeout(function() {
+ document.location.href += '';
+ }, 10);
+ },
+
+ /**
+ * highlight the search words provided in the url in the text
+ */
+ highlightSearchWords : function() {
+ var params = $.getQueryParameters();
+ var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : [];
+ if (terms.length) {
+ var body = $('div.body');
+ if (!body.length) {
+ body = $('body');
+ }
+ window.setTimeout(function() {
+ $.each(terms, function() {
+ body.highlightText(this.toLowerCase(), 'highlighted');
+ });
+ }, 10);
+ $('