diff --git a/.gitmodules b/.gitmodules
index e1b0917e9d..67b820a247 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -3,10 +3,4 @@
url = https://github.com/pypeclub/avalon-core.git
[submodule "repos/avalon-unreal-integration"]
path = repos/avalon-unreal-integration
- url = https://github.com/pypeclub/avalon-unreal-integration.git
-[submodule "openpype/modules/default_modules/ftrack/python2_vendor/arrow"]
- path = openpype/modules/default_modules/ftrack/python2_vendor/arrow
- url = https://github.com/arrow-py/arrow.git
-[submodule "openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api"]
- path = openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
- url = https://bitbucket.org/ftrack/ftrack-python-api.git
\ No newline at end of file
+ url = https://github.com/pypeclub/avalon-unreal-integration.git
\ No newline at end of file
diff --git a/openpype/cli.py b/openpype/cli.py
index 0597c387d0..155e07dea3 100644
--- a/openpype/cli.py
+++ b/openpype/cli.py
@@ -42,6 +42,12 @@ def standalonepublisher():
PypeCommands().launch_standalone_publisher()
+@main.command()
+def traypublisher():
+ """Show new OpenPype Standalone publisher UI."""
+ PypeCommands().launch_traypublisher()
+
+
@main.command()
@click.option("-d", "--debug",
is_flag=True, help=("Run pype tray in debug mode"))
@@ -371,10 +377,15 @@ def run(script):
"--app_variant",
help="Provide specific app variant for test, empty for latest",
default=None)
-def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant):
+@click.option("-t",
+ "--timeout",
+ help="Provide specific timeout value for test case",
+ default=None)
+def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant,
+ timeout):
"""Run all automatic tests after proper initialization via start.py"""
PypeCommands().run_tests(folder, mark, pyargs, test_data_folder,
- persist, app_variant)
+ persist, app_variant, timeout)
@main.command()
diff --git a/openpype/hooks/pre_add_last_workfile_arg.py b/openpype/hooks/pre_add_last_workfile_arg.py
index 89f627b37f..8edccd48d4 100644
--- a/openpype/hooks/pre_add_last_workfile_arg.py
+++ b/openpype/hooks/pre_add_last_workfile_arg.py
@@ -17,6 +17,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook):
"nuke",
"nukex",
"hiero",
+ "houdini",
"nukestudio",
"blender",
"photoshop",
diff --git a/openpype/hooks/pre_global_host_data.py b/openpype/hooks/pre_global_host_data.py
index bae967e25f..4c85a511ed 100644
--- a/openpype/hooks/pre_global_host_data.py
+++ b/openpype/hooks/pre_global_host_data.py
@@ -2,7 +2,7 @@ from openpype.api import Anatomy
from openpype.lib import (
PreLaunchHook,
EnvironmentPrepData,
- prepare_host_environments,
+ prepare_app_environments,
prepare_context_environments
)
@@ -14,14 +14,6 @@ class GlobalHostDataHook(PreLaunchHook):
def execute(self):
"""Prepare global objects to `data` that will be used for sure."""
- if not self.application.is_host:
- self.log.info(
- "Skipped hook {}. Application is not marked as host.".format(
- self.__class__.__name__
- )
- )
- return
-
self.prepare_global_data()
if not self.data.get("asset_doc"):
@@ -49,7 +41,7 @@ class GlobalHostDataHook(PreLaunchHook):
"log": self.log
})
- prepare_host_environments(temp_data, self.launch_context.env_group)
+ prepare_app_environments(temp_data, self.launch_context.env_group)
prepare_context_environments(temp_data)
temp_data.pop("log")
diff --git a/openpype/hosts/flame/api/lib.py b/openpype/hosts/flame/api/lib.py
index bbb7c38119..74d9e7607a 100644
--- a/openpype/hosts/flame/api/lib.py
+++ b/openpype/hosts/flame/api/lib.py
@@ -527,6 +527,7 @@ def get_segment_attributes(segment):
# Add timeline segment to tree
clip_data = {
+ "shot_name": segment.shot_name.get_value(),
"segment_name": segment.name.get_value(),
"segment_comment": segment.comment.get_value(),
"tape_name": segment.tape_name,
diff --git a/openpype/hosts/flame/api/plugin.py b/openpype/hosts/flame/api/plugin.py
index db1793cba8..ec49db1601 100644
--- a/openpype/hosts/flame/api/plugin.py
+++ b/openpype/hosts/flame/api/plugin.py
@@ -361,6 +361,7 @@ class PublishableClip:
vertical_sync_default = False
driving_layer_default = ""
index_from_segment_default = False
+ use_shot_name_default = False
def __init__(self, segment, **kwargs):
self.rename_index = kwargs["rename_index"]
@@ -376,6 +377,7 @@ class PublishableClip:
# segment (clip) main attributes
self.cs_name = self.clip_data["segment_name"]
self.cs_index = int(self.clip_data["segment"])
+ self.shot_name = self.clip_data["shot_name"]
# get track name and index
self.track_index = int(self.clip_data["track"])
@@ -419,18 +421,21 @@ class PublishableClip:
# deal with clip name
new_name = self.marker_data.pop("newClipName")
- if self.rename:
+ if self.rename and not self.use_shot_name:
# rename segment
self.current_segment.name = str(new_name)
self.marker_data["asset"] = str(new_name)
+ elif self.use_shot_name:
+ self.marker_data["asset"] = self.shot_name
+ self.marker_data["hierarchyData"]["shot"] = self.shot_name
else:
self.marker_data["asset"] = self.cs_name
self.marker_data["hierarchyData"]["shot"] = self.cs_name
if self.marker_data["heroTrack"] and self.review_layer:
- self.marker_data.update({"reviewTrack": self.review_layer})
+ self.marker_data["reviewTrack"] = self.review_layer
else:
- self.marker_data.update({"reviewTrack": None})
+ self.marker_data["reviewTrack"] = None
# create pype tag on track_item and add data
fpipeline.imprint(self.current_segment, self.marker_data)
@@ -463,6 +468,8 @@ class PublishableClip:
# ui_inputs data or default values if gui was not used
self.rename = self.ui_inputs.get(
"clipRename", {}).get("value") or self.rename_default
+ self.use_shot_name = self.ui_inputs.get(
+ "useShotName", {}).get("value") or self.use_shot_name_default
self.clip_name = self.ui_inputs.get(
"clipName", {}).get("value") or self.clip_name_default
self.hierarchy = self.ui_inputs.get(
diff --git a/openpype/hosts/flame/plugins/create/create_shot_clip.py b/openpype/hosts/flame/plugins/create/create_shot_clip.py
index f055c77a89..11c00dab42 100644
--- a/openpype/hosts/flame/plugins/create/create_shot_clip.py
+++ b/openpype/hosts/flame/plugins/create/create_shot_clip.py
@@ -87,41 +87,48 @@ class CreateShotClip(opfapi.Creator):
"target": "tag",
"toolTip": "Parents folder for shot root folder, Template filled with `Hierarchy Data` section", # noqa
"order": 0},
+ "useShotName": {
+ "value": True,
+ "type": "QCheckBox",
+ "label": "Use Shot Name",
+ "target": "ui",
+ "toolTip": "Use name form Shot name clip attribute", # noqa
+ "order": 1},
"clipRename": {
"value": False,
"type": "QCheckBox",
"label": "Rename clips",
"target": "ui",
"toolTip": "Renaming selected clips on fly", # noqa
- "order": 1},
+ "order": 2},
"clipName": {
"value": "{sequence}{shot}",
"type": "QLineEdit",
"label": "Clip Name Template",
"target": "ui",
"toolTip": "template for creating shot namespaused for renaming (use rename: on)", # noqa
- "order": 2},
+ "order": 3},
"segmentIndex": {
"value": True,
"type": "QCheckBox",
"label": "Segment index",
"target": "ui",
"toolTip": "Take number from segment index", # noqa
- "order": 3},
+ "order": 4},
"countFrom": {
"value": 10,
"type": "QSpinBox",
"label": "Count sequence from",
"target": "ui",
"toolTip": "Set when the sequence number stafrom", # noqa
- "order": 4},
+ "order": 5},
"countSteps": {
"value": 10,
"type": "QSpinBox",
"label": "Stepping number",
"target": "ui",
"toolTip": "What number is adding every new step", # noqa
- "order": 5},
+ "order": 6},
}
},
"hierarchyData": {
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml b/openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml
similarity index 97%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml
rename to openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml
index fa43ceece7..44a7bd9770 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_thumbnails_jpg.xml
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_thumbnails_jpg.xml
@@ -29,7 +29,7 @@
Jpeg
923688
- <segment name>
+ <shot name>
100
2
4
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml b/openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml
similarity index 95%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml
rename to openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml
index 3ca185b8b4..1d2c5a28bb 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/export_preset/openpype_seg_video_h264.xml
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/export_preset/openpype_seg_video_h264.xml
@@ -27,7 +27,7 @@
QuickTime
- <segment name>
+ <shot name>
0
PCS_709
None
@@ -43,7 +43,7 @@
2021
/profiles/.33622016/HDTV_720p_8Mbits.cdxprof
- <segment name>_<video codec>
+ <shot name>_<video codec>
50
2
4
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/__init__.py b/openpype/hosts/flame/startup/openpype_babypublisher/modules/__init__.py
similarity index 100%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/__init__.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/modules/__init__.py
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/app_utils.py b/openpype/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py
similarity index 98%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/app_utils.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py
index b255d8d3f5..e639c3f482 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/app_utils.py
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/modules/app_utils.py
@@ -8,7 +8,7 @@ PLUGIN_DIR = os.path.dirname(os.path.dirname(__file__))
EXPORT_PRESETS_DIR = os.path.join(PLUGIN_DIR, "export_preset")
CONFIG_DIR = os.path.join(os.path.expanduser(
- "~/.openpype"), "openpype_flame_to_ftrack")
+ "~/.openpype"), "openpype_babypublisher")
@contextmanager
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/ftrack_lib.py b/openpype/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py
similarity index 96%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/ftrack_lib.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py
index c2168016c6..0e84a5ef52 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/ftrack_lib.py
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/modules/ftrack_lib.py
@@ -360,6 +360,8 @@ class FtrackComponentCreator:
class FtrackEntityOperator:
+ existing_tasks = []
+
def __init__(self, session, project_entity):
self.session = session
self.project_entity = project_entity
@@ -392,10 +394,7 @@ class FtrackEntityOperator:
query = '{} where name is "{}" and project_id is "{}"'.format(
type, name, self.project_entity["id"])
- try:
- entity = session.query(query).one()
- except Exception:
- entity = None
+ entity = session.query(query).first()
# if entity doesnt exist then create one
if not entity:
@@ -430,10 +429,21 @@ class FtrackEntityOperator:
return parents
def create_task(self, task_type, task_types, parent):
- existing_task = [
+ _exising_tasks = [
child for child in parent['children']
if child.entity_type.lower() == 'task'
- if child['name'].lower() in task_type.lower()
+ ]
+
+ # add task into existing tasks if they are not already there
+ for _t in _exising_tasks:
+ if _t in self.existing_tasks:
+ continue
+ self.existing_tasks.append(_t)
+
+ existing_task = [
+ task for task in self.existing_tasks
+ if task['name'].lower() in task_type.lower()
+ if task['parent'] == parent
]
if existing_task:
@@ -445,4 +455,5 @@ class FtrackEntityOperator:
})
task["type"] = task_types[task_type]
+ self.existing_tasks.append(task)
return task
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/panel_app.py b/openpype/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py
similarity index 96%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/panel_app.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py
index 648f902872..1e8011efaa 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/panel_app.py
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/modules/panel_app.py
@@ -1,4 +1,4 @@
-from PySide2 import QtWidgets, QtCore
+from Qt import QtWidgets, QtCore
import uiwidgets
import app_utils
@@ -33,11 +33,12 @@ class MainWindow(QtWidgets.QWidget):
self.panel_class.clear_temp_data()
self.panel_class.close()
clear_inner_modules()
+ ftrack_lib.FtrackEntityOperator.existing_tasks = []
# now the panel can be closed
event.accept()
-class FlameToFtrackPanel(object):
+class FlameBabyPublisherPanel(object):
session = None
temp_data_dir = None
processed_components = []
@@ -78,7 +79,7 @@ class FlameToFtrackPanel(object):
# creating ui
self.window.setMinimumSize(1500, 600)
- self.window.setWindowTitle('Sequence Shots to Ftrack')
+ self.window.setWindowTitle('OpenPype: Baby-publisher')
self.window.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint)
self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.window.setFocusPolicy(QtCore.Qt.StrongFocus)
@@ -469,10 +470,14 @@ class FlameToFtrackPanel(object):
for sequence in self.selection:
frame_rate = float(str(sequence.frame_rate)[:-4])
for ver in sequence.versions:
- for tracks in ver.tracks:
- for segment in tracks.segments:
+ for track in ver.tracks:
+ if len(track.segments) == 0 and track.hidden:
+ continue
+ for segment in track.segments:
print(segment.attributes)
- if str(segment.name)[1:-1] == "":
+ if segment.name.get_value() == "":
+ continue
+ if segment.hidden.get_value() is True:
continue
# get clip frame duration
record_duration = str(segment.record_duration)[1:-1]
@@ -492,11 +497,11 @@ class FlameToFtrackPanel(object):
# Add timeline segment to tree
QtWidgets.QTreeWidgetItem(self.tree, [
- str(sequence.name)[1:-1], # seq
- str(segment.name)[1:-1], # shot
+ sequence.name.get_value(), # seq name
+ segment.shot_name.get_value(), # shot name
str(clip_duration), # clip duration
shot_description, # shot description
- str(segment.comment)[1:-1] # task description
+ segment.comment.get_value() # task description
]).setFlags(
QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/uiwidgets.py b/openpype/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py
similarity index 99%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/uiwidgets.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py
index 0d4807a4ea..c6db875df0 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/modules/uiwidgets.py
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/modules/uiwidgets.py
@@ -1,4 +1,4 @@
-from PySide2 import QtWidgets, QtCore
+from Qt import QtWidgets, QtCore
class FlameLabel(QtWidgets.QLabel):
diff --git a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py b/openpype/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py
similarity index 85%
rename from openpype/hosts/flame/startup/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py
rename to openpype/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py
index 5a72706ba1..4675d163e3 100644
--- a/openpype/hosts/flame/startup/openpype_flame_to_ftrack/openpype_flame_to_ftrack.py
+++ b/openpype/hosts/flame/startup/openpype_babypublisher/openpype_babypublisher.py
@@ -16,10 +16,11 @@ def flame_panel_executor(selection):
if "panel_app" in sys.modules.keys():
print("panel_app module is already loaded")
del sys.modules["panel_app"]
+ import panel_app
+ reload(panel_app) # noqa
print("panel_app module removed from sys.modules")
- import panel_app
- panel_app.FlameToFtrackPanel(selection)
+ panel_app.FlameBabyPublisherPanel(selection)
def scope_sequence(selection):
@@ -30,7 +31,7 @@ def scope_sequence(selection):
def get_media_panel_custom_ui_actions():
return [
{
- "name": "OpenPype: Ftrack",
+ "name": "OpenPype: Baby-publisher",
"actions": [
{
"name": "Create Shots",
diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py
index 041b53f6c9..9dd8a351e4 100644
--- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py
+++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py
@@ -176,7 +176,7 @@ def update_frame_range(comp, representations):
versions = list(versions)
versions = [v for v in versions
- if v["data"].get("startFrame", None) is not None]
+ if v["data"].get("frameStart", None) is not None]
if not versions:
log.warning("No versions loaded to match frame range to.\n")
diff --git a/openpype/hosts/houdini/api/__init__.py b/openpype/hosts/houdini/api/__init__.py
index e1500aa5f5..fddf7ab98d 100644
--- a/openpype/hosts/houdini/api/__init__.py
+++ b/openpype/hosts/houdini/api/__init__.py
@@ -24,8 +24,7 @@ from .lib import (
lsattrs,
read,
- maintained_selection,
- unique_name
+ maintained_selection
)
@@ -51,8 +50,7 @@ __all__ = [
"lsattrs",
"read",
- "maintained_selection",
- "unique_name"
+ "maintained_selection"
]
# Backwards API compatibility
diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py
index 72f1c8e71f..bd41618856 100644
--- a/openpype/hosts/houdini/api/lib.py
+++ b/openpype/hosts/houdini/api/lib.py
@@ -99,65 +99,6 @@ def get_id_required_nodes():
return list(nodes)
-def get_additional_data(container):
- """Not implemented yet!"""
- return container
-
-
-def set_parameter_callback(node, parameter, language, callback):
- """Link a callback to a parameter of a node
-
- Args:
- node(hou.Node): instance of the nodee
- parameter(str): name of the parameter
- language(str): name of the language, e.g.: python
- callback(str): command which needs to be triggered
-
- Returns:
- None
-
- """
-
- template_grp = node.parmTemplateGroup()
- template = template_grp.find(parameter)
- if not template:
- return
-
- script_language = (hou.scriptLanguage.Python if language == "python" else
- hou.scriptLanguage.Hscript)
-
- template.setScriptCallbackLanguage(script_language)
- template.setScriptCallback(callback)
-
- template.setTags({"script_callback": callback,
- "script_callback_language": language.lower()})
-
- # Replace the existing template with the adjusted one
- template_grp.replace(parameter, template)
-
- node.setParmTemplateGroup(template_grp)
-
-
-def set_parameter_callbacks(node, parameter_callbacks):
- """Set callbacks for multiple parameters of a node
-
- Args:
- node(hou.Node): instance of a hou.Node
- parameter_callbacks(dict): collection of parameter and callback data
- example: {"active" :
- {"language": "python",
- "callback": "print('hello world)'"}
- }
- Returns:
- None
- """
- for parameter, data in parameter_callbacks.items():
- language = data["language"]
- callback = data["callback"]
-
- set_parameter_callback(node, parameter, language, callback)
-
-
def get_output_parameter(node):
"""Return the render output parameter name of the given node
@@ -189,19 +130,6 @@ def get_output_parameter(node):
raise TypeError("Node type '%s' not supported" % node_type)
-@contextmanager
-def attribute_values(node, data):
-
- previous_attrs = {key: node.parm(key).eval() for key in data.keys()}
- try:
- node.setParms(data)
- yield
- except Exception as exc:
- pass
- finally:
- node.setParms(previous_attrs)
-
-
def set_scene_fps(fps):
hou.setFps(fps)
@@ -349,10 +277,6 @@ def render_rop(ropnode):
raise RuntimeError("Render failed: {0}".format(exc))
-def children_as_string(node):
- return [c.name() for c in node.children()]
-
-
def imprint(node, data):
"""Store attributes with value on a node
@@ -473,53 +397,6 @@ def read(node):
parameter in node.spareParms()}
-def unique_name(name, format="%03d", namespace="", prefix="", suffix="",
- separator="_"):
- """Return unique `name`
-
- The function takes into consideration an optional `namespace`
- and `suffix`. The suffix is included in evaluating whether a
- name exists - such as `name` + "_GRP" - but isn't included
- in the returned value.
-
- If a namespace is provided, only names within that namespace
- are considered when evaluating whether the name is unique.
-
- Arguments:
- format (str, optional): The `name` is given a number, this determines
- how this number is formatted. Defaults to a padding of 2.
- E.g. my_name01, my_name02.
- namespace (str, optional): Only consider names within this namespace.
- suffix (str, optional): Only consider names with this suffix.
-
- Example:
- >>> name = hou.node("/obj").createNode("geo", name="MyName")
- >>> assert hou.node("/obj/MyName")
- True
- >>> unique = unique_name(name)
- >>> assert hou.node("/obj/{}".format(unique))
- False
-
- """
-
- iteration = 1
-
- parts = [prefix, name, format % iteration, suffix]
- if namespace:
- parts.insert(0, namespace)
-
- unique = separator.join(parts)
- children = children_as_string(hou.node("/obj"))
- while unique in children:
- iteration += 1
- unique = separator.join(parts)
-
- if suffix:
- return unique[:-len(suffix)]
-
- return unique
-
-
@contextmanager
def maintained_selection():
"""Maintain selection during context
@@ -542,3 +419,37 @@ def maintained_selection():
if previous_selection:
for node in previous_selection:
node.setSelected(on=True)
+
+
+def reset_framerange():
+ """Set frame range to current asset"""
+
+ asset_name = api.Session["AVALON_ASSET"]
+ asset = io.find_one({"name": asset_name, "type": "asset"})
+
+ frame_start = asset["data"].get("frameStart")
+ frame_end = asset["data"].get("frameEnd")
+ # Backwards compatibility
+ if frame_start is None or frame_end is None:
+ frame_start = asset["data"].get("edit_in")
+ frame_end = asset["data"].get("edit_out")
+
+ if frame_start is None or frame_end is None:
+ log.warning("No edit information found for %s" % asset_name)
+ return
+
+ handles = asset["data"].get("handles") or 0
+ handle_start = asset["data"].get("handleStart")
+ if handle_start is None:
+ handle_start = handles
+
+ handle_end = asset["data"].get("handleEnd")
+ if handle_end is None:
+ handle_end = handles
+
+ frame_start -= int(handle_start)
+ frame_end += int(handle_end)
+
+ hou.playbar.setFrameRange(frame_start, frame_end)
+ hou.playbar.setPlaybackRange(frame_start, frame_end)
+ hou.setFrame(frame_start)
diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py
index c3dbdc5ef5..1c08e72d65 100644
--- a/openpype/hosts/houdini/api/pipeline.py
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -4,6 +4,7 @@ import logging
import contextlib
import hou
+import hdefereval
import pyblish.api
import avalon.api
@@ -66,9 +67,10 @@ def install():
sys.path.append(hou_pythonpath)
- # Set asset FPS for the empty scene directly after launch of Houdini
- # so it initializes into the correct scene FPS
- _set_asset_fps()
+ # Set asset settings for the empty scene directly after launch of Houdini
+ # so it initializes into the correct scene FPS, Frame Range, etc.
+ # todo: make sure this doesn't trigger when opening with last workfile
+ _set_context_settings()
def uninstall():
@@ -279,18 +281,49 @@ def on_open(*args):
def on_new(_):
"""Set project resolution and fps when create a new file"""
+
+ if hou.hipFile.isLoadingHipFile():
+ # This event also triggers when Houdini opens a file due to the
+ # new event being registered to 'afterClear'. As such we can skip
+ # 'new' logic if the user is opening a file anyway
+ log.debug("Skipping on new callback due to scene being opened.")
+ return
+
log.info("Running callback on new..")
- _set_asset_fps()
+ _set_context_settings()
+
+ # It seems that the current frame always gets reset to frame 1 on
+ # new scene. So we enforce current frame to be at the start of the playbar
+ # with execute deferred
+ def _enforce_start_frame():
+ start = hou.playbar.playbackRange()[0]
+ hou.setFrame(start)
+
+ hdefereval.executeDeferred(_enforce_start_frame)
-def _set_asset_fps():
- """Set Houdini scene FPS to the default required for current asset"""
+def _set_context_settings():
+ """Apply the project settings from the project definition
+
+ Settings can be overwritten by an asset if the asset.data contains
+ any information regarding those settings.
+
+ Examples of settings:
+ fps
+ resolution
+ renderer
+
+ Returns:
+ None
+ """
# Set new scene fps
fps = get_asset_fps()
print("Setting scene FPS to %i" % fps)
lib.set_scene_fps(fps)
+ lib.reset_framerange()
+
def on_pyblish_instance_toggled(instance, new_value, old_value):
"""Toggle saver tool passthrough states on instance toggles."""
diff --git a/openpype/hosts/houdini/plugins/load/actions.py b/openpype/hosts/houdini/plugins/load/actions.py
index 6e9410ff58..acdb998c16 100644
--- a/openpype/hosts/houdini/plugins/load/actions.py
+++ b/openpype/hosts/houdini/plugins/load/actions.py
@@ -29,8 +29,8 @@ class SetFrameRangeLoader(api.Loader):
version = context["version"]
version_data = version.get("data", {})
- start = version_data.get("startFrame", None)
- end = version_data.get("endFrame", None)
+ start = version_data.get("frameStart", None)
+ end = version_data.get("frameEnd", None)
if start is None or end is None:
print(
@@ -67,8 +67,8 @@ class SetFrameRangeWithHandlesLoader(api.Loader):
version = context["version"]
version_data = version.get("data", {})
- start = version_data.get("startFrame", None)
- end = version_data.get("endFrame", None)
+ start = version_data.get("frameStart", None)
+ end = version_data.get("frameEnd", None)
if start is None or end is None:
print(
@@ -78,9 +78,8 @@ class SetFrameRangeWithHandlesLoader(api.Loader):
return
# Include handles
- handles = version_data.get("handles", 0)
- start -= handles
- end += handles
+ start -= version_data.get("handleStart", 0)
+ end += version_data.get("handleEnd", 0)
hou.playbar.setFrameRange(start, end)
hou.playbar.setPlaybackRange(start, end)
diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py
index 1b12efa603..fe5962fbd3 100644
--- a/openpype/hosts/houdini/plugins/publish/save_scene.py
+++ b/openpype/hosts/houdini/plugins/publish/save_scene.py
@@ -2,26 +2,14 @@ import pyblish.api
import avalon.api
-class SaveCurrentScene(pyblish.api.InstancePlugin):
+class SaveCurrentScene(pyblish.api.ContextPlugin):
"""Save current scene"""
label = "Save current file"
- order = pyblish.api.IntegratorOrder - 0.49
+ order = pyblish.api.ExtractorOrder - 0.49
hosts = ["houdini"]
- families = ["usdrender",
- "redshift_rop"]
- targets = ["local"]
- def process(self, instance):
-
- # This should be a ContextPlugin, but this is a workaround
- # for a bug in pyblish to run once for a family: issue #250
- context = instance.context
- key = "__hasRun{}".format(self.__class__.__name__)
- if context.data.get(key, False):
- return
- else:
- context.data[key] = True
+ def process(self, context):
# Filename must not have changed since collecting
host = avalon.api.registered_host()
diff --git a/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py b/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py
deleted file mode 100644
index a0efd0610c..0000000000
--- a/openpype/hosts/houdini/plugins/publish/save_scene_deadline.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import pyblish.api
-
-
-class SaveCurrentSceneDeadline(pyblish.api.ContextPlugin):
- """Save current scene"""
-
- label = "Save current file"
- order = pyblish.api.IntegratorOrder - 0.49
- hosts = ["houdini"]
- targets = ["deadline"]
-
- def process(self, context):
- import hou
-
- assert (
- context.data["currentFile"] == hou.hipFile.path()
- ), "Collected filename from current scene name."
-
- if hou.hipFile.hasUnsavedChanges():
- self.log.info("Saving current file..")
- hou.hipFile.save(save_to_recent_files=True)
- else:
- self.log.debug("No unsaved changes, skipping file save..")
diff --git a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py
index 8fe1b44b7a..3e17d3e8de 100644
--- a/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py
+++ b/openpype/hosts/houdini/plugins/publish/validate_abc_primitive_to_detail.py
@@ -65,7 +65,7 @@ class ValidateAbcPrimitiveToDetail(pyblish.api.InstancePlugin):
cls.log.debug("Checking with path attribute: %s" % path_attr)
# Check if the primitive attribute exists
- frame = instance.data.get("startFrame", 0)
+ frame = instance.data.get("frameStart", 0)
geo = output.geometryAtFrame(frame)
# If there are no primitives on the start frame then it might be
diff --git a/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py b/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py
index 17c9da837a..8d7e3b611f 100644
--- a/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py
+++ b/openpype/hosts/houdini/plugins/publish/validate_alembic_input_node.py
@@ -38,7 +38,7 @@ class ValidateAlembicInputNode(pyblish.api.InstancePlugin):
cls.log.warning("No geometry output node found, skipping check..")
return
- frame = instance.data.get("startFrame", 0)
+ frame = instance.data.get("frameStart", 0)
geo = node.geometryAtFrame(frame)
invalid = False
diff --git a/openpype/hosts/houdini/plugins/publish/validate_output_node.py b/openpype/hosts/houdini/plugins/publish/validate_output_node.py
deleted file mode 100644
index 0b60ab5c48..0000000000
--- a/openpype/hosts/houdini/plugins/publish/validate_output_node.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import pyblish.api
-
-
-class ValidateOutputNode(pyblish.api.InstancePlugin):
- """Validate the instance SOP Output Node.
-
- This will ensure:
- - The SOP Path is set.
- - The SOP Path refers to an existing object.
- - The SOP Path node is a SOP node.
- - The SOP Path node has at least one input connection (has an input)
- - The SOP Path has geometry data.
-
- """
-
- order = pyblish.api.ValidatorOrder
- families = ["pointcache", "vdbcache"]
- hosts = ["houdini"]
- label = "Validate Output Node"
-
- def process(self, instance):
-
- invalid = self.get_invalid(instance)
- if invalid:
- raise RuntimeError(
- "Output node(s) `%s` are incorrect. "
- "See plug-in log for details." % invalid
- )
-
- @classmethod
- def get_invalid(cls, instance):
-
- import hou
-
- output_node = instance.data["output_node"]
-
- if output_node is None:
- node = instance[0]
- cls.log.error(
- "SOP Output node in '%s' does not exist. "
- "Ensure a valid SOP output path is set." % node.path()
- )
-
- return [node.path()]
-
- # Output node must be a Sop node.
- if not isinstance(output_node, hou.SopNode):
- cls.log.error(
- "Output node %s is not a SOP node. "
- "SOP Path must point to a SOP node, "
- "instead found category type: %s"
- % (output_node.path(), output_node.type().category().name())
- )
- return [output_node.path()]
-
- # For the sake of completeness also assert the category type
- # is Sop to avoid potential edge case scenarios even though
- # the isinstance check above should be stricter than this category
- assert output_node.type().category().name() == "Sop", (
- "Output node %s is not of category Sop. This is a bug.."
- % output_node.path()
- )
-
- # Check if output node has incoming connections
- if not output_node.inputConnections():
- cls.log.error(
- "Output node `%s` has no incoming connections"
- % output_node.path()
- )
- return [output_node.path()]
-
- # Ensure the output node has at least Geometry data
- if not output_node.geometry():
- cls.log.error(
- "Output node `%s` has no geometry data." % output_node.path()
- )
- return [output_node.path()]
diff --git a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py
index 3c15532be8..1eb36763bb 100644
--- a/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py
+++ b/openpype/hosts/houdini/plugins/publish/validate_primitive_hierarchy_paths.py
@@ -51,7 +51,7 @@ class ValidatePrimitiveHierarchyPaths(pyblish.api.InstancePlugin):
cls.log.debug("Checking for attribute: %s" % path_attr)
# Check if the primitive attribute exists
- frame = instance.data.get("startFrame", 0)
+ frame = instance.data.get("frameStart", 0)
geo = output.geometryAtFrame(frame)
# If there are no primitives on the current frame then we can't
diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml
index b8c7f93d76..abfa3f136e 100644
--- a/openpype/hosts/houdini/startup/MainMenuCommon.xml
+++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml
@@ -66,6 +66,14 @@ host_tools.show_workfiles(parent)
]]>
+
+
+
+
+
diff --git a/openpype/hosts/houdini/startup/scripts/123.py b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py
similarity index 100%
rename from openpype/hosts/houdini/startup/scripts/123.py
rename to openpype/hosts/houdini/startup/python2.7libs/pythonrc.py
diff --git a/openpype/hosts/houdini/startup/scripts/houdinicore.py b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py
similarity index 100%
rename from openpype/hosts/houdini/startup/scripts/houdinicore.py
rename to openpype/hosts/houdini/startup/python3.7libs/pythonrc.py
diff --git a/openpype/hosts/maya/api/__init__.py b/openpype/hosts/maya/api/__init__.py
index 9ea798e927..5d76bf0f04 100644
--- a/openpype/hosts/maya/api/__init__.py
+++ b/openpype/hosts/maya/api/__init__.py
@@ -10,12 +10,6 @@ from .pipeline import (
ls,
containerise,
-
- lock,
- unlock,
- is_locked,
- lock_ignored,
-
)
from .plugin import (
Creator,
@@ -38,11 +32,9 @@ from .lib import (
read,
apply_shaders,
- without_extension,
maintained_selection,
suspended_refresh,
- unique_name,
unique_namespace,
)
@@ -54,11 +46,6 @@ __all__ = [
"ls",
"containerise",
- "lock",
- "unlock",
- "is_locked",
- "lock_ignored",
-
"Creator",
"Loader",
@@ -76,11 +63,9 @@ __all__ = [
"lsattrs",
"read",
- "unique_name",
"unique_namespace",
"apply_shaders",
- "without_extension",
"maintained_selection",
"suspended_refresh",
diff --git a/openpype/hosts/maya/api/customize.py b/openpype/hosts/maya/api/customize.py
index 37fd543315..683e6b24b0 100644
--- a/openpype/hosts/maya/api/customize.py
+++ b/openpype/hosts/maya/api/customize.py
@@ -5,7 +5,7 @@ import logging
from functools import partial
-import maya.cmds as mc
+import maya.cmds as cmds
import maya.mel as mel
from openpype.api import resources
@@ -30,9 +30,9 @@ def override_component_mask_commands():
log.info("Installing override_component_mask_commands..")
# Get all object mask buttons
- buttons = mc.formLayout("objectMaskIcons",
- query=True,
- childArray=True)
+ buttons = cmds.formLayout("objectMaskIcons",
+ query=True,
+ childArray=True)
# Skip the triangle list item
buttons = [btn for btn in buttons if btn != "objPickMenuLayout"]
@@ -43,14 +43,14 @@ def override_component_mask_commands():
# toggle the others based on whether any of the buttons
# was remaining active after the toggle, if not then
# enable all
- if mc.getModifiers() == 4: # = CTRL
+ if cmds.getModifiers() == 4: # = CTRL
state = True
- active = [mc.iconTextCheckBox(btn, query=True, value=True) for btn
- in buttons]
+ active = [cmds.iconTextCheckBox(btn, query=True, value=True)
+ for btn in buttons]
if any(active):
- mc.selectType(allObjects=False)
+ cmds.selectType(allObjects=False)
else:
- mc.selectType(allObjects=True)
+ cmds.selectType(allObjects=True)
# Replace #1 with the current button state
cmd = raw_command.replace(" #1", " {}".format(int(state)))
@@ -63,13 +63,13 @@ def override_component_mask_commands():
# try to implement the fix. (This also allows us to
# "uninstall" the behavior later)
if btn not in COMPONENT_MASK_ORIGINAL:
- original = mc.iconTextCheckBox(btn, query=True, cc=True)
+ original = cmds.iconTextCheckBox(btn, query=True, cc=True)
COMPONENT_MASK_ORIGINAL[btn] = original
# Assign the special callback
original = COMPONENT_MASK_ORIGINAL[btn]
new_fn = partial(on_changed_callback, original)
- mc.iconTextCheckBox(btn, edit=True, cc=new_fn)
+ cmds.iconTextCheckBox(btn, edit=True, cc=new_fn)
def override_toolbox_ui():
@@ -78,25 +78,36 @@ def override_toolbox_ui():
parent_widget = get_main_window()
# Ensure the maya web icon on toolbox exists
- web_button = "ToolBox|MainToolboxLayout|mayaWebButton"
- if not mc.iconTextButton(web_button, query=True, exists=True):
+ button_names = [
+ # Maya 2022.1+ with maya.cmds.iconTextStaticLabel
+ "ToolBox|MainToolboxLayout|mayaHomeToolboxButton",
+ # Older with maya.cmds.iconTextButton
+ "ToolBox|MainToolboxLayout|mayaWebButton"
+ ]
+ for name in button_names:
+ if cmds.control(name, query=True, exists=True):
+ web_button = name
+ break
+ else:
+ # Button does not exist
+ log.warning("Can't find Maya Home/Web button to override toolbox ui..")
return
- mc.iconTextButton(web_button, edit=True, visible=False)
+ cmds.control(web_button, edit=True, visible=False)
# real = 32, but 36 with padding - according to toolbox mel script
icon_size = 36
parent = web_button.rsplit("|", 1)[0]
# Ensure the parent is a formLayout
- if not mc.objectTypeUI(parent) == "formLayout":
+ if not cmds.objectTypeUI(parent) == "formLayout":
return
# Create our controls
controls = []
controls.append(
- mc.iconTextButton(
+ cmds.iconTextButton(
"pype_toolbox_lookmanager",
annotation="Look Manager",
label="Look Manager",
@@ -109,7 +120,7 @@ def override_toolbox_ui():
)
controls.append(
- mc.iconTextButton(
+ cmds.iconTextButton(
"pype_toolbox_workfiles",
annotation="Work Files",
label="Work Files",
@@ -124,7 +135,7 @@ def override_toolbox_ui():
)
controls.append(
- mc.iconTextButton(
+ cmds.iconTextButton(
"pype_toolbox_loader",
annotation="Loader",
label="Loader",
@@ -139,7 +150,7 @@ def override_toolbox_ui():
)
controls.append(
- mc.iconTextButton(
+ cmds.iconTextButton(
"pype_toolbox_manager",
annotation="Inventory",
label="Inventory",
@@ -159,7 +170,7 @@ def override_toolbox_ui():
for i, control in enumerate(controls):
previous = controls[i - 1] if i > 0 else web_button
- mc.formLayout(parent, edit=True,
- attachControl=[control, "bottom", 0, previous],
- attachForm=([control, "left", 1],
- [control, "right", 1]))
+ cmds.formLayout(parent, edit=True,
+ attachControl=[control, "bottom", 0, previous],
+ attachForm=([control, "left", 1],
+ [control, "right", 1]))
diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py
index 21e027109e..41c67a6209 100644
--- a/openpype/hosts/maya/api/lib.py
+++ b/openpype/hosts/maya/api/lib.py
@@ -2,14 +2,12 @@
import os
import sys
-import re
import platform
import uuid
import math
import json
import logging
-import itertools
import contextlib
from collections import OrderedDict, defaultdict
from math import ceil
@@ -154,53 +152,9 @@ def maintained_selection():
cmds.select(clear=True)
-def unique_name(name, format="%02d", namespace="", prefix="", suffix=""):
- """Return unique `name`
-
- The function takes into consideration an optional `namespace`
- and `suffix`. The suffix is included in evaluating whether a
- name exists - such as `name` + "_GRP" - but isn't included
- in the returned value.
-
- If a namespace is provided, only names within that namespace
- are considered when evaluating whether the name is unique.
-
- Arguments:
- format (str, optional): The `name` is given a number, this determines
- how this number is formatted. Defaults to a padding of 2.
- E.g. my_name01, my_name02.
- namespace (str, optional): Only consider names within this namespace.
- suffix (str, optional): Only consider names with this suffix.
-
- Example:
- >>> name = cmds.createNode("transform", name="MyName")
- >>> cmds.objExists(name)
- True
- >>> unique = unique_name(name)
- >>> cmds.objExists(unique)
- False
-
- """
-
- iteration = 1
- unique = prefix + (name + format % iteration) + suffix
-
- while cmds.objExists(namespace + ":" + unique):
- iteration += 1
- unique = prefix + (name + format % iteration) + suffix
-
- if suffix:
- return unique[:-len(suffix)]
-
- return unique
-
-
def unique_namespace(namespace, format="%02d", prefix="", suffix=""):
"""Return unique namespace
- Similar to :func:`unique_name` but evaluating namespaces
- as opposed to object names.
-
Arguments:
namespace (str): Name of namespace to consider
format (str, optional): Formatting of the given iteration number
@@ -312,155 +266,10 @@ def float_round(num, places=0, direction=ceil):
def pairwise(iterable):
"""s -> (s0,s1), (s2,s3), (s4, s5), ..."""
+ from six.moves import zip
+
a = iter(iterable)
- return itertools.izip(a, a)
-
-
-def unique(name):
- assert isinstance(name, string_types), "`name` must be string"
-
- while cmds.objExists(name):
- matches = re.findall(r"\d+$", name)
-
- if matches:
- match = matches[-1]
- name = name.rstrip(match)
- number = int(match) + 1
- else:
- number = 1
-
- name = name + str(number)
-
- return name
-
-
-def uv_from_element(element):
- """Return the UV coordinate of given 'element'
-
- Supports components, meshes, nurbs.
-
- """
-
- supported = ["mesh", "nurbsSurface"]
-
- uv = [0.5, 0.5]
-
- if "." not in element:
- type = cmds.nodeType(element)
- if type == "transform":
- geometry_shape = cmds.listRelatives(element, shapes=True)
-
- if len(geometry_shape) >= 1:
- geometry_shape = geometry_shape[0]
- else:
- return
-
- elif type in supported:
- geometry_shape = element
-
- else:
- cmds.error("Could not do what you wanted..")
- return
- else:
- # If it is indeed a component - get the current Mesh
- try:
- parent = element.split(".", 1)[0]
-
- # Maya is funny in that when the transform of the shape
- # of the component element has children, the name returned
- # by that elementection is the shape. Otherwise, it is
- # the transform. So lets see what type we're dealing with here.
- if cmds.nodeType(parent) in supported:
- geometry_shape = parent
- else:
- geometry_shape = cmds.listRelatives(parent, shapes=1)[0]
-
- if not geometry_shape:
- cmds.error("Skipping %s: Could not find shape." % element)
- return
-
- if len(cmds.ls(geometry_shape)) > 1:
- cmds.warning("Multiple shapes with identical "
- "names found. This might not work")
-
- except TypeError as e:
- cmds.warning("Skipping %s: Didn't find a shape "
- "for component elementection. %s" % (element, e))
- return
-
- try:
- type = cmds.nodeType(geometry_shape)
-
- if type == "nurbsSurface":
- # If a surfacePoint is elementected on a nurbs surface
- root, u, v = element.rsplit("[", 2)
- uv = [float(u[:-1]), float(v[:-1])]
-
- if type == "mesh":
- # -----------
- # Average the U and V values
- # ===========
- uvs = cmds.polyListComponentConversion(element, toUV=1)
- if not uvs:
- cmds.warning("Couldn't derive any UV's from "
- "component, reverting to default U and V")
- raise TypeError
-
- # Flatten list of Uv's as sometimes it returns
- # neighbors like this [2:3] instead of [2], [3]
- flattened = []
-
- for uv in uvs:
- flattened.extend(cmds.ls(uv, flatten=True))
-
- uvs = flattened
-
- sumU = 0
- sumV = 0
- for uv in uvs:
- try:
- u, v = cmds.polyEditUV(uv, query=True)
- except Exception:
- cmds.warning("Couldn't find any UV coordinated, "
- "reverting to default U and V")
- raise TypeError
-
- sumU += u
- sumV += v
-
- averagedU = sumU / len(uvs)
- averagedV = sumV / len(uvs)
-
- uv = [averagedU, averagedV]
- except TypeError:
- pass
-
- return uv
-
-
-def shape_from_element(element):
- """Return shape of given 'element'
-
- Supports components, meshes, and surfaces
-
- """
-
- try:
- # Get either shape or transform, based on element-type
- node = cmds.ls(element, objectsOnly=True)[0]
- except Exception:
- cmds.warning("Could not find node in %s" % element)
- return None
-
- if cmds.nodeType(node) == 'transform':
- try:
- return cmds.listRelatives(node, shapes=True)[0]
- except Exception:
- cmds.warning("Could not find shape in %s" % element)
- return None
-
- else:
- return node
+ return zip(a, a)
def export_alembic(nodes,
@@ -546,7 +355,8 @@ def collect_animation_data(fps=False):
data = OrderedDict()
data["frameStart"] = start
data["frameEnd"] = end
- data["handles"] = 0
+ data["handleStart"] = 0
+ data["handleEnd"] = 0
data["step"] = 1.0
if fps:
@@ -607,115 +417,6 @@ def imprint(node, data):
cmds.setAttr(node + "." + key, value, **set_type)
-def serialise_shaders(nodes):
- """Generate a shader set dictionary
-
- Arguments:
- nodes (list): Absolute paths to nodes
-
- Returns:
- dictionary of (shader: id) pairs
-
- Schema:
- {
- "shader1": ["id1", "id2"],
- "shader2": ["id3", "id1"]
- }
-
- Example:
- {
- "Bazooka_Brothers01_:blinn4SG": [
- "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4922:5001]",
- "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4587:4634]",
- "f9520572-ac1d-11e6-b39e-3085a99791c9.f[1120:1567]",
- "f9520572-ac1d-11e6-b39e-3085a99791c9.f[4251:4362]"
- ],
- "lambert2SG": [
- "f9520571-ac1d-11e6-9dbb-3085a99791c9"
- ]
- }
-
- """
-
- valid_nodes = cmds.ls(
- nodes,
- long=True,
- recursive=True,
- showType=True,
- objectsOnly=True,
- type="transform"
- )
-
- meshes_by_id = {}
- for mesh in valid_nodes:
- shapes = cmds.listRelatives(valid_nodes[0],
- shapes=True,
- fullPath=True) or list()
-
- if shapes:
- shape = shapes[0]
- if not cmds.nodeType(shape):
- continue
-
- try:
- id_ = cmds.getAttr(mesh + ".mbID")
-
- if id_ not in meshes_by_id:
- meshes_by_id[id_] = list()
-
- meshes_by_id[id_].append(mesh)
-
- except ValueError:
- continue
-
- meshes_by_shader = dict()
- for mesh in meshes_by_id.values():
- shape = cmds.listRelatives(mesh,
- shapes=True,
- fullPath=True) or list()
-
- for shader in cmds.listConnections(shape,
- type="shadingEngine") or list():
-
- # Objects in this group are those that haven't got
- # any shaders. These are expected to be managed
- # elsewhere, such as by the default model loader.
- if shader == "initialShadingGroup":
- continue
-
- if shader not in meshes_by_shader:
- meshes_by_shader[shader] = list()
-
- shaded = cmds.sets(shader, query=True) or list()
- meshes_by_shader[shader].extend(shaded)
-
- shader_by_id = {}
- for shader, shaded in meshes_by_shader.items():
-
- if shader not in shader_by_id:
- shader_by_id[shader] = list()
-
- for mesh in shaded:
-
- # Enable shader assignment to faces.
- name = mesh.split(".f[")[0]
-
- transform = name
- if cmds.objectType(transform) == "mesh":
- transform = cmds.listRelatives(name, parent=True)[0]
-
- try:
- id_ = cmds.getAttr(transform + ".mbID")
- shader_by_id[shader].append(mesh.replace(name, id_))
- except KeyError:
- continue
-
- # Remove duplicates
- shader_by_id[shader] = list(set(shader_by_id[shader]))
-
- return shader_by_id
-
-
def lsattr(attr, value=None):
"""Return nodes matching `key` and `value`
@@ -794,17 +495,6 @@ def lsattrs(attrs):
return list(matches)
-@contextlib.contextmanager
-def without_extension():
- """Use cmds.file with defaultExtensions=False"""
- previous_setting = cmds.file(defaultExtensions=True, query=True)
- try:
- cmds.file(defaultExtensions=False)
- yield
- finally:
- cmds.file(defaultExtensions=previous_setting)
-
-
@contextlib.contextmanager
def attribute_values(attr_values):
"""Remaps node attributes to values during context.
@@ -883,26 +573,6 @@ def evaluation(mode="off"):
cmds.evaluationManager(mode=original)
-@contextlib.contextmanager
-def no_refresh():
- """Temporarily disables Maya's UI updates
-
- Note:
- This only disabled the main pane and will sometimes still
- trigger updates in torn off panels.
-
- """
-
- pane = _get_mel_global('gMainPane')
- state = cmds.paneLayout(pane, query=True, manage=True)
- cmds.paneLayout(pane, edit=True, manage=False)
-
- try:
- yield
- finally:
- cmds.paneLayout(pane, edit=True, manage=state)
-
-
@contextlib.contextmanager
def empty_sets(sets, force=False):
"""Remove all members of the sets during the context"""
@@ -1569,15 +1239,6 @@ def extract_alembic(file,
return file
-def maya_temp_folder():
- scene_dir = os.path.dirname(cmds.file(query=True, sceneName=True))
- tmp_dir = os.path.abspath(os.path.join(scene_dir, "..", "tmp"))
- if not os.path.isdir(tmp_dir):
- os.makedirs(tmp_dir)
-
- return tmp_dir
-
-
# region ID
def get_id_required_nodes(referenced_nodes=False, nodes=None):
"""Filter out any node which are locked (reference) or readOnly
@@ -1762,22 +1423,6 @@ def set_id(node, unique_id, overwrite=False):
cmds.setAttr(attr, unique_id, type="string")
-def remove_id(node):
- """Remove the id attribute from the input node.
-
- Args:
- node (str): The node name
-
- Returns:
- bool: Whether an id attribute was deleted
-
- """
- if cmds.attributeQuery("cbId", node=node, exists=True):
- cmds.deleteAttr("{}.cbId".format(node))
- return True
- return False
-
-
# endregion ID
def get_reference_node(path):
"""
@@ -2453,6 +2098,7 @@ def reset_scene_resolution():
set_scene_resolution(width, height, pixelAspect)
+
def set_context_settings():
"""Apply the project settings from the project definition
@@ -2911,7 +2557,7 @@ def get_attr_in_layer(attr, layer):
def fix_incompatible_containers():
- """Return whether the current scene has any outdated content"""
+ """Backwards compatibility: old containers to use new ReferenceLoader"""
host = api.registered_host()
for container in host.ls():
@@ -3150,7 +2796,7 @@ class RenderSetupListObserver:
cmds.delete(render_layer_set_name)
-class RenderSetupItemObserver():
+class RenderSetupItemObserver:
"""Handle changes in render setup items."""
def __init__(self, item):
@@ -3342,7 +2988,27 @@ def set_colorspace():
"""
project_name = os.getenv("AVALON_PROJECT")
imageio = get_anatomy_settings(project_name)["imageio"]["maya"]
- root_dict = imageio["colorManagementPreference"]
+
+ # Maya 2022+ introduces new OCIO v2 color management settings that
+ # can override the old color managenement preferences. OpenPype has
+ # separate settings for both so we fall back when necessary.
+ use_ocio_v2 = imageio["colorManagementPreference_v2"]["enabled"]
+ required_maya_version = 2022
+ maya_version = int(cmds.about(version=True))
+ maya_supports_ocio_v2 = maya_version >= required_maya_version
+ if use_ocio_v2 and not maya_supports_ocio_v2:
+ # Fallback to legacy behavior with a warning
+ log.warning("Color Management Preference v2 is enabled but not "
+ "supported by current Maya version: {} (< {}). Falling "
+ "back to legacy settings.".format(
+ maya_version, required_maya_version)
+ )
+ use_ocio_v2 = False
+
+ if use_ocio_v2:
+ root_dict = imageio["colorManagementPreference_v2"]
+ else:
+ root_dict = imageio["colorManagementPreference"]
if not isinstance(root_dict, dict):
msg = "set_colorspace(): argument should be dictionary"
@@ -3350,11 +3016,12 @@ def set_colorspace():
log.debug(">> root_dict: {}".format(root_dict))
- # first enable color management
+ # enable color management
cmds.colorManagementPrefs(e=True, cmEnabled=True)
cmds.colorManagementPrefs(e=True, ocioRulesEnabled=True)
- # second set config path
+ # set config path
+ custom_ocio_config = False
if root_dict.get("configFilePath"):
unresolved_path = root_dict["configFilePath"]
ocio_paths = unresolved_path[platform.system().lower()]
@@ -3371,22 +3038,56 @@ def set_colorspace():
cmds.colorManagementPrefs(e=True, cmConfigFileEnabled=True)
log.debug("maya '{}' changed to: {}".format(
"configFilePath", resolved_path))
- root_dict.pop("configFilePath")
+ custom_ocio_config = True
else:
cmds.colorManagementPrefs(e=True, cmConfigFileEnabled=False)
- cmds.colorManagementPrefs(e=True, configFilePath="" )
+ cmds.colorManagementPrefs(e=True, configFilePath="")
- # third set rendering space and view transform
- renderSpace = root_dict["renderSpace"]
- cmds.colorManagementPrefs(e=True, renderingSpaceName=renderSpace)
- viewTransform = root_dict["viewTransform"]
- cmds.colorManagementPrefs(e=True, viewTransformName=viewTransform)
+ # If no custom OCIO config file was set we make sure that Maya 2022+
+ # either chooses between Maya's newer default v2 or legacy config based
+ # on OpenPype setting to use ocio v2 or not.
+ if maya_supports_ocio_v2 and not custom_ocio_config:
+ if use_ocio_v2:
+ # Use Maya 2022+ default OCIO v2 config
+ log.info("Setting default Maya OCIO v2 config")
+ cmds.colorManagementPrefs(edit=True, configFilePath="")
+ else:
+ # Set the Maya default config file path
+ log.info("Setting default Maya OCIO v1 legacy config")
+ cmds.colorManagementPrefs(edit=True, configFilePath="legacy")
+
+ # set color spaces for rendering space and view transforms
+ def _colormanage(**kwargs):
+ """Wrapper around `cmds.colorManagementPrefs`.
+
+ This logs errors instead of raising an error so color management
+ settings get applied as much as possible.
+
+ """
+ assert len(kwargs) == 1, "Must receive one keyword argument"
+ try:
+ cmds.colorManagementPrefs(edit=True, **kwargs)
+ log.debug("Setting Color Management Preference: {}".format(kwargs))
+ except RuntimeError as exc:
+ log.error(exc)
+
+ if use_ocio_v2:
+ _colormanage(renderingSpaceName=root_dict["renderSpace"])
+ _colormanage(displayName=root_dict["displayName"])
+ _colormanage(viewName=root_dict["viewName"])
+ else:
+ _colormanage(renderingSpaceName=root_dict["renderSpace"])
+ if maya_supports_ocio_v2:
+ _colormanage(viewName=root_dict["viewTransform"])
+ _colormanage(displayName="legacy")
+ else:
+ _colormanage(viewTransformName=root_dict["viewTransform"])
@contextlib.contextmanager
def root_parent(nodes):
# type: (list) -> list
- """Context manager to un-parent provided nodes and return then back."""
+ """Context manager to un-parent provided nodes and return them back."""
import pymel.core as pm # noqa
node_parents = []
diff --git a/openpype/hosts/maya/api/menu.json b/openpype/hosts/maya/api/menu.json
deleted file mode 100644
index a2efd5233c..0000000000
--- a/openpype/hosts/maya/api/menu.json
+++ /dev/null
@@ -1,924 +0,0 @@
-[
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\save_scene_incremental.py",
- "sourcetype": "file",
- "title": "# Version Up",
- "tooltip": "Incremental save with a specific format"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\open_current_folder.py",
- "sourcetype": "file",
- "title": "Open working folder..",
- "tooltip": "Show current scene in Explorer"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\avalon\\launch_manager.py",
- "sourcetype": "file",
- "title": "# Project Manager",
- "tooltip": "Add assets to the project"
- },
- {
- "type": "action",
- "command": "from openpype.tools.assetcreator import app as assetcreator; assetcreator.show(context='maya')",
- "sourcetype": "python",
- "title": "Asset Creator",
- "tooltip": "Open the Asset Creator"
- },
- {
- "type": "separator"
- },
- {
- "type": "menu",
- "title": "Modeling",
- "items": [
- {
- "type": "action",
- "command": "import easyTreezSource; reload(easyTreezSource); easyTreezSource.easyTreez()",
- "sourcetype": "python",
- "tags": ["modeling", "trees", "generate", "create", "plants"],
- "title": "EasyTreez",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\separateMeshPerShader.py",
- "sourcetype": "file",
- "tags": ["modeling", "separateMeshPerShader"],
- "title": "# Separate Mesh Per Shader",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polyDetachSeparate.py",
- "sourcetype": "file",
- "tags": ["modeling", "poly", "detach", "separate"],
- "title": "# Polygon Detach and Separate",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py",
- "sourcetype": "file",
- "tags": ["modeling", "select", "nth", "edge", "ui"],
- "title": "# Select Every Nth Edge"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\djPFXUVs.py",
- "sourcetype": "file",
- "tags": ["modeling", "djPFX", "UVs"],
- "title": "# dj PFX UVs",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Rigging",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\advancedSkeleton.py",
- "sourcetype": "file",
- "tags": [
- "rigging",
- "autorigger",
- "advanced",
- "skeleton",
- "advancedskeleton",
- "file"
- ],
- "title": "Advanced Skeleton"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Shading",
- "items": [
- {
- "type": "menu",
- "title": "# VRay",
- "items": [
- {
- "type": "action",
- "title": "# Import Proxies",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayImportProxies.py",
- "sourcetype": "file",
- "tags": ["shading", "vray", "import", "proxies"],
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Select All GES",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGES.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "select All GES"]
- },
- {
- "type": "action",
- "title": "# Select All GES Under Selection",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "select", "all", "GES"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Selection To VRay Mesh",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "selection", "vraymesh"]
- },
- {
- "type": "action",
- "title": "# Add VRay Round Edges Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "round edges", "attribute"]
- },
- {
- "type": "action",
- "title": "# Add Gamma",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayAddGamma.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "add gamma"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\select_vraymesh_materials_with_unconnected_shader_slots.py",
- "sourcetype": "file",
- "title": "# Select Unconnected Shader Materials",
- "tags": [
- "shading",
- "vray",
- "select",
- "vraymesh",
- "materials",
- "unconnected shader slots"
- ],
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py",
- "sourcetype": "file",
- "title": "# Merge Similar VRay Mesh Materials",
- "tags": [
- "shading",
- "vray",
- "Merge",
- "VRayMesh",
- "Materials"
- ],
- "tooltip": ""
- },
- {
- "type": "action",
- "title": "# Create Two Sided Material",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtlRenamed.py",
- "sourcetype": "file",
- "tooltip": "Creates two sided material for selected material and renames it",
- "tags": ["shading", "vray", "two sided", "material"]
- },
- {
- "type": "action",
- "title": "# Create Two Sided Material For Selected",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtl.py",
- "sourcetype": "file",
- "tooltip": "Select material to create a two sided version from it",
- "tags": [
- "shading",
- "vray",
- "Create2SidedMtlForSelectedMtl.py"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Add OpenSubdiv Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "add",
- "open subdiv",
- "attribute"
- ]
- },
- {
- "type": "action",
- "title": "# Remove OpenSubdiv Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "remove",
- "opensubdiv",
- "attributee"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Add Subdivision Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "addVraySubdivisionAttribute"
- ]
- },
- {
- "type": "action",
- "title": "# Remove Subdivision Attribute.py",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "remove",
- "subdivision",
- "attribute"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Add Vray Object Ids",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayObjectIds.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "add", "object id"]
- },
- {
- "type": "action",
- "title": "# Add Vray Material Ids",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "addVrayMaterialIds.py"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "# Set Physical DOF Depth",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "physical", "DOF ", "Depth"]
- },
- {
- "type": "action",
- "title": "# Magic Vray Proxy UI",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "magicVrayProxyUI"]
- }
- ]
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\pyblish\\lighting\\set_filename_prefix.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "lookdev",
- "assign",
- "shaders",
- "prefix",
- "filename",
- "render"
- ],
- "title": "# Set filename prefix",
- "tooltip": "Set the render file name prefix."
- },
- {
- "type": "action",
- "command": "import mayalookassigner; mayalookassigner.show()",
- "sourcetype": "python",
- "tags": ["shading", "look", "assign", "shaders", "auto"],
- "title": "Look Manager",
- "tooltip": "Open the Look Manager UI for look assignment"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\LightLinkUi.py",
- "sourcetype": "file",
- "tags": ["shading", "light", "link", "ui"],
- "title": "# Light Link UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vdviewer_ui.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "look",
- "vray",
- "displacement",
- "shaders",
- "auto"
- ],
- "title": "# VRay Displ Viewer",
- "tooltip": "Open the VRay Displacement Viewer, select and control the content of the set"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py",
- "sourcetype": "file",
- "tags": ["shading", "CLRImage", "textures", "preview"],
- "title": "# Set Texture Preview To CLRImage",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixDefaultShaderSetBehavior.py",
- "sourcetype": "file",
- "tags": ["shading", "fix", "DefaultShaderSet", "Behavior"],
- "title": "# Fix Default Shader Set Behavior",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "fix",
- "Selected",
- "Shapes",
- "Reference",
- "Assignments"
- ],
- "title": "# Fix Shapes Reference Assignments",
- "tooltip": "Select shapes to fix the reference assignments"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\selectLambert1Members.py",
- "sourcetype": "file",
- "tags": ["shading", "selectLambert1Members"],
- "title": "# Select Lambert1 Members",
- "tooltip": "Selects all objects which have the Lambert1 shader assigned"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\selectShapesWithoutShader.py",
- "sourcetype": "file",
- "tags": ["shading", "selectShapesWithoutShader"],
- "title": "# Select Shapes Without Shader",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py",
- "sourcetype": "file",
- "tags": ["shading", "fixRenderLayerOutAdjustmentErrors"],
- "title": "# Fix RenderLayer Out Adjustment Errors",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fix_renderlayer_missing_node_override.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "renderlayer",
- "missing",
- "reference",
- "switch",
- "layer"
- ],
- "title": "# Fix RenderLayer Missing Referenced Nodes Overrides",
- "tooltip": ""
- },
- {
- "type": "action",
- "title": "# Image 2 Tiled EXR",
- "command": "$OPENPYPE_SCRIPTS\\shading\\open_img2exr.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "exr"]
- }
- ]
- },
- {
- "type": "menu",
- "title": "# Rendering",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\pyblish\\open_deadline_submission_settings.py",
- "sourcetype": "file",
- "tags": ["settings", "deadline", "globals", "render"],
- "title": "# DL Submission Settings UI",
- "tooltip": "Open the Deadline Submission Settings UI"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Animation",
- "items": [
- {
- "type": "menu",
- "title": "# Attributes",
- "tooltip": "",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyValues.py",
- "sourcetype": "file",
- "tags": ["animation", "copy", "attributes"],
- "title": "# Copy Values",
- "tooltip": "Copy attribute values"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyInConnections.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "connections",
- "incoming"
- ],
- "title": "# Copy In Connections",
- "tooltip": "Copy incoming connections"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyOutConnections.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "connections",
- "out"
- ],
- "title": "# Copy Out Connections",
- "tooltip": "Copy outcoming connections"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformLocal.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "local"
- ],
- "title": "# Copy Local Transforms",
- "tooltip": "Copy local transforms"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "matrix"
- ],
- "title": "# Copy Matrix Transforms",
- "tooltip": "Copy Matrix transforms"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformUI.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "UI"
- ],
- "title": "# Copy Transforms UI",
- "tooltip": "Open the Copy Transforms UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\simpleCopyUI.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "UI",
- "simple"
- ],
- "title": "# Simple Copy UI",
- "tooltip": "Open the simple Copy Transforms UI"
- }
- ]
- },
- {
- "type": "menu",
- "title": "# Optimize",
- "tooltip": "Optimization scripts",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py",
- "sourcetype": "file",
- "tags": ["animation", "hierarchy", "toggle", "freeze"],
- "title": "# Toggle Freeze Hierarchy",
- "tooltip": "Freeze and unfreeze hierarchy"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py",
- "sourcetype": "file",
- "tags": ["animation", "nucleus", "toggle", "parallel"],
- "title": "# Toggle Parallel Nucleus",
- "tooltip": "Toggle parallel nucleus"
- }
- ]
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py",
- "tags": ["animation", "bake", "selection", "worldspace.py"],
- "title": "# Bake Selected To Worldspace",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\timeStepper.py",
- "tags": ["animation", "time", "stepper"],
- "title": "# Time Stepper",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\capture_ui.py",
- "tags": [
- "animation",
- "capture",
- "ui",
- "screen",
- "movie",
- "image"
- ],
- "title": "# Capture UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\simplePlayblastUI.py",
- "tags": ["animation", "simple", "playblast", "ui"],
- "title": "# Simple Playblast UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\tweenMachineUI.py",
- "tags": ["animation", "tween", "machine"],
- "title": "# Tween Machine UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\selectAllAnimationCurves.py",
- "tags": ["animation", "select", "curves"],
- "title": "# Select All Animation Curves",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\pathAnimation.py",
- "tags": ["animation", "path", "along"],
- "title": "# Path Animation",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\offsetSelectedObjectsUI.py",
- "tags": ["animation", "offsetSelectedObjectsUI.py"],
- "title": "# Offset Selected Objects UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\key_amplifier_ui.py",
- "tags": ["animation", "key", "amplifier"],
- "title": "# Key Amplifier UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\anim_scene_optimizer.py",
- "tags": ["animation", "anim_scene_optimizer.py"],
- "title": "# Anim_Scene_Optimizer",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\zvParentMaster.py",
- "tags": ["animation", "zvParentMaster.py"],
- "title": "# ZV Parent Master",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\animLibrary.py",
- "tags": ["animation", "studiolibrary.py"],
- "title": "Anim Library",
- "type": "action"
- }
- ]
- },
- {
- "type": "menu",
- "title": "# Layout",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\alignDistributeUI.py",
- "sourcetype": "file",
- "tags": ["layout", "align", "Distribute", "UI"],
- "title": "# Align Distribute UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\alignSimpleUI.py",
- "sourcetype": "file",
- "tags": ["layout", "align", "UI", "Simple"],
- "title": "# Align Simple UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\center_locator.py",
- "sourcetype": "file",
- "tags": ["layout", "center", "locator"],
- "title": "# Center Locator",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\average_locator.py",
- "sourcetype": "file",
- "tags": ["layout", "average", "locator"],
- "title": "# Average Locator",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\selectWithinProximityUI.py",
- "sourcetype": "file",
- "tags": ["layout", "select", "proximity", "ui"],
- "title": "# Select Within Proximity UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\dupCurveUI.py",
- "sourcetype": "file",
- "tags": ["layout", "Duplicate", "Curve", "UI"],
- "title": "# Duplicate Curve UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\randomDeselectUI.py",
- "sourcetype": "file",
- "tags": ["layout", "random", "Deselect", "UI"],
- "title": "# Random Deselect UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\multiReferencerUI.py",
- "sourcetype": "file",
- "tags": ["layout", "multi", "reference"],
- "title": "# Multi Referencer UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\duplicateOffsetUI.py",
- "sourcetype": "file",
- "tags": ["layout", "duplicate", "offset", "UI"],
- "title": "# Duplicate Offset UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\spPaint3d.py",
- "sourcetype": "file",
- "tags": ["layout", "spPaint3d", "paint", "tool"],
- "title": "# SP Paint 3d",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\randomizeUI.py",
- "sourcetype": "file",
- "tags": ["layout", "randomize", "UI"],
- "title": "# Randomize UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\distributeWithinObjectUI.py",
- "sourcetype": "file",
- "tags": ["layout", "distribute", "ObjectUI", "within"],
- "title": "# Distribute Within Object UI",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "# Particles",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjects.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjects"],
- "title": "# Instancer To Objects",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstances.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjectsInstances"],
- "title": "# Instancer To Objects Instances",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstancesWithAnimation.py",
- "sourcetype": "file",
- "tags": [
- "particles",
- "instancerToObjectsInstancesWithAnimation"
- ],
- "title": "# Instancer To Objects Instances With Animation",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjectsWithAnimation"],
- "title": "# Instancer To Objects With Animation",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Cleanup",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\repair_faulty_containers.py",
- "sourcetype": "file",
- "tags": ["cleanup", "repair", "containers"],
- "title": "# Find and Repair Containers",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeNamespaces.py",
- "sourcetype": "file",
- "tags": ["cleanup", "remove", "namespaces"],
- "title": "# Remove Namespaces",
- "tooltip": "Remove all namespaces"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_user_defined_attributes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "remove_user_defined_attributes"],
- "title": "# Remove User Defined Attributes",
- "tooltip": "Remove all user-defined attributes from all nodes"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnknownNodes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnknownNodes"],
- "title": "# Remove Unknown Nodes",
- "tooltip": "Remove all unknown nodes"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnloadedReferences.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnloadedReferences"],
- "title": "# Remove Unloaded References",
- "tooltip": "Remove all unloaded references"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeReferencesFailedEdits.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeReferencesFailedEdits"],
- "title": "# Remove References Failed Edits",
- "tooltip": "Remove failed edits for all references"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_unused_looks.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnusedLooks"],
- "title": "# Remove Unused Looks",
- "tooltip": "Remove all loaded yet unused Avalon look containers"
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\uniqifyNodeNames.py",
- "sourcetype": "file",
- "tags": ["cleanup", "uniqifyNodeNames"],
- "title": "# Uniqify Node Names",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\autoRenameFileNodes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "auto", "rename", "filenodes"],
- "title": "# Auto Rename File Nodes",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\update_asset_id.py",
- "sourcetype": "file",
- "tags": ["cleanup", "update", "database", "asset", "id"],
- "title": "# Update Asset ID",
- "tooltip": "Will replace the Colorbleed ID with a new one (asset ID : Unique number)"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\ccRenameReplace.py",
- "sourcetype": "file",
- "tags": ["cleanup", "rename", "ui"],
- "title": "Renamer",
- "tooltip": "Rename UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\renameShapesToTransform.py",
- "sourcetype": "file",
- "tags": ["cleanup", "renameShapesToTransform"],
- "title": "# Rename Shapes To Transform",
- "tooltip": ""
- }
- ]
- }
-]
diff --git a/openpype/hosts/maya/api/menu_backup.json b/openpype/hosts/maya/api/menu_backup.json
deleted file mode 100644
index e2a558aedc..0000000000
--- a/openpype/hosts/maya/api/menu_backup.json
+++ /dev/null
@@ -1,1567 +0,0 @@
-[
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\save_scene_incremental.py",
- "sourcetype": "file",
- "title": "Version Up",
- "tooltip": "Incremental save with a specific format"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\show_current_scene_in_explorer.py",
- "sourcetype": "file",
- "title": "Explore current scene..",
- "tooltip": "Show current scene in Explorer"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\avalon\\launch_manager.py",
- "sourcetype": "file",
- "title": "Project Manager",
- "tooltip": "Add assets to the project"
- },
- {
- "type": "separator"
- },
- {
- "type": "menu",
- "title": "Modeling",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\duplicate_normalized.py",
- "sourcetype": "file",
- "tags": ["modeling", "duplicate", "normalized"],
- "title": "Duplicate Normalized",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\transferUVs.py",
- "sourcetype": "file",
- "tags": ["modeling", "transfer", "uv"],
- "title": "Transfer UVs",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\mirrorSymmetry.py",
- "sourcetype": "file",
- "tags": ["modeling", "mirror", "symmetry"],
- "title": "Mirror Symmetry",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\selectOutlineUI.py",
- "sourcetype": "file",
- "tags": ["modeling", "select", "outline", "ui"],
- "title": "Select Outline UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polyDeleteOtherUVSets.py",
- "sourcetype": "file",
- "tags": ["modeling", "polygon", "uvset", "delete"],
- "title": "Polygon Delete Other UV Sets",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polyCombineQuick.py",
- "sourcetype": "file",
- "tags": ["modeling", "combine", "polygon", "quick"],
- "title": "Polygon Combine Quick",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\separateMeshPerShader.py",
- "sourcetype": "file",
- "tags": ["modeling", "separateMeshPerShader"],
- "title": "Separate Mesh Per Shader",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polyDetachSeparate.py",
- "sourcetype": "file",
- "tags": ["modeling", "poly", "detach", "separate"],
- "title": "Polygon Detach and Separate",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polyRelaxVerts.py",
- "sourcetype": "file",
- "tags": ["modeling", "relax", "verts"],
- "title": "Polygon Relax Vertices",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\polySelectEveryNthEdgeUI.py",
- "sourcetype": "file",
- "tags": ["modeling", "select", "nth", "edge", "ui"],
- "title": "Select Every Nth Edge"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\modeling\\djPFXUVs.py",
- "sourcetype": "file",
- "tags": ["modeling", "djPFX", "UVs"],
- "title": "dj PFX UVs",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Rigging",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\addCurveBetween.py",
- "sourcetype": "file",
- "tags": ["rigging", "addCurveBetween", "file"],
- "title": "Add Curve Between"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\averageSkinWeights.py",
- "sourcetype": "file",
- "tags": ["rigging", "average", "skin weights", "file"],
- "title": "Average Skin Weights"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\cbSmoothSkinWeightUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "cbSmoothSkinWeightUI", "file"],
- "title": "CB Smooth Skin Weight UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\channelBoxManagerUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "channelBoxManagerUI", "file"],
- "title": "Channel Box Manager UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\characterAutorigger.py",
- "sourcetype": "file",
- "tags": ["rigging", "characterAutorigger", "file"],
- "title": "Character Auto Rigger"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\connectUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "connectUI", "file"],
- "title": "Connect UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\copySkinWeightsLocal.py",
- "sourcetype": "file",
- "tags": ["rigging", "copySkinWeightsLocal", "file"],
- "title": "Copy Skin Weights Local"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\createCenterLocator.py",
- "sourcetype": "file",
- "tags": ["rigging", "createCenterLocator", "file"],
- "title": "Create Center Locator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\freezeTransformToGroup.py",
- "sourcetype": "file",
- "tags": ["rigging", "freezeTransformToGroup", "file"],
- "title": "Freeze Transform To Group"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\groupSelected.py",
- "sourcetype": "file",
- "tags": ["rigging", "groupSelected", "file"],
- "title": "Group Selected"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\ikHandlePoleVectorLocator.py",
- "sourcetype": "file",
- "tags": ["rigging", "ikHandlePoleVectorLocator", "file"],
- "title": "IK Handle Pole Vector Locator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\jointOrientUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "jointOrientUI", "file"],
- "title": "Joint Orient UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\jointsOnCurve.py",
- "sourcetype": "file",
- "tags": ["rigging", "jointsOnCurve", "file"],
- "title": "Joints On Curve"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\resetBindSelectedSkinJoints.py",
- "sourcetype": "file",
- "tags": ["rigging", "resetBindSelectedSkinJoints", "file"],
- "title": "Reset Bind Selected Skin Joints"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedComponents.py",
- "sourcetype": "file",
- "tags": [
- "rigging",
- "selectSkinclusterJointsFromSelectedComponents",
- "file"
- ],
- "title": "Select Skincluster Joints From Selected Components"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\selectSkinclusterJointsFromSelectedMesh.py",
- "sourcetype": "file",
- "tags": [
- "rigging",
- "selectSkinclusterJointsFromSelectedMesh",
- "file"
- ],
- "title": "Select Skincluster Joints From Selected Mesh"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\setJointLabels.py",
- "sourcetype": "file",
- "tags": ["rigging", "setJointLabels", "file"],
- "title": "Set Joint Labels"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\setJointOrientationFromCurrentRotation.py",
- "sourcetype": "file",
- "tags": [
- "rigging",
- "setJointOrientationFromCurrentRotation",
- "file"
- ],
- "title": "Set Joint Orientation From Current Rotation"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\setSelectedJointsOrientationZero.py",
- "sourcetype": "file",
- "tags": ["rigging", "setSelectedJointsOrientationZero", "file"],
- "title": "Set Selected Joints Orientation Zero"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\mirrorCurveShape.py",
- "sourcetype": "file",
- "tags": ["rigging", "mirrorCurveShape", "file"],
- "title": "Mirror Curve Shape"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\setRotationOrderUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "setRotationOrderUI", "file"],
- "title": "Set Rotation Order UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\paintItNowUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "paintItNowUI", "file"],
- "title": "Paint It Now UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\parentScaleConstraint.py",
- "sourcetype": "file",
- "tags": ["rigging", "parentScaleConstraint", "file"],
- "title": "Parent Scale Constraint"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\quickSetWeightsUI.py",
- "sourcetype": "file",
- "tags": ["rigging", "quickSetWeightsUI", "file"],
- "title": "Quick Set Weights UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\rapidRig.py",
- "sourcetype": "file",
- "tags": ["rigging", "rapidRig", "file"],
- "title": "Rapid Rig"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\regenerate_blendshape_targets.py",
- "sourcetype": "file",
- "tags": ["rigging", "regenerate_blendshape_targets", "file"],
- "title": "Regenerate Blendshape Targets"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\removeRotationAxis.py",
- "sourcetype": "file",
- "tags": ["rigging", "removeRotationAxis", "file"],
- "title": "Remove Rotation Axis"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\resetBindSelectedMeshes.py",
- "sourcetype": "file",
- "tags": ["rigging", "resetBindSelectedMeshes", "file"],
- "title": "Reset Bind Selected Meshes"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\simpleControllerOnSelection.py",
- "sourcetype": "file",
- "tags": ["rigging", "simpleControllerOnSelection", "file"],
- "title": "Simple Controller On Selection"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\simpleControllerOnSelectionHierarchy.py",
- "sourcetype": "file",
- "tags": [
- "rigging",
- "simpleControllerOnSelectionHierarchy",
- "file"
- ],
- "title": "Simple Controller On Selection Hierarchy"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\superRelativeCluster.py",
- "sourcetype": "file",
- "tags": ["rigging", "superRelativeCluster", "file"],
- "title": "Super Relative Cluster"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\tfSmoothSkinWeight.py",
- "sourcetype": "file",
- "tags": ["rigging", "tfSmoothSkinWeight", "file"],
- "title": "TF Smooth Skin Weight"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleIntermediates.py",
- "sourcetype": "file",
- "tags": ["rigging", "toggleIntermediates", "file"],
- "title": "Toggle Intermediates"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleSegmentScaleCompensate.py",
- "sourcetype": "file",
- "tags": ["rigging", "toggleSegmentScaleCompensate", "file"],
- "title": "Toggle Segment Scale Compensate"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\rigging\\toggleSkinclusterDeformNormals.py",
- "sourcetype": "file",
- "tags": ["rigging", "toggleSkinclusterDeformNormals", "file"],
- "title": "Toggle Skincluster Deform Normals"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Shading",
- "items": [
- {
- "type": "menu",
- "title": "VRay",
- "items": [
- {
- "type": "action",
- "title": "Import Proxies",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayImportProxies.py",
- "sourcetype": "file",
- "tags": ["shading", "vray", "import", "proxies"],
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Select All GES",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGES.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "select All GES"]
- },
- {
- "type": "action",
- "title": "Select All GES Under Selection",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectAllGESUnderSelection.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "select", "all", "GES"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Selection To VRay Mesh",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\selectionToVrayMesh.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "selection", "vraymesh"]
- },
- {
- "type": "action",
- "title": "Add VRay Round Edges Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayRoundEdgesAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "round edges", "attribute"]
- },
- {
- "type": "action",
- "title": "Add Gamma",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayAddGamma.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "add gamma"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\select_vraymesh_materials_with_unconnected_shader_slots.py",
- "sourcetype": "file",
- "title": "Select Unconnected Shader Materials",
- "tags": [
- "shading",
- "vray",
- "select",
- "vraymesh",
- "materials",
- "unconnected shader slots"
- ],
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayMergeSimilarVRayMeshMaterials.py",
- "sourcetype": "file",
- "title": "Merge Similar VRay Mesh Materials",
- "tags": [
- "shading",
- "vray",
- "Merge",
- "VRayMesh",
- "Materials"
- ],
- "tooltip": ""
- },
- {
- "type": "action",
- "title": "Create Two Sided Material",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtlRenamed.py",
- "sourcetype": "file",
- "tooltip": "Creates two sided material for selected material and renames it",
- "tags": ["shading", "vray", "two sided", "material"]
- },
- {
- "type": "action",
- "title": "Create Two Sided Material For Selected",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayCreate2SidedMtlForSelectedMtl.py",
- "sourcetype": "file",
- "tooltip": "Select material to create a two sided version from it",
- "tags": [
- "shading",
- "vray",
- "Create2SidedMtlForSelectedMtl.py"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Add OpenSubdiv Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayOpenSubdivAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "add",
- "open subdiv",
- "attribute"
- ]
- },
- {
- "type": "action",
- "title": "Remove OpenSubdiv Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVrayOpenSubdivAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "remove",
- "opensubdiv",
- "attributee"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Add Subdivision Attribute",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVraySubdivisionAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "addVraySubdivisionAttribute"
- ]
- },
- {
- "type": "action",
- "title": "Remove Subdivision Attribute.py",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\removeVraySubdivisionAttribute.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": [
- "shading",
- "vray",
- "remove",
- "subdivision",
- "attribute"
- ]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Add Vray Object Ids",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayObjectIds.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "add", "object id"]
- },
- {
- "type": "action",
- "title": "Add Vray Material Ids",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\addVrayMaterialIds.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "addVrayMaterialIds.py"]
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "title": "Set Physical DOF Depth",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\vrayPhysicalDOFSetDepth.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "physical", "DOF ", "Depth"]
- },
- {
- "type": "action",
- "title": "Magic Vray Proxy UI",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vray\\magicVrayProxyUI.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "magicVrayProxyUI"]
- }
- ]
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\pyblish\\lighting\\set_filename_prefix.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "lookdev",
- "assign",
- "shaders",
- "prefix",
- "filename",
- "render"
- ],
- "title": "Set filename prefix",
- "tooltip": "Set the render file name prefix."
- },
- {
- "type": "action",
- "command": "import mayalookassigner; mayalookassigner.show()",
- "sourcetype": "python",
- "tags": ["shading", "look", "assign", "shaders", "auto"],
- "title": "Look Manager",
- "tooltip": "Open the Look Manager UI for look assignment"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\LightLinkUi.py",
- "sourcetype": "file",
- "tags": ["shading", "light", "link", "ui"],
- "title": "Light Link UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\vdviewer_ui.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "look",
- "vray",
- "displacement",
- "shaders",
- "auto"
- ],
- "title": "VRay Displ Viewer",
- "tooltip": "Open the VRay Displacement Viewer, select and control the content of the set"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\setTexturePreviewToCLRImage.py",
- "sourcetype": "file",
- "tags": ["shading", "CLRImage", "textures", "preview"],
- "title": "Set Texture Preview To CLRImage",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixDefaultShaderSetBehavior.py",
- "sourcetype": "file",
- "tags": ["shading", "fix", "DefaultShaderSet", "Behavior"],
- "title": "Fix Default Shader Set Behavior",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixSelectedShapesReferenceAssignments.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "fix",
- "Selected",
- "Shapes",
- "Reference",
- "Assignments"
- ],
- "title": "Fix Shapes Reference Assignments",
- "tooltip": "Select shapes to fix the reference assignments"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\selectLambert1Members.py",
- "sourcetype": "file",
- "tags": ["shading", "selectLambert1Members"],
- "title": "Select Lambert1 Members",
- "tooltip": "Selects all objects which have the Lambert1 shader assigned"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\selectShapesWithoutShader.py",
- "sourcetype": "file",
- "tags": ["shading", "selectShapesWithoutShader"],
- "title": "Select Shapes Without Shader",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fixRenderLayerOutAdjustmentErrors.py",
- "sourcetype": "file",
- "tags": ["shading", "fixRenderLayerOutAdjustmentErrors"],
- "title": "Fix RenderLayer Out Adjustment Errors",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\shading\\fix_renderlayer_missing_node_override.py",
- "sourcetype": "file",
- "tags": [
- "shading",
- "renderlayer",
- "missing",
- "reference",
- "switch",
- "layer"
- ],
- "title": "Fix RenderLayer Missing Referenced Nodes Overrides",
- "tooltip": ""
- },
- {
- "type": "action",
- "title": "Image 2 Tiled EXR",
- "command": "$OPENPYPE_SCRIPTS\\shading\\open_img2exr.py",
- "sourcetype": "file",
- "tooltip": "",
- "tags": ["shading", "vray", "exr"]
- }
- ]
- },
- {
- "type": "menu",
- "title": "Rendering",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\pyblish\\open_deadline_submission_settings.py",
- "sourcetype": "file",
- "tags": ["settings", "deadline", "globals", "render"],
- "title": "DL Submission Settings UI",
- "tooltip": "Open the Deadline Submission Settings UI"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Animation",
- "items": [
- {
- "type": "menu",
- "title": "Attributes",
- "tooltip": "",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyValues.py",
- "sourcetype": "file",
- "tags": ["animation", "copy", "attributes"],
- "title": "Copy Values",
- "tooltip": "Copy attribute values"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyInConnections.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "connections",
- "incoming"
- ],
- "title": "Copy In Connections",
- "tooltip": "Copy incoming connections"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyOutConnections.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "connections",
- "out"
- ],
- "title": "Copy Out Connections",
- "tooltip": "Copy outcoming connections"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformLocal.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "local"
- ],
- "title": "Copy Local Transforms",
- "tooltip": "Copy local transforms"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformMatrix.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "matrix"
- ],
- "title": "Copy Matrix Transforms",
- "tooltip": "Copy Matrix transforms"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\copyTransformUI.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "UI"
- ],
- "title": "Copy Transforms UI",
- "tooltip": "Open the Copy Transforms UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\attributes\\simpleCopyUI.py",
- "sourcetype": "file",
- "tags": [
- "animation",
- "copy",
- "attributes",
- "transforms",
- "UI",
- "simple"
- ],
- "title": "Simple Copy UI",
- "tooltip": "Open the simple Copy Transforms UI"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Optimize",
- "tooltip": "Optimization scripts",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleFreezeHierarchy.py",
- "sourcetype": "file",
- "tags": ["animation", "hierarchy", "toggle", "freeze"],
- "title": "Toggle Freeze Hierarchy",
- "tooltip": "Freeze and unfreeze hierarchy"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\animation\\optimize\\toggleParallelNucleus.py",
- "sourcetype": "file",
- "tags": ["animation", "nucleus", "toggle", "parallel"],
- "title": "Toggle Parallel Nucleus",
- "tooltip": "Toggle parallel nucleus"
- }
- ]
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\bakeSelectedToWorldSpace.py",
- "tags": ["animation", "bake", "selection", "worldspace.py"],
- "title": "Bake Selected To Worldspace",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\timeStepper.py",
- "tags": ["animation", "time", "stepper"],
- "title": "Time Stepper",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\capture_ui.py",
- "tags": [
- "animation",
- "capture",
- "ui",
- "screen",
- "movie",
- "image"
- ],
- "title": "Capture UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\simplePlayblastUI.py",
- "tags": ["animation", "simple", "playblast", "ui"],
- "title": "Simple Playblast UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\tweenMachineUI.py",
- "tags": ["animation", "tween", "machine"],
- "title": "Tween Machine UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\selectAllAnimationCurves.py",
- "tags": ["animation", "select", "curves"],
- "title": "Select All Animation Curves",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\pathAnimation.py",
- "tags": ["animation", "path", "along"],
- "title": "Path Animation",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\offsetSelectedObjectsUI.py",
- "tags": ["animation", "offsetSelectedObjectsUI.py"],
- "title": "Offset Selected Objects UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\key_amplifier_ui.py",
- "tags": ["animation", "key", "amplifier"],
- "title": "Key Amplifier UI",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\anim_scene_optimizer.py",
- "tags": ["animation", "anim_scene_optimizer.py"],
- "title": "Anim_Scene_Optimizer",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\zvParentMaster.py",
- "tags": ["animation", "zvParentMaster.py"],
- "title": "ZV Parent Master",
- "type": "action"
- },
- {
- "sourcetype": "file",
- "command": "$OPENPYPE_SCRIPTS\\animation\\poseLibrary.py",
- "tags": ["animation", "poseLibrary.py"],
- "title": "Pose Library",
- "type": "action"
- }
- ]
- },
- {
- "type": "menu",
- "title": "Layout",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\alignDistributeUI.py",
- "sourcetype": "file",
- "tags": ["layout", "align", "Distribute", "UI"],
- "title": "Align Distribute UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\alignSimpleUI.py",
- "sourcetype": "file",
- "tags": ["layout", "align", "UI", "Simple"],
- "title": "Align Simple UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\center_locator.py",
- "sourcetype": "file",
- "tags": ["layout", "center", "locator"],
- "title": "Center Locator",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\average_locator.py",
- "sourcetype": "file",
- "tags": ["layout", "average", "locator"],
- "title": "Average Locator",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\selectWithinProximityUI.py",
- "sourcetype": "file",
- "tags": ["layout", "select", "proximity", "ui"],
- "title": "Select Within Proximity UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\dupCurveUI.py",
- "sourcetype": "file",
- "tags": ["layout", "Duplicate", "Curve", "UI"],
- "title": "Duplicate Curve UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\randomDeselectUI.py",
- "sourcetype": "file",
- "tags": ["layout", "random", "Deselect", "UI"],
- "title": "Random Deselect UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\multiReferencerUI.py",
- "sourcetype": "file",
- "tags": ["layout", "multi", "reference"],
- "title": "Multi Referencer UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\duplicateOffsetUI.py",
- "sourcetype": "file",
- "tags": ["layout", "duplicate", "offset", "UI"],
- "title": "Duplicate Offset UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\spPaint3d.py",
- "sourcetype": "file",
- "tags": ["layout", "spPaint3d", "paint", "tool"],
- "title": "SP Paint 3d",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\randomizeUI.py",
- "sourcetype": "file",
- "tags": ["layout", "randomize", "UI"],
- "title": "Randomize UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\layout\\distributeWithinObjectUI.py",
- "sourcetype": "file",
- "tags": ["layout", "distribute", "ObjectUI", "within"],
- "title": "Distribute Within Object UI",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Particles",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjects.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjects"],
- "title": "Instancer To Objects",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstances.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjectsInstances"],
- "title": "Instancer To Objects Instances",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\objectsToParticlesAndInstancerCleanSource.py",
- "sourcetype": "file",
- "tags": [
- "particles",
- "objects",
- "Particles",
- "Instancer",
- "Clean",
- "Source"
- ],
- "title": "Objects To Particles & Instancer - Clean Source",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\particleComponentsToLocators.py",
- "sourcetype": "file",
- "tags": ["particles", "components", "locators"],
- "title": "Particle Components To Locators",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\objectsToParticlesAndInstancer.py",
- "sourcetype": "file",
- "tags": ["particles", "objects", "particles", "instancer"],
- "title": "Objects To Particles And Instancer",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\spawnParticlesOnMesh.py",
- "sourcetype": "file",
- "tags": ["particles", "spawn", "on", "mesh"],
- "title": "Spawn Particles On Mesh",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsInstancesWithAnimation.py",
- "sourcetype": "file",
- "tags": [
- "particles",
- "instancerToObjectsInstancesWithAnimation"
- ],
- "title": "Instancer To Objects Instances With Animation",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\objectsToParticles.py",
- "sourcetype": "file",
- "tags": ["particles", "objectsToParticles"],
- "title": "Objects To Particles",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\add_particle_cacheFile_attrs.py",
- "sourcetype": "file",
- "tags": ["particles", "add_particle_cacheFile_attrs"],
- "title": "Add Particle CacheFile Attributes",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\mergeParticleSystems.py",
- "sourcetype": "file",
- "tags": ["particles", "mergeParticleSystems"],
- "title": "Merge Particle Systems",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\particlesToLocators.py",
- "sourcetype": "file",
- "tags": ["particles", "particlesToLocators"],
- "title": "Particles To Locators",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\instancerToObjectsWithAnimation.py",
- "sourcetype": "file",
- "tags": ["particles", "instancerToObjectsWithAnimation"],
- "title": "Instancer To Objects With Animation",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\mayaReplicateHoudiniTool.py",
- "sourcetype": "file",
- "tags": [
- "particles",
- "houdini",
- "houdiniTool",
- "houdiniEngine"
- ],
- "title": "Replicate Houdini Tool",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\clearInitialState.py",
- "sourcetype": "file",
- "tags": ["particles", "clearInitialState"],
- "title": "Clear Initial State",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\particles\\killSelectedParticles.py",
- "sourcetype": "file",
- "tags": ["particles", "killSelectedParticles"],
- "title": "Kill Selected Particles",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Yeti",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\yeti\\yeti_rig_manager.py",
- "sourcetype": "file",
- "tags": ["yeti", "rig", "fur", "manager"],
- "title": "Open Yeti Rig Manager",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Cleanup",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\repair_faulty_containers.py",
- "sourcetype": "file",
- "tags": ["cleanup", "repair", "containers"],
- "title": "Find and Repair Containers",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectByType.py",
- "sourcetype": "file",
- "tags": ["cleanup", "selectByType"],
- "title": "Select By Type",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectIntermediateObjects.py",
- "sourcetype": "file",
- "tags": ["cleanup", "selectIntermediateObjects"],
- "title": "Select Intermediate Objects",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\selectNonUniqueNames.py",
- "sourcetype": "file",
- "tags": ["cleanup", "select", "non unique", "names"],
- "title": "Select Non Unique Names",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeNamespaces.py",
- "sourcetype": "file",
- "tags": ["cleanup", "remove", "namespaces"],
- "title": "Remove Namespaces",
- "tooltip": "Remove all namespaces"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_user_defined_attributes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "remove_user_defined_attributes"],
- "title": "Remove User Defined Attributes",
- "tooltip": "Remove all user-defined attributes from all nodes"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnknownNodes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnknownNodes"],
- "title": "Remove Unknown Nodes",
- "tooltip": "Remove all unknown nodes"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeUnloadedReferences.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnloadedReferences"],
- "title": "Remove Unloaded References",
- "tooltip": "Remove all unloaded references"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\removeReferencesFailedEdits.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeReferencesFailedEdits"],
- "title": "Remove References Failed Edits",
- "tooltip": "Remove failed edits for all references"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\remove_unused_looks.py",
- "sourcetype": "file",
- "tags": ["cleanup", "removeUnusedLooks"],
- "title": "Remove Unused Looks",
- "tooltip": "Remove all loaded yet unused Avalon look containers"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\deleteGhostIntermediateObjects.py",
- "sourcetype": "file",
- "tags": ["cleanup", "deleteGhostIntermediateObjects"],
- "title": "Delete Ghost Intermediate Objects",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\resetViewportCache.py",
- "sourcetype": "file",
- "tags": ["cleanup", "reset", "viewport", "cache"],
- "title": "Reset Viewport Cache",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\uniqifyNodeNames.py",
- "sourcetype": "file",
- "tags": ["cleanup", "uniqifyNodeNames"],
- "title": "Uniqify Node Names",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\autoRenameFileNodes.py",
- "sourcetype": "file",
- "tags": ["cleanup", "auto", "rename", "filenodes"],
- "title": "Auto Rename File Nodes",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\update_asset_id.py",
- "sourcetype": "file",
- "tags": ["cleanup", "update", "database", "asset", "id"],
- "title": "Update Asset ID",
- "tooltip": "Will replace the Colorbleed ID with a new one (asset ID : Unique number)"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\colorbleedRename.py",
- "sourcetype": "file",
- "tags": ["cleanup", "rename", "ui"],
- "title": "Colorbleed Renamer",
- "tooltip": "Colorbleed Rename UI"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\renameShapesToTransform.py",
- "sourcetype": "file",
- "tags": ["cleanup", "renameShapesToTransform"],
- "title": "Rename Shapes To Transform",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\reorderUI.py",
- "sourcetype": "file",
- "tags": ["cleanup", "reorderUI"],
- "title": "Reorder UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\cleanup\\pastedCleaner.py",
- "sourcetype": "file",
- "tags": ["cleanup", "pastedCleaner"],
- "title": "Pasted Cleaner",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Others",
- "items": [
- {
- "type": "menu",
- "sourcetype": "file",
- "title": "Yeti",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\yeti\\cache_selected_yeti_nodes.py",
- "sourcetype": "file",
- "tags": ["others", "yeti", "cache", "selected"],
- "title": "Cache Selected Yeti Nodes",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "title": "Hair",
- "tooltip": "",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\hair\\recolorHairCurrentCurve",
- "sourcetype": "file",
- "tags": ["others", "selectSoftSelection"],
- "title": "Select Soft Selection",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "menu",
- "command": "$OPENPYPE_SCRIPTS\\others\\display",
- "sourcetype": "file",
- "tags": ["others", "display"],
- "title": "Display",
- "items": [
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\display\\wireframeSelectedObjects.py",
- "sourcetype": "file",
- "tags": ["others", "wireframe", "selected", "objects"],
- "title": "Wireframe Selected Objects",
- "tooltip": ""
- }
- ]
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\archiveSceneUI.py",
- "sourcetype": "file",
- "tags": ["others", "archiveSceneUI"],
- "title": "Archive Scene UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\getSimilarMeshes.py",
- "sourcetype": "file",
- "tags": ["others", "getSimilarMeshes"],
- "title": "Get Similar Meshes",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\createBoundingBoxEachSelected.py",
- "sourcetype": "file",
- "tags": ["others", "createBoundingBoxEachSelected"],
- "title": "Create BoundingBox Each Selected",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\curveFromPositionEveryFrame.py",
- "sourcetype": "file",
- "tags": ["others", "curveFromPositionEveryFrame"],
- "title": "Curve From Position",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\instanceLeafSmartTransform.py",
- "sourcetype": "file",
- "tags": ["others", "instance", "leaf", "smart", "transform"],
- "title": "Instance Leaf Smart Transform",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\instanceSmartTransform.py",
- "sourcetype": "file",
- "tags": ["others", "instance", "smart", "transform"],
- "title": "Instance Smart Transform",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\randomizeUVShellsSelectedObjects.py",
- "sourcetype": "file",
- "tags": ["others", "randomizeUVShellsSelectedObjects"],
- "title": "Randomize UV Shells",
- "tooltip": "Select objects before running action"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\centerPivotGroup.py",
- "sourcetype": "file",
- "tags": ["others", "centerPivotGroup"],
- "title": "Center Pivot Group",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\locatorsOnSelectedFaces.py",
- "sourcetype": "file",
- "tags": ["others", "locatorsOnSelectedFaces"],
- "title": "Locators On Selected Faces",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\locatorsOnEdgeSelectionPrompt.py",
- "sourcetype": "file",
- "tags": ["others", "locatorsOnEdgeSelectionPrompt"],
- "title": "Locators On Edge Selection Prompt",
- "tooltip": ""
- },
- {
- "type": "separator"
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\copyDeformers.py",
- "sourcetype": "file",
- "tags": ["others", "copyDeformers"],
- "title": "Copy Deformers",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\selectInReferenceEditor.py",
- "sourcetype": "file",
- "tags": ["others", "selectInReferenceEditor"],
- "title": "Select In Reference Editor",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\selectConstrainingObject.py",
- "sourcetype": "file",
- "tags": ["others", "selectConstrainingObject"],
- "title": "Select Constraining Object",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\deformerSetRelationsUI.py",
- "sourcetype": "file",
- "tags": ["others", "deformerSetRelationsUI"],
- "title": "Deformer Set Relations UI",
- "tooltip": ""
- },
- {
- "type": "action",
- "command": "$OPENPYPE_SCRIPTS\\others\\recreateBaseNodesForAllLatticeNodes.py",
- "sourcetype": "file",
- "tags": ["others", "recreate", "base", "nodes", "lattice"],
- "title": "Recreate Base Nodes For Lattice Nodes",
- "tooltip": ""
- }
- ]
- }
-]
diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py
index 14e8f4eb45..1b3bb9feb3 100644
--- a/openpype/hosts/maya/api/pipeline.py
+++ b/openpype/hosts/maya/api/pipeline.py
@@ -184,76 +184,6 @@ def uninstall():
menu.uninstall()
-def lock():
- """Lock scene
-
- Add an invisible node to your Maya scene with the name of the
- current file, indicating that this file is "locked" and cannot
- be modified any further.
-
- """
-
- if not cmds.objExists("lock"):
- with lib.maintained_selection():
- cmds.createNode("objectSet", name="lock")
- cmds.addAttr("lock", ln="basename", dataType="string")
-
- # Permanently hide from outliner
- cmds.setAttr("lock.verticesOnlySet", True)
-
- fname = cmds.file(query=True, sceneName=True)
- basename = os.path.basename(fname)
- cmds.setAttr("lock.basename", basename, type="string")
-
-
-def unlock():
- """Permanently unlock a locked scene
-
- Doesn't throw an error if scene is already unlocked.
-
- """
-
- try:
- cmds.delete("lock")
- except ValueError:
- pass
-
-
-def is_locked():
- """Query whether current scene is locked"""
- fname = cmds.file(query=True, sceneName=True)
- basename = os.path.basename(fname)
-
- if self._ignore_lock:
- return False
-
- try:
- return cmds.getAttr("lock.basename") == basename
- except ValueError:
- return False
-
-
-@contextlib.contextmanager
-def lock_ignored():
- """Context manager for temporarily ignoring the lock of a scene
-
- The purpose of this function is to enable locking a scene and
- saving it with the lock still in place.
-
- Example:
- >>> with lock_ignored():
- ... pass # Do things without lock
-
- """
-
- self._ignore_lock = True
-
- try:
- yield
- finally:
- self._ignore_lock = False
-
-
def parse_container(container):
"""Return the container node's full container data.
diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py
index 97a190d57d..743ec26778 100644
--- a/openpype/hosts/maya/plugins/create/create_render.py
+++ b/openpype/hosts/maya/plugins/create/create_render.py
@@ -253,7 +253,7 @@ class CreateRender(plugin.Creator):
# get pools
pool_names = []
- self.server_aliases = self.deadline_servers.keys()
+ self.server_aliases = list(self.deadline_servers.keys())
self.data["deadlineServers"] = self.server_aliases
self.data["suspendPublishJob"] = False
self.data["review"] = True
@@ -286,15 +286,12 @@ class CreateRender(plugin.Creator):
raise RuntimeError("Both Deadline and Muster are enabled")
if deadline_enabled:
- # if default server is not between selected, use first one for
- # initial list of pools.
try:
deadline_url = self.deadline_servers["default"]
except KeyError:
- deadline_url = [
- self.deadline_servers[k]
- for k in self.deadline_servers.keys()
- ][0]
+ # if 'default' server is not between selected,
+ # use first one for initial list of pools.
+ deadline_url = next(iter(self.deadline_servers.values()))
pool_names = self._get_deadline_pools(deadline_url)
diff --git a/openpype/hosts/maya/plugins/load/actions.py b/openpype/hosts/maya/plugins/load/actions.py
index 1cc7ee0c03..1cb63c8a7a 100644
--- a/openpype/hosts/maya/plugins/load/actions.py
+++ b/openpype/hosts/maya/plugins/load/actions.py
@@ -72,9 +72,8 @@ class SetFrameRangeWithHandlesLoader(api.Loader):
return
# Include handles
- handles = version_data.get("handles", 0)
- start -= handles
- end += handles
+ start -= version_data.get("handleStart", 0)
+ end += version_data.get("handleEnd", 0)
cmds.playbackOptions(minTime=start,
maxTime=end,
diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py
index d39750e917..b6a76f1e21 100644
--- a/openpype/hosts/maya/plugins/publish/collect_look.py
+++ b/openpype/hosts/maya/plugins/publish/collect_look.py
@@ -320,7 +320,7 @@ class CollectLook(pyblish.api.InstancePlugin):
# Collect file nodes used by shading engines (if we have any)
files = []
- look_sets = sets.keys()
+ look_sets = list(sets.keys())
shader_attrs = [
"surfaceShader",
"volumeShader",
diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py
index 13ae1924b9..d99e81573b 100644
--- a/openpype/hosts/maya/plugins/publish/collect_render.py
+++ b/openpype/hosts/maya/plugins/publish/collect_render.py
@@ -234,13 +234,14 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
publish_meta_path = None
for aov in exp_files:
full_paths = []
- for file in aov[aov.keys()[0]]:
+ aov_first_key = list(aov.keys())[0]
+ for file in aov[aov_first_key]:
full_path = os.path.join(workspace, default_render_file,
file)
full_path = full_path.replace("\\", "/")
full_paths.append(full_path)
publish_meta_path = os.path.dirname(full_path)
- aov_dict[aov.keys()[0]] = full_paths
+ aov_dict[aov_first_key] = full_paths
frame_start_render = int(self.get_render_attribute(
"startFrame", layer=layer_name))
diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py
index 269972d996..8a8bd67cd8 100644
--- a/openpype/hosts/maya/plugins/publish/extract_animation.py
+++ b/openpype/hosts/maya/plugins/publish/extract_animation.py
@@ -38,12 +38,8 @@ class ExtractAnimation(openpype.api.Extractor):
fullPath=True) or []
# Collect the start and end including handles
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
- handles = instance.data.get("handles", 0) or 0
- if handles:
- start -= handles
- end += handles
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
self.log.info("Extracting animation..")
dirname = self.staging_dir(instance)
diff --git a/openpype/hosts/maya/plugins/publish/extract_ass.py b/openpype/hosts/maya/plugins/publish/extract_ass.py
index ab149de700..760f410f91 100644
--- a/openpype/hosts/maya/plugins/publish/extract_ass.py
+++ b/openpype/hosts/maya/plugins/publish/extract_ass.py
@@ -38,13 +38,9 @@ class ExtractAssStandin(openpype.api.Extractor):
self.log.info("Extracting ass sequence")
# Collect the start and end including handles
- start = instance.data.get("frameStart", 1)
- end = instance.data.get("frameEnd", 1)
- handles = instance.data.get("handles", 0)
+ start = instance.data.get("frameStartHandle", 1)
+ end = instance.data.get("frameEndHandle", 1)
step = instance.data.get("step", 0)
- if handles:
- start -= handles
- end += handles
exported_files = cmds.arnoldExportAss(filename=file_path,
selected=True,
diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
index 806a079940..5ad6b79d5c 100644
--- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
+++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py
@@ -21,17 +21,9 @@ class ExtractCameraAlembic(openpype.api.Extractor):
def process(self, instance):
- # get settings
- framerange = [instance.data.get("frameStart", 1),
- instance.data.get("frameEnd", 1)]
- handle_start = instance.data.get("handleStart", 0)
- handle_end = instance.data.get("handleEnd", 0)
-
- # TODO: deprecated attribute "handles"
-
- if handle_start is None:
- handle_start = instance.data.get("handles", 0)
- handle_end = instance.data.get("handles", 0)
+ # Collect the start and end including handles
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
step = instance.data.get("step", 1.0)
bake_to_worldspace = instance.data("bakeToWorldSpace", True)
@@ -61,10 +53,7 @@ class ExtractCameraAlembic(openpype.api.Extractor):
job_str = ' -selection -dataFormat "ogawa" '
job_str += ' -attrPrefix cb'
- job_str += ' -frameRange {0} {1} '.format(framerange[0]
- - handle_start,
- framerange[1]
- + handle_end)
+ job_str += ' -frameRange {0} {1} '.format(start, end)
job_str += ' -step {0} '.format(step)
if bake_to_worldspace:
diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
index 9d25b147de..49c156f9cd 100644
--- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
+++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py
@@ -43,7 +43,8 @@ def grouper(iterable, n, fillvalue=None):
"""
args = [iter(iterable)] * n
- return itertools.izip_longest(fillvalue=fillvalue, *args)
+ from six.moves import zip_longest
+ return zip_longest(fillvalue=fillvalue, *args)
def unlock(plug):
@@ -117,19 +118,9 @@ class ExtractCameraMayaScene(openpype.api.Extractor):
# no preset found
pass
- framerange = [instance.data.get("frameStart", 1),
- instance.data.get("frameEnd", 1)]
- handle_start = instance.data.get("handleStart", 0)
- handle_end = instance.data.get("handleEnd", 0)
-
- # TODO: deprecated attribute "handles"
-
- if handle_start is None:
- handle_start = instance.data.get("handles", 0)
- handle_end = instance.data.get("handles", 0)
-
- range_with_handles = [framerange[0] - handle_start,
- framerange[1] + handle_end]
+ # Collect the start and end including handles
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
step = instance.data.get("step", 1.0)
bake_to_worldspace = instance.data("bakeToWorldSpace", True)
@@ -164,7 +155,7 @@ class ExtractCameraMayaScene(openpype.api.Extractor):
"Performing camera bakes: {}".format(transform))
baked = lib.bake_to_world_space(
transform,
- frame_range=range_with_handles,
+ frame_range=[start, end],
step=step
)
baked_shapes = cmds.ls(baked,
diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx.py b/openpype/hosts/maya/plugins/publish/extract_fbx.py
index 844084b9ab..a2adcb3091 100644
--- a/openpype/hosts/maya/plugins/publish/extract_fbx.py
+++ b/openpype/hosts/maya/plugins/publish/extract_fbx.py
@@ -168,12 +168,8 @@ class ExtractFBX(openpype.api.Extractor):
self.log.info("Export options: {0}".format(options))
# Collect the start and end including handles
- start = instance.data["frameStart"]
- end = instance.data["frameEnd"]
- handles = instance.data.get("handles", 0)
- if handles:
- start -= handles
- end += handles
+ start = instance.data["frameStartHandle"]
+ end = instance.data["frameEndHandle"]
options['bakeComplexStart'] = start
options['bakeComplexEnd'] = end
diff --git a/openpype/hosts/maya/plugins/publish/extract_look.py b/openpype/hosts/maya/plugins/publish/extract_look.py
index fe89038a24..a9a2a7b60c 100644
--- a/openpype/hosts/maya/plugins/publish/extract_look.py
+++ b/openpype/hosts/maya/plugins/publish/extract_look.py
@@ -4,6 +4,7 @@ import os
import sys
import json
import tempfile
+import platform
import contextlib
import subprocess
from collections import OrderedDict
@@ -62,6 +63,11 @@ def maketx(source, destination, *args):
from openpype.lib import get_oiio_tools_path
maketx_path = get_oiio_tools_path("maketx")
+
+ if platform.system().lower() == "windows":
+ # Ensure .exe extension
+ maketx_path += ".exe"
+
if not os.path.exists(maketx_path):
print(
"OIIO tool not found in {}".format(maketx_path))
@@ -216,7 +222,7 @@ class ExtractLook(openpype.api.Extractor):
self.log.info("Extract sets (%s) ..." % _scene_type)
lookdata = instance.data["lookData"]
relationships = lookdata["relationships"]
- sets = relationships.keys()
+ sets = list(relationships.keys())
if not sets:
self.log.info("No sets found")
return
diff --git a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
index 615bc27878..562ca078e1 100644
--- a/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
+++ b/openpype/hosts/maya/plugins/publish/extract_vrayproxy.py
@@ -28,14 +28,19 @@ class ExtractVRayProxy(openpype.api.Extractor):
if not anim_on:
# Remove animation information because it is not required for
# non-animated subsets
- instance.data.pop("frameStart", None)
- instance.data.pop("frameEnd", None)
+ keys = ["frameStart", "frameEnd",
+ "handleStart", "handleEnd",
+ "frameStartHandle", "frameEndHandle",
+ # Backwards compatibility
+ "handles"]
+ for key in keys:
+ instance.data.pop(key, None)
start_frame = 1
end_frame = 1
else:
- start_frame = instance.data["frameStart"]
- end_frame = instance.data["frameEnd"]
+ start_frame = instance.data["frameStartHandle"]
+ end_frame = instance.data["frameEndHandle"]
vertex_colors = instance.data.get("vertexColors", False)
diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py
index 05fe79ecc5..0d85708789 100644
--- a/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py
+++ b/openpype/hosts/maya/plugins/publish/extract_yeti_cache.py
@@ -29,8 +29,8 @@ class ExtractYetiCache(openpype.api.Extractor):
data_file = os.path.join(dirname, "yeti.fursettings")
# Collect information for writing cache
- start_frame = instance.data.get("frameStart")
- end_frame = instance.data.get("frameEnd")
+ start_frame = instance.data.get("frameStartHandle")
+ end_frame = instance.data.get("frameEndHandle")
preroll = instance.data.get("preroll")
if preroll > 0:
start_frame -= preroll
diff --git a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
index 3625d4ab32..5fb9bd98b1 100644
--- a/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
+++ b/openpype/hosts/maya/plugins/publish/validate_ass_relative_paths.py
@@ -110,9 +110,9 @@ class ValidateAssRelativePaths(pyblish.api.InstancePlugin):
Maya API will return a list of values, which need to be properly
handled to evaluate properly.
"""
- if isinstance(attr_val, types.BooleanType):
+ if isinstance(attr_val, bool):
return attr_val
- elif isinstance(attr_val, (types.ListType, types.GeneratorType)):
+ elif isinstance(attr_val, (list, types.GeneratorType)):
return any(attr_val)
else:
return bool(attr_val)
diff --git a/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py b/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py
index 5ce422239d..bf95d8ba09 100644
--- a/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py
+++ b/openpype/hosts/maya/plugins/publish/validate_mesh_overlapping_uvs.py
@@ -5,6 +5,8 @@ import math
import maya.api.OpenMaya as om
import pymel.core as pm
+from six.moves import xrange
+
class GetOverlappingUVs(object):
diff --git a/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py b/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
index 6cfbd4049b..7a48c29b7d 100644
--- a/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
+++ b/openpype/hosts/maya/plugins/publish/validate_vray_referenced_aovs.py
@@ -82,9 +82,9 @@ class ValidateVrayReferencedAOVs(pyblish.api.InstancePlugin):
bool: cast Maya attribute to Pythons boolean value.
"""
- if isinstance(attr_val, types.BooleanType):
+ if isinstance(attr_val, bool):
return attr_val
- elif isinstance(attr_val, (types.ListType, types.GeneratorType)):
+ elif isinstance(attr_val, (list, types.GeneratorType)):
return any(attr_val)
else:
return bool(attr_val)
diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py
index 16a1d23244..112cd8fe3f 100644
--- a/openpype/hosts/photoshop/api/launch_logic.py
+++ b/openpype/hosts/photoshop/api/launch_logic.py
@@ -175,7 +175,7 @@ class ProcessLauncher(QtCore.QObject):
def start(self):
if self._started:
return
- self.log.info("Started launch logic of AfterEffects")
+ self.log.info("Started launch logic of Photoshop")
self._started = True
self._start_process_timer.start()
diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py
index fd8377d4e0..d4406d17b9 100644
--- a/openpype/hosts/photoshop/api/ws_stub.py
+++ b/openpype/hosts/photoshop/api/ws_stub.py
@@ -344,6 +344,28 @@ class PhotoshopServerStub:
)
)
+ def hide_all_others_layers(self, layers):
+ """hides all layers that are not part of the list or that are not
+ children of this list
+
+ Args:
+ layers (list): list of PSItem - highest hierarchy
+ """
+ extract_ids = set([ll.id for ll in self.get_layers_in_layers(layers)])
+
+ self.hide_all_others_layers_ids(extract_ids)
+
+ def hide_all_others_layers_ids(self, extract_ids):
+ """hides all layers that are not part of the list or that are not
+ children of this list
+
+ Args:
+ extract_ids (list): list of integer that should be visible
+ """
+ for layer in self.get_layers():
+ if layer.visible and layer.id not in extract_ids:
+ self.set_visible(layer.id, False)
+
def get_layers_metadata(self):
"""Reads layers metadata from Headline from active document in PS.
(Headline accessible by File > File Info)
diff --git a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py
index c1ae88fbbb..7d44d55a80 100644
--- a/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py
+++ b/openpype/hosts/photoshop/plugins/publish/collect_color_coded_instances.py
@@ -38,10 +38,15 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
def process(self, context):
self.log.info("CollectColorCodedInstances")
- self.log.debug("mapping:: {}".format(self.color_code_mapping))
+ batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")
+ if (os.environ.get("IS_TEST") and
+ (not batch_dir or not os.path.exists(batch_dir))):
+ self.log.debug("Automatic testing, no batch data, skipping")
+ return
existing_subset_names = self._get_existing_subset_names(context)
- asset_name, task_name, variant = self._parse_batch()
+
+ asset_name, task_name, variant = self._parse_batch(batch_dir)
stub = photoshop.stub()
layers = stub.get_layers()
@@ -125,9 +130,8 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin):
return existing_subset_names
- def _parse_batch(self):
+ def _parse_batch(self, batch_dir):
"""Parses asset_name, task_name, variant from batch manifest."""
- batch_dir = os.environ.get("OPENPYPE_PUBLISH_DATA")
task_data = None
if batch_dir and os.path.exists(batch_dir):
task_data = parse_json(os.path.join(batch_dir,
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py
index beb904215b..04ce77ee34 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_image.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py
@@ -26,7 +26,6 @@ class ExtractImage(openpype.api.Extractor):
with photoshop.maintained_selection():
self.log.info("Extracting %s" % str(list(instance)))
with photoshop.maintained_visibility():
- # Hide all other layers.
layer = instance.data.get("layer")
ids = set([layer.id])
add_ids = instance.data.pop("ids", None)
@@ -34,11 +33,7 @@ class ExtractImage(openpype.api.Extractor):
ids.update(set(add_ids))
extract_ids = set([ll.id for ll in stub.
get_layers_in_layers_ids(ids)])
-
- for layer in stub.get_layers():
- # limit unnecessary calls to client
- if layer.visible and layer.id not in extract_ids:
- stub.set_visible(layer.id, False)
+ stub.hide_all_others_layers_ids(extract_ids)
file_basename = os.path.splitext(
stub.get_active_document_name()
diff --git a/openpype/hosts/photoshop/plugins/publish/extract_review.py b/openpype/hosts/photoshop/plugins/publish/extract_review.py
index b6c7e2d189..b8f4470c7b 100644
--- a/openpype/hosts/photoshop/plugins/publish/extract_review.py
+++ b/openpype/hosts/photoshop/plugins/publish/extract_review.py
@@ -1,4 +1,5 @@
import os
+import shutil
import openpype.api
import openpype.lib
@@ -7,7 +8,7 @@ from openpype.hosts.photoshop import api as photoshop
class ExtractReview(openpype.api.Extractor):
"""
- Produce a flattened image file from all 'image' instances.
+ Produce a flattened or sequence image file from all 'image' instances.
If no 'image' instance is created, it produces flattened image from
all visible layers.
@@ -20,54 +21,58 @@ class ExtractReview(openpype.api.Extractor):
# Extract Options
jpg_options = None
mov_options = None
+ make_image_sequence = None
def process(self, instance):
staging_dir = self.staging_dir(instance)
self.log.info("Outputting image to {}".format(staging_dir))
+ fps = instance.data.get("fps", 25)
stub = photoshop.stub()
+ self.output_seq_filename = os.path.splitext(
+ stub.get_active_document_name())[0] + ".%04d.jpg"
- layers = []
- for image_instance in instance.context:
- if image_instance.data["family"] != "image":
- continue
- layers.append(image_instance.data.get("layer"))
+ layers = self._get_layers_from_image_instances(instance)
+ self.log.info("Layers image instance found: {}".format(layers))
- # Perform extraction
- output_image = "{}.jpg".format(
- os.path.splitext(stub.get_active_document_name())[0]
- )
- output_image_path = os.path.join(staging_dir, output_image)
- with photoshop.maintained_visibility():
- if layers:
- # Hide all other layers.
- extract_ids = set([ll.id for ll in stub.
- get_layers_in_layers(layers)])
- self.log.debug("extract_ids {}".format(extract_ids))
- for layer in stub.get_layers():
- # limit unnecessary calls to client
- if layer.visible and layer.id not in extract_ids:
- stub.set_visible(layer.id, False)
+ if self.make_image_sequence and len(layers) > 1:
+ self.log.info("Extract layers to image sequence.")
+ img_list = self._saves_sequences_layers(staging_dir, layers)
- stub.saveAs(output_image_path, 'jpg', True)
+ instance.data["representations"].append({
+ "name": "jpg",
+ "ext": "jpg",
+ "files": img_list,
+ "frameStart": 0,
+ "frameEnd": len(img_list),
+ "fps": fps,
+ "stagingDir": staging_dir,
+ "tags": self.jpg_options['tags'],
+ })
+
+ else:
+ self.log.info("Extract layers to flatten image.")
+ img_list = self._saves_flattened_layers(staging_dir, layers)
+
+ instance.data["representations"].append({
+ "name": "jpg",
+ "ext": "jpg",
+ "files": img_list,
+ "stagingDir": staging_dir,
+ "tags": self.jpg_options['tags']
+ })
ffmpeg_path = openpype.lib.get_ffmpeg_tool_path("ffmpeg")
- instance.data["representations"].append({
- "name": "jpg",
- "ext": "jpg",
- "files": output_image,
- "stagingDir": staging_dir,
- "tags": self.jpg_options['tags']
- })
instance.data["stagingDir"] = staging_dir
# Generate thumbnail.
thumbnail_path = os.path.join(staging_dir, "thumbnail.jpg")
+ self.log.info(f"Generate thumbnail {thumbnail_path}")
args = [
ffmpeg_path,
"-y",
- "-i", output_image_path,
+ "-i", os.path.join(staging_dir, self.output_seq_filename),
"-vf", "scale=300:-1",
"-vframes", "1",
thumbnail_path
@@ -81,14 +86,17 @@ class ExtractReview(openpype.api.Extractor):
"stagingDir": staging_dir,
"tags": ["thumbnail"]
})
+
# Generate mov.
mov_path = os.path.join(staging_dir, "review.mov")
+ self.log.info(f"Generate mov review: {mov_path}")
+ img_number = len(img_list)
args = [
ffmpeg_path,
"-y",
- "-i", output_image_path,
+ "-i", os.path.join(staging_dir, self.output_seq_filename),
"-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2",
- "-vframes", "1",
+ "-vframes", str(img_number),
mov_path
]
output = openpype.lib.run_subprocess(args)
@@ -99,15 +107,86 @@ class ExtractReview(openpype.api.Extractor):
"files": os.path.basename(mov_path),
"stagingDir": staging_dir,
"frameStart": 1,
- "frameEnd": 1,
- "fps": 25,
+ "frameEnd": img_number,
+ "fps": fps,
"preview": True,
"tags": self.mov_options['tags']
})
# Required for extract_review plugin (L222 onwards).
instance.data["frameStart"] = 1
- instance.data["frameEnd"] = 1
+ instance.data["frameEnd"] = img_number
instance.data["fps"] = 25
self.log.info(f"Extracted {instance} to {staging_dir}")
+
+ def _get_image_path_from_instances(self, instance):
+ img_list = []
+
+ for instance in sorted(instance.context):
+ if instance.data["family"] != "image":
+ continue
+
+ for rep in instance.data["representations"]:
+ img_path = os.path.join(
+ rep["stagingDir"],
+ rep["files"]
+ )
+ img_list.append(img_path)
+
+ return img_list
+
+ def _copy_image_to_staging_dir(self, staging_dir, img_list):
+ copy_files = []
+ for i, img_src in enumerate(img_list):
+ img_filename = self.output_seq_filename % i
+ img_dst = os.path.join(staging_dir, img_filename)
+
+ self.log.debug(
+ "Copying file .. {} -> {}".format(img_src, img_dst)
+ )
+ shutil.copy(img_src, img_dst)
+ copy_files.append(img_filename)
+
+ return copy_files
+
+ def _get_layers_from_image_instances(self, instance):
+ layers = []
+ for image_instance in instance.context:
+ if image_instance.data["family"] != "image":
+ continue
+ layers.append(image_instance.data.get("layer"))
+
+ return sorted(layers)
+
+ def _saves_flattened_layers(self, staging_dir, layers):
+ img_filename = self.output_seq_filename % 0
+ output_image_path = os.path.join(staging_dir, img_filename)
+ stub = photoshop.stub()
+
+ with photoshop.maintained_visibility():
+ self.log.info("Extracting {}".format(layers))
+ if layers:
+ stub.hide_all_others_layers(layers)
+
+ stub.saveAs(output_image_path, 'jpg', True)
+
+ return img_filename
+
+ def _saves_sequences_layers(self, staging_dir, layers):
+ stub = photoshop.stub()
+
+ list_img_filename = []
+ with photoshop.maintained_visibility():
+ for i, layer in enumerate(layers):
+ self.log.info("Extracting {}".format(layer))
+
+ img_filename = self.output_seq_filename % i
+ output_image_path = os.path.join(staging_dir, img_filename)
+ list_img_filename.append(img_filename)
+
+ with photoshop.maintained_visibility():
+ stub.hide_all_others_layers([layer])
+ stub.saveAs(output_image_path, 'jpg', True)
+
+ return list_img_filename
diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py
index 3dee17cb01..9b3762f328 100644
--- a/openpype/hosts/resolve/api/utils.py
+++ b/openpype/hosts/resolve/api/utils.py
@@ -70,9 +70,9 @@ def get_resolve_module():
sys.exit()
# assign global var and return
bmdvr = bmd.scriptapp("Resolve")
- # bmdvf = bmd.scriptapp("Fusion")
+ bmdvf = bmd.scriptapp("Fusion")
resolve.api.bmdvr = bmdvr
- resolve.api.bmdvf = bmdvr.Fusion()
+ resolve.api.bmdvf = bmdvf
log.info(("Assigning resolve module to "
f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}"))
log.info(("Assigning resolve module to "
diff --git a/openpype/hosts/testhost/api/instances.json b/openpype/hosts/testhost/api/instances.json
index 84021eff91..d955012514 100644
--- a/openpype/hosts/testhost/api/instances.json
+++ b/openpype/hosts/testhost/api/instances.json
@@ -8,7 +8,7 @@
"asset": "sq01_sh0010",
"task": "Compositing",
"variant": "myVariant",
- "uuid": "a485f148-9121-46a5-8157-aa64df0fb449",
+ "instance_id": "a485f148-9121-46a5-8157-aa64df0fb449",
"creator_attributes": {
"number_key": 10,
"ha": 10
@@ -29,8 +29,8 @@
"asset": "sq01_sh0010",
"task": "Compositing",
"variant": "myVariant2",
- "uuid": "a485f148-9121-46a5-8157-aa64df0fb444",
"creator_attributes": {},
+ "instance_id": "a485f148-9121-46a5-8157-aa64df0fb444",
"publish_attributes": {
"CollectFtrackApi": {
"add_ftrack_family": true
@@ -47,8 +47,8 @@
"asset": "sq01_sh0010",
"task": "Compositing",
"variant": "Main",
- "uuid": "3607bc95-75f6-4648-a58d-e699f413d09f",
"creator_attributes": {},
+ "instance_id": "3607bc95-75f6-4648-a58d-e699f413d09f",
"publish_attributes": {
"CollectFtrackApi": {
"add_ftrack_family": true
@@ -65,7 +65,7 @@
"asset": "sq01_sh0020",
"task": "Compositing",
"variant": "Main2",
- "uuid": "4ccf56f6-9982-4837-967c-a49695dbe8eb",
+ "instance_id": "4ccf56f6-9982-4837-967c-a49695dbe8eb",
"creator_attributes": {},
"publish_attributes": {
"CollectFtrackApi": {
@@ -83,7 +83,7 @@
"asset": "sq01_sh0020",
"task": "Compositing",
"variant": "Main2",
- "uuid": "4ccf56f6-9982-4837-967c-a49695dbe8ec",
+ "instance_id": "4ccf56f6-9982-4837-967c-a49695dbe8ec",
"creator_attributes": {},
"publish_attributes": {
"CollectFtrackApi": {
@@ -101,7 +101,7 @@
"asset": "Alpaca_01",
"task": "modeling",
"variant": "Main",
- "uuid": "7c9ddfc7-9f9c-4c1c-b233-38c966735fb6",
+ "instance_id": "7c9ddfc7-9f9c-4c1c-b233-38c966735fb6",
"creator_attributes": {},
"publish_attributes": {}
}
diff --git a/openpype/hosts/testhost/api/pipeline.py b/openpype/hosts/testhost/api/pipeline.py
index 49f1d3f33d..1f5d680705 100644
--- a/openpype/hosts/testhost/api/pipeline.py
+++ b/openpype/hosts/testhost/api/pipeline.py
@@ -114,7 +114,7 @@ def update_instances(update_list):
instances = HostContext.get_instances()
for instance_data in instances:
- instance_id = instance_data["uuid"]
+ instance_id = instance_data["instance_id"]
if instance_id in updated_instances:
new_instance_data = updated_instances[instance_id]
old_keys = set(instance_data.keys())
@@ -132,10 +132,10 @@ def remove_instances(instances):
current_instances = HostContext.get_instances()
for instance in instances:
- instance_id = instance.data["uuid"]
+ instance_id = instance.data["instance_id"]
found_idx = None
for idx, _instance in enumerate(current_instances):
- if instance_id == _instance["uuid"]:
+ if instance_id == _instance["instance_id"]:
found_idx = idx
break
diff --git a/openpype/hosts/traypublisher/api/__init__.py b/openpype/hosts/traypublisher/api/__init__.py
new file mode 100644
index 0000000000..c461c0c526
--- /dev/null
+++ b/openpype/hosts/traypublisher/api/__init__.py
@@ -0,0 +1,20 @@
+from .pipeline import (
+ install,
+ ls,
+
+ set_project_name,
+ get_context_title,
+ get_context_data,
+ update_context_data,
+)
+
+
+__all__ = (
+ "install",
+ "ls",
+
+ "set_project_name",
+ "get_context_title",
+ "get_context_data",
+ "update_context_data",
+)
diff --git a/openpype/hosts/traypublisher/api/pipeline.py b/openpype/hosts/traypublisher/api/pipeline.py
new file mode 100644
index 0000000000..a39e5641ae
--- /dev/null
+++ b/openpype/hosts/traypublisher/api/pipeline.py
@@ -0,0 +1,180 @@
+import os
+import json
+import tempfile
+import atexit
+
+from avalon import io
+import avalon.api
+import pyblish.api
+
+from openpype.pipeline import BaseCreator
+
+ROOT_DIR = os.path.dirname(os.path.dirname(
+ os.path.abspath(__file__)
+))
+PUBLISH_PATH = os.path.join(ROOT_DIR, "plugins", "publish")
+CREATE_PATH = os.path.join(ROOT_DIR, "plugins", "create")
+
+
+class HostContext:
+ _context_json_path = None
+
+ @staticmethod
+ def _on_exit():
+ if (
+ HostContext._context_json_path
+ and os.path.exists(HostContext._context_json_path)
+ ):
+ os.remove(HostContext._context_json_path)
+
+ @classmethod
+ def get_context_json_path(cls):
+ if cls._context_json_path is None:
+ output_file = tempfile.NamedTemporaryFile(
+ mode="w", prefix="traypub_", suffix=".json"
+ )
+ output_file.close()
+ cls._context_json_path = output_file.name
+ atexit.register(HostContext._on_exit)
+ print(cls._context_json_path)
+ return cls._context_json_path
+
+ @classmethod
+ def _get_data(cls, group=None):
+ json_path = cls.get_context_json_path()
+ data = {}
+ if not os.path.exists(json_path):
+ with open(json_path, "w") as json_stream:
+ json.dump(data, json_stream)
+ else:
+ with open(json_path, "r") as json_stream:
+ content = json_stream.read()
+ if content:
+ data = json.loads(content)
+ if group is None:
+ return data
+ return data.get(group)
+
+ @classmethod
+ def _save_data(cls, group, new_data):
+ json_path = cls.get_context_json_path()
+ data = cls._get_data()
+ data[group] = new_data
+ with open(json_path, "w") as json_stream:
+ json.dump(data, json_stream)
+
+ @classmethod
+ def add_instance(cls, instance):
+ instances = cls.get_instances()
+ instances.append(instance)
+ cls.save_instances(instances)
+
+ @classmethod
+ def get_instances(cls):
+ return cls._get_data("instances") or []
+
+ @classmethod
+ def save_instances(cls, instances):
+ cls._save_data("instances", instances)
+
+ @classmethod
+ def get_context_data(cls):
+ return cls._get_data("context") or {}
+
+ @classmethod
+ def save_context_data(cls, data):
+ cls._save_data("context", data)
+
+ @classmethod
+ def get_project_name(cls):
+ return cls._get_data("project_name")
+
+ @classmethod
+ def set_project_name(cls, project_name):
+ cls._save_data("project_name", project_name)
+
+ @classmethod
+ def get_data_to_store(cls):
+ return {
+ "project_name": cls.get_project_name(),
+ "instances": cls.get_instances(),
+ "context": cls.get_context_data(),
+ }
+
+
+def list_instances():
+ return HostContext.get_instances()
+
+
+def update_instances(update_list):
+ updated_instances = {}
+ for instance, _changes in update_list:
+ updated_instances[instance.id] = instance.data_to_store()
+
+ instances = HostContext.get_instances()
+ for instance_data in instances:
+ instance_id = instance_data["instance_id"]
+ if instance_id in updated_instances:
+ new_instance_data = updated_instances[instance_id]
+ old_keys = set(instance_data.keys())
+ new_keys = set(new_instance_data.keys())
+ instance_data.update(new_instance_data)
+ for key in (old_keys - new_keys):
+ instance_data.pop(key)
+
+ HostContext.save_instances(instances)
+
+
+def remove_instances(instances):
+ if not isinstance(instances, (tuple, list)):
+ instances = [instances]
+
+ current_instances = HostContext.get_instances()
+ for instance in instances:
+ instance_id = instance.data["instance_id"]
+ found_idx = None
+ for idx, _instance in enumerate(current_instances):
+ if instance_id == _instance["instance_id"]:
+ found_idx = idx
+ break
+
+ if found_idx is not None:
+ current_instances.pop(found_idx)
+ HostContext.save_instances(current_instances)
+
+
+def get_context_data():
+ return HostContext.get_context_data()
+
+
+def update_context_data(data, changes):
+ HostContext.save_context_data(data)
+
+
+def get_context_title():
+ return HostContext.get_project_name()
+
+
+def ls():
+ """Probably will never return loaded containers."""
+ return []
+
+
+def install():
+ """This is called before a project is known.
+
+ Project is defined with 'set_project_name'.
+ """
+ os.environ["AVALON_APP"] = "traypublisher"
+
+ pyblish.api.register_host("traypublisher")
+ pyblish.api.register_plugin_path(PUBLISH_PATH)
+ avalon.api.register_plugin_path(BaseCreator, CREATE_PATH)
+
+
+def set_project_name(project_name):
+ # TODO Deregister project specific plugins and register new project plugins
+ os.environ["AVALON_PROJECT"] = project_name
+ avalon.api.Session["AVALON_PROJECT"] = project_name
+ io.install()
+ HostContext.set_project_name(project_name)
diff --git a/openpype/hosts/traypublisher/plugins/create/create_workfile.py b/openpype/hosts/traypublisher/plugins/create/create_workfile.py
new file mode 100644
index 0000000000..2db4770bbc
--- /dev/null
+++ b/openpype/hosts/traypublisher/plugins/create/create_workfile.py
@@ -0,0 +1,97 @@
+from openpype.hosts.traypublisher.api import pipeline
+from openpype.pipeline import (
+ Creator,
+ CreatedInstance,
+ lib
+)
+
+
+class WorkfileCreator(Creator):
+ identifier = "workfile"
+ label = "Workfile"
+ family = "workfile"
+ description = "Publish backup of workfile"
+
+ create_allow_context_change = True
+
+ extensions = [
+ # Maya
+ ".ma", ".mb",
+ # Nuke
+ ".nk",
+ # Hiero
+ ".hrox",
+ # Houdini
+ ".hip", ".hiplc", ".hipnc",
+ # Blender
+ ".blend",
+ # Celaction
+ ".scn",
+ # TVPaint
+ ".tvpp",
+ # Fusion
+ ".comp",
+ # Harmony
+ ".zip",
+ # Premiere
+ ".prproj",
+ # Resolve
+ ".drp",
+ # Photoshop
+ ".psd", ".psb",
+ # Aftereffects
+ ".aep"
+ ]
+
+ def get_icon(self):
+ return "fa.file"
+
+ def collect_instances(self):
+ for instance_data in pipeline.list_instances():
+ creator_id = instance_data.get("creator_identifier")
+ if creator_id == self.identifier:
+ instance = CreatedInstance.from_existing(
+ instance_data, self
+ )
+ self._add_instance_to_context(instance)
+
+ def update_instances(self, update_list):
+ pipeline.update_instances(update_list)
+
+ def remove_instances(self, instances):
+ pipeline.remove_instances(instances)
+ for instance in instances:
+ self._remove_instance_from_context(instance)
+
+ def create(self, subset_name, data, pre_create_data):
+ # Pass precreate data to creator attributes
+ data["creator_attributes"] = pre_create_data
+ # Create new instance
+ new_instance = CreatedInstance(self.family, subset_name, data, self)
+ # Host implementation of storing metadata about instance
+ pipeline.HostContext.add_instance(new_instance.data_to_store())
+ # Add instance to current context
+ self._add_instance_to_context(new_instance)
+
+ def get_default_variants(self):
+ return [
+ "Main"
+ ]
+
+ def get_instance_attr_defs(self):
+ output = [
+ lib.FileDef(
+ "filepath",
+ folders=False,
+ extensions=self.extensions,
+ label="Filepath"
+ )
+ ]
+ return output
+
+ def get_pre_create_attr_defs(self):
+ # Use same attributes as for instance attrobites
+ return self.get_instance_attr_defs()
+
+ def get_detail_description(self):
+ return """# Publish workfile backup"""
diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_source.py b/openpype/hosts/traypublisher/plugins/publish/collect_source.py
new file mode 100644
index 0000000000..6ff22be13a
--- /dev/null
+++ b/openpype/hosts/traypublisher/plugins/publish/collect_source.py
@@ -0,0 +1,24 @@
+import pyblish.api
+
+
+class CollectSource(pyblish.api.ContextPlugin):
+ """Collecting instances from traypublisher host."""
+
+ label = "Collect source"
+ order = pyblish.api.CollectorOrder - 0.49
+ hosts = ["traypublisher"]
+
+ def process(self, context):
+ # get json paths from os and load them
+ source_name = "traypublisher"
+ for instance in context:
+ source = instance.data.get("source")
+ if not source:
+ instance.data["source"] = source_name
+ self.log.info((
+ "Source of instance \"{}\" is changed to \"{}\""
+ ).format(instance.data["name"], source_name))
+ else:
+ self.log.info((
+ "Source of instance \"{}\" was already set to \"{}\""
+ ).format(instance.data["name"], source))
diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py b/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py
new file mode 100644
index 0000000000..d48bace047
--- /dev/null
+++ b/openpype/hosts/traypublisher/plugins/publish/collect_workfile.py
@@ -0,0 +1,31 @@
+import os
+import pyblish.api
+
+
+class CollectWorkfile(pyblish.api.InstancePlugin):
+ """Collect representation of workfile instances."""
+
+ label = "Collect Workfile"
+ order = pyblish.api.CollectorOrder - 0.49
+ families = ["workfile"]
+ hosts = ["traypublisher"]
+
+ def process(self, instance):
+ if "representations" not in instance.data:
+ instance.data["representations"] = []
+ repres = instance.data["representations"]
+
+ creator_attributes = instance.data["creator_attributes"]
+ filepath = creator_attributes["filepath"]
+ instance.data["sourceFilepath"] = filepath
+
+ staging_dir = os.path.dirname(filepath)
+ filename = os.path.basename(filepath)
+ ext = os.path.splitext(filename)[-1]
+
+ repres.append({
+ "ext": ext,
+ "name": ext,
+ "stagingDir": staging_dir,
+ "files": filename
+ })
diff --git a/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py
new file mode 100644
index 0000000000..88339d2aac
--- /dev/null
+++ b/openpype/hosts/traypublisher/plugins/publish/validate_workfile.py
@@ -0,0 +1,24 @@
+import os
+import pyblish.api
+from openpype.pipeline import PublishValidationError
+
+
+class ValidateWorkfilePath(pyblish.api.InstancePlugin):
+ """Validate existence of workfile instance existence."""
+
+ label = "Collect Workfile"
+ order = pyblish.api.ValidatorOrder - 0.49
+ families = ["workfile"]
+ hosts = ["traypublisher"]
+
+ def process(self, instance):
+ filepath = instance.data["sourceFilepath"]
+ if not filepath:
+ raise PublishValidationError((
+ "Filepath of 'workfile' instance \"{}\" is not set"
+ ).format(instance.data["name"]))
+
+ if not os.path.exists(filepath):
+ raise PublishValidationError((
+ "Filepath of 'workfile' instance \"{}\" does not exist: {}"
+ ).format(instance.data["name"], filepath))
diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py
index ebe7648ad7..882ff03e61 100644
--- a/openpype/lib/__init__.py
+++ b/openpype/lib/__init__.py
@@ -29,6 +29,7 @@ from .execute import (
get_linux_launcher_args,
execute,
run_subprocess,
+ run_detached_process,
run_openpype_process,
clean_envs_for_openpype_process,
path_to_subprocess_arg,
@@ -130,7 +131,7 @@ from .applications import (
PostLaunchHook,
EnvironmentPrepData,
- prepare_host_environments,
+ prepare_app_environments,
prepare_context_environments,
get_app_environments_for_context,
apply_project_environments_value
@@ -188,6 +189,7 @@ __all__ = [
"get_linux_launcher_args",
"execute",
"run_subprocess",
+ "run_detached_process",
"run_openpype_process",
"clean_envs_for_openpype_process",
"path_to_subprocess_arg",
@@ -261,7 +263,7 @@ __all__ = [
"PreLaunchHook",
"PostLaunchHook",
"EnvironmentPrepData",
- "prepare_host_environments",
+ "prepare_app_environments",
"prepare_context_environments",
"get_app_environments_for_context",
"apply_project_environments_value",
diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py
index 393c83e9be..0b51a6629c 100644
--- a/openpype/lib/applications.py
+++ b/openpype/lib/applications.py
@@ -1295,7 +1295,7 @@ def get_app_environments_for_context(
"env": env
})
- prepare_host_environments(data, env_group)
+ prepare_app_environments(data, env_group)
prepare_context_environments(data, env_group)
# Discard avalon connection
@@ -1316,7 +1316,7 @@ def _merge_env(env, current_env):
return result
-def prepare_host_environments(data, env_group=None, implementation_envs=True):
+def prepare_app_environments(data, env_group=None, implementation_envs=True):
"""Modify launch environments based on launched app and context.
Args:
@@ -1474,6 +1474,22 @@ def prepare_context_environments(data, env_group=None):
)
app = data["app"]
+ context_env = {
+ "AVALON_PROJECT": project_doc["name"],
+ "AVALON_ASSET": asset_doc["name"],
+ "AVALON_TASK": task_name,
+ "AVALON_APP_NAME": app.full_name
+ }
+
+ log.debug(
+ "Context environments set:\n{}".format(
+ json.dumps(context_env, indent=4)
+ )
+ )
+ data["env"].update(context_env)
+ if not app.is_host:
+ return
+
workdir_data = get_workdir_data(
project_doc, asset_doc, task_name, app.host_name
)
@@ -1504,20 +1520,8 @@ def prepare_context_environments(data, env_group=None):
"Couldn't create workdir because: {}".format(str(exc))
)
- context_env = {
- "AVALON_PROJECT": project_doc["name"],
- "AVALON_ASSET": asset_doc["name"],
- "AVALON_TASK": task_name,
- "AVALON_APP": app.host_name,
- "AVALON_APP_NAME": app.full_name,
- "AVALON_WORKDIR": workdir
- }
- log.debug(
- "Context environments set:\n{}".format(
- json.dumps(context_env, indent=4)
- )
- )
- data["env"].update(context_env)
+ data["env"]["AVALON_APP"] = app.host_name
+ data["env"]["AVALON_WORKDIR"] = workdir
_prepare_last_workfile(data, workdir)
diff --git a/openpype/lib/delivery.py b/openpype/lib/delivery.py
index 01fcc907ed..a61603fa05 100644
--- a/openpype/lib/delivery.py
+++ b/openpype/lib/delivery.py
@@ -17,7 +17,7 @@ def collect_frames(files):
Returns:
(dict): {'/asset/subset_v001.0001.png': '0001', ....}
"""
- collections, remainder = clique.assemble(files)
+ collections, remainder = clique.assemble(files, minimum_items=1)
sources_and_frames = {}
if collections:
diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py
index afde844f2d..f2eb97c5f5 100644
--- a/openpype/lib/execute.py
+++ b/openpype/lib/execute.py
@@ -1,5 +1,9 @@
import os
+import sys
import subprocess
+import platform
+import json
+import tempfile
import distutils.spawn
from .log import PypeLogger as Logger
@@ -181,6 +185,80 @@ def run_openpype_process(*args, **kwargs):
return run_subprocess(args, env=env, **kwargs)
+def run_detached_process(args, **kwargs):
+ """Execute process with passed arguments as separated process.
+
+ Values from 'os.environ' are used for environments if are not passed.
+ They are cleaned using 'clean_envs_for_openpype_process' function.
+
+ Example:
+ ```
+ run_detached_openpype_process("run", "")
+ ```
+
+ Args:
+ *args (tuple): OpenPype cli arguments.
+ **kwargs (dict): Keyword arguments for for subprocess.Popen.
+
+ Returns:
+ subprocess.Popen: Pointer to launched process but it is possible that
+ launched process is already killed (on linux).
+ """
+ env = kwargs.pop("env", None)
+ # Keep env untouched if are passed and not empty
+ if not env:
+ env = os.environ
+
+ # Create copy of passed env
+ kwargs["env"] = {k: v for k, v in env.items()}
+
+ low_platform = platform.system().lower()
+ if low_platform == "darwin":
+ new_args = ["open", "-na", args.pop(0), "--args"]
+ new_args.extend(args)
+ args = new_args
+
+ elif low_platform == "windows":
+ flags = (
+ subprocess.CREATE_NEW_PROCESS_GROUP
+ | subprocess.DETACHED_PROCESS
+ )
+ kwargs["creationflags"] = flags
+
+ if not sys.stdout:
+ kwargs["stdout"] = subprocess.DEVNULL
+ kwargs["stderr"] = subprocess.DEVNULL
+
+ elif low_platform == "linux" and get_linux_launcher_args() is not None:
+ json_data = {
+ "args": args,
+ "env": kwargs.pop("env")
+ }
+ json_temp = tempfile.NamedTemporaryFile(
+ mode="w", prefix="op_app_args", suffix=".json", delete=False
+ )
+ json_temp.close()
+ json_temp_filpath = json_temp.name
+ with open(json_temp_filpath, "w") as stream:
+ json.dump(json_data, stream)
+
+ new_args = get_linux_launcher_args()
+ new_args.append(json_temp_filpath)
+
+ # Create mid-process which will launch application
+ process = subprocess.Popen(new_args, **kwargs)
+ # Wait until the process finishes
+ # - This is important! The process would stay in "open" state.
+ process.wait()
+ # Remove the temp file
+ os.remove(json_temp_filpath)
+ # Return process which is already terminated
+ return process
+
+ process = subprocess.Popen(args, **kwargs)
+ return process
+
+
def path_to_subprocess_arg(path):
"""Prepare path for subprocess arguments.
diff --git a/openpype/lib/terminal.py b/openpype/lib/terminal.py
index bc0744931a..5121b6ec26 100644
--- a/openpype/lib/terminal.py
+++ b/openpype/lib/terminal.py
@@ -49,11 +49,13 @@ class Terminal:
"""
from openpype.lib import env_value_to_bool
- use_colors = env_value_to_bool(
- "OPENPYPE_LOG_NO_COLORS", default=Terminal.use_colors
+ log_no_colors = env_value_to_bool(
+ "OPENPYPE_LOG_NO_COLORS", default=None
)
- if not use_colors:
- Terminal.use_colors = use_colors
+ if log_no_colors is not None:
+ Terminal.use_colors = not log_no_colors
+
+ if not Terminal.use_colors:
Terminal._initialized = True
return
diff --git a/openpype/modules/base.py b/openpype/modules/base.py
index d566692439..c7078475df 100644
--- a/openpype/modules/base.py
+++ b/openpype/modules/base.py
@@ -33,16 +33,21 @@ DEFAULT_OPENPYPE_MODULES = (
"avalon_apps",
"clockify",
"log_viewer",
+ "deadline",
"muster",
+ "royalrender",
"python_console_interpreter",
+ "ftrack",
"slack",
"webserver",
"launcher_action",
"project_manager_action",
"settings_action",
"standalonepublish_action",
+ "traypublish_action",
"job_queue",
"timers_manager",
+ "sync_server",
)
@@ -218,8 +223,6 @@ def load_interfaces(force=False):
def _load_interfaces():
# Key under which will be modules imported in `sys.modules`
- from openpype.lib import import_filepath
-
modules_key = "openpype_interfaces"
sys.modules[modules_key] = openpype_interfaces = (
@@ -844,6 +847,7 @@ class TrayModulesManager(ModulesManager):
"avalon",
"clockify",
"standalonepublish_tool",
+ "traypublish_tool",
"log_viewer",
"local_settings",
"settings"
diff --git a/openpype/modules/default_modules/deadline/__init__.py b/openpype/modules/deadline/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/__init__.py
rename to openpype/modules/deadline/__init__.py
diff --git a/openpype/lib/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py
similarity index 99%
rename from openpype/lib/abstract_submit_deadline.py
rename to openpype/modules/deadline/abstract_submit_deadline.py
index f54a2501a3..22902d79ea 100644
--- a/openpype/lib/abstract_submit_deadline.py
+++ b/openpype/modules/deadline/abstract_submit_deadline.py
@@ -15,7 +15,7 @@ import attr
import requests
import pyblish.api
-from .abstract_metaplugins import AbstractMetaInstancePlugin
+from openpype.lib.abstract_metaplugins import AbstractMetaInstancePlugin
def requests_post(*args, **kwargs):
diff --git a/openpype/modules/default_modules/deadline/deadline_module.py b/openpype/modules/deadline/deadline_module.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/deadline_module.py
rename to openpype/modules/deadline/deadline_module.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
rename to openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/collect_default_deadline_server.py
rename to openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
similarity index 95%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py
rename to openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
index 1fff55500e..2918b54d4a 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/submit_aftereffects_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_aftereffects_deadline.py
@@ -5,9 +5,9 @@ import pyblish.api
from avalon import api
-from openpype.lib import abstract_submit_deadline
-from openpype.lib.abstract_submit_deadline import DeadlineJobInfo
from openpype.lib import env_value_to_bool
+from openpype_modules.deadline import abstract_submit_deadline
+from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
@attr.s
@@ -24,7 +24,9 @@ class DeadlinePluginInfo():
MultiProcess = attr.ib(default=None)
-class AfterEffectsSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline):
+class AfterEffectsSubmitDeadline(
+ abstract_submit_deadline.AbstractSubmitDeadline
+):
label = "Submit AE to Deadline"
order = pyblish.api.IntegratorOrder + 0.1
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
similarity index 98%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py
rename to openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
index 9d55d43ba6..918efb6630 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/submit_harmony_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_harmony_deadline.py
@@ -8,11 +8,11 @@ import re
import attr
import pyblish.api
-
-import openpype.lib.abstract_submit_deadline
-from openpype.lib.abstract_submit_deadline import DeadlineJobInfo
from avalon import api
+from openpype_modules.deadline import abstract_submit_deadline
+from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo
+
class _ZipFile(ZipFile):
"""Extended check for windows invalid characters."""
@@ -217,7 +217,8 @@ class PluginInfo(object):
class HarmonySubmitDeadline(
- openpype.lib.abstract_submit_deadline.AbstractSubmitDeadline):
+ abstract_submit_deadline.AbstractSubmitDeadline
+):
"""Submit render write of Harmony scene to Deadline.
Renders are submitted to a Deadline Web Service as
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py b/openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_remote_publish.py
rename to openpype/modules/deadline/plugins/publish/submit_houdini_remote_publish.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
similarity index 98%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py
rename to openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
index 2cd6b0e6b0..59aeb68b79 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py
+++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py
@@ -50,8 +50,8 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin):
# StartFrame to EndFrame by byFrameStep
frames = "{start}-{end}x{step}".format(
- start=int(instance.data["startFrame"]),
- end=int(instance.data["endFrame"]),
+ start=int(instance.data["frameStart"]),
+ end=int(instance.data["frameEnd"]),
step=int(instance.data["byFrameStep"]),
)
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_maya_deadline.py
rename to openpype/modules/deadline/plugins/publish/submit_maya_deadline.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_nuke_deadline.py
rename to openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
similarity index 99%
rename from openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py
rename to openpype/modules/deadline/plugins/publish/submit_publish_job.py
index a77a968815..c7a14791e4 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/submit_publish_job.py
+++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py
@@ -316,8 +316,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
import speedcopy
self.log.info("Preparing to copy ...")
- start = instance.data.get("startFrame")
- end = instance.data.get("endFrame")
+ start = instance.data.get("frameStart")
+ end = instance.data.get("frameEnd")
# get latest version of subset
# this will stop if subset wasn't published yet
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py b/openpype/modules/deadline/plugins/publish/validate_deadline_connection.py
similarity index 100%
rename from openpype/modules/default_modules/deadline/plugins/publish/validate_deadline_connection.py
rename to openpype/modules/deadline/plugins/publish/validate_deadline_connection.py
diff --git a/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
similarity index 50%
rename from openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
rename to openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
index 719c7dfe3e..d49e314179 100644
--- a/openpype/modules/default_modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
+++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py
@@ -1,11 +1,10 @@
import os
-import json
import requests
import pyblish.api
-from openpype.lib.abstract_submit_deadline import requests_get
from openpype.lib.delivery import collect_frames
+from openpype_modules.deadline.abstract_submit_deadline import requests_get
class ValidateExpectedFiles(pyblish.api.InstancePlugin):
@@ -30,47 +29,58 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
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:
+ # We always check for user override because the user might have
+ # also overridden the Job frame list to be longer than the
+ # originally submitted frame range
+ # todo: We should first check if Job frame range was overridden
+ # at all so we don't unnecessarily override anything
+ file_name_template, frame_placeholder = \
+ self._get_file_name_template_and_placeholder(
+ expected_files)
- if self.allow_user_override:
- file_name_template, frame_placeholder = \
- self._get_file_name_template_and_placeholder(
- expected_files)
+ if not file_name_template:
+ raise RuntimeError("Unable to retrieve file_name template"
+ "from files: {}".format(expected_files))
- if not file_name_template:
- return
+ job_expected_files = self._get_job_expected_files(
+ file_name_template,
+ frame_placeholder,
+ frame_list)
- real_expected_rendered = self._get_real_render_expected(
- file_name_template,
- frame_placeholder,
- frame_list)
+ job_files_diff = job_expected_files.difference(expected_files)
+ if job_files_diff:
+ self.log.debug(
+ "Detected difference in expected output files from "
+ "Deadline job. Assuming an updated frame list by the "
+ "user. Difference: {}".format(sorted(job_files_diff))
+ )
- 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))
+ # Update the representation expected files
+ self.log.info("Update range from actual job range "
+ "to frame list: {}".format(frame_list))
+ repre["files"] = sorted(job_expected_files)
+
+ # Update the expected files
+ expected_files = job_expected_files
+
+ # We don't use set.difference because we do allow other existing
+ # files to be in the folder that we might not want to use.
+ missing = expected_files - existing_files
+ if missing:
+ raise RuntimeError("Missing expected files: {}".format(
+ sorted(missing)))
def _get_frame_list(self, original_job_id):
- """
- Returns list of frame ranges from all render job.
+ """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.
+ Render job might be re-submitted so job_id in metadata.json could be
+ invalid. GlobalJobPreload injects current job id to RENDER_JOB_IDS.
- Args:
- original_job_id (str)
- Returns:
- (list)
+ Args:
+ original_job_id (str)
+ Returns:
+ (list)
"""
all_frame_lists = []
render_job_ids = os.environ.get("RENDER_JOB_IDS")
@@ -87,13 +97,15 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
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.
+ def _get_job_expected_files(self,
+ file_name_template,
+ frame_placeholder,
+ frame_list):
+ """Calculates list of names of expected rendered files.
+
+ Might be different from expected files from submission if user
+ explicitly and manually changed the frame list on the Deadline job.
- 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))
@@ -115,6 +127,14 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
file_name_template = frame_placeholder = None
for file_name, frame in sources_and_frames.items():
+
+ # There might be cases where clique was unable to collect
+ # collections in `collect_frames` - thus we capture that case
+ if frame is None:
+ self.log.warning("Unable to detect frame from filename: "
+ "{}".format(file_name))
+ continue
+
frame_placeholder = "#" * len(frame)
file_name_template = os.path.basename(
file_name.replace(frame, frame_placeholder))
@@ -123,11 +143,11 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
return file_name_template, frame_placeholder
def _get_job_info(self, job_id):
- """
- Calls DL for actual job info for '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.
- Might be different than job info saved in metadata.json if user
- manually changes job pre/during rendering.
"""
# get default deadline webservice url from deadline module
deadline_url = self.instance.context.data["defaultDeadline"]
@@ -140,8 +160,8 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
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))
+ self.log.error("Deadline is not accessible at "
+ "{}".format(deadline_url))
return {}
if not response.ok:
@@ -155,29 +175,26 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
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'"""
+ def _get_existing_files(self, staging_dir):
+ """Returns set of existing file names from 'staging_dir'"""
existing_files = set()
- for file_name in os.listdir(out_dir):
+ for file_name in os.listdir(staging_dir):
existing_files.add(file_name)
return existing_files
def _get_expected_files(self, repre):
- """Returns set of file names from metadata.json"""
+ """Returns set of file names in representation['files']
+
+ The representations are collected from `CollectRenderedFiles` using
+ the metadata.json file submitted along with the render job.
+
+ Args:
+ repre (dict): The representation containing 'files'
+
+ Returns:
+ set: Set of expected file_names in the staging directory.
+
+ """
expected_files = set()
files = repre["files"]
diff --git a/vendor/deadline/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py
similarity index 100%
rename from vendor/deadline/custom/plugins/GlobalJobPreLoad.py
rename to openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py
diff --git a/vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.ico b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.ico
similarity index 100%
rename from vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.ico
rename to openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.ico
diff --git a/vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.options b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.options
similarity index 100%
rename from vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.options
rename to openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.options
diff --git a/vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param
similarity index 100%
rename from vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param
rename to openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.param
diff --git a/vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py b/openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py
similarity index 100%
rename from vendor/deadline/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py
rename to openpype/modules/deadline/repository/custom/plugins/HarmonyOpenPype/HarmonyOpenPype.py
diff --git a/vendor/deadline/custom/plugins/OpenPype/OpenPype.ico b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.ico
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPype/OpenPype.ico
rename to openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.ico
diff --git a/vendor/deadline/custom/plugins/OpenPype/OpenPype.options b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.options
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPype/OpenPype.options
rename to openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.options
diff --git a/vendor/deadline/custom/plugins/OpenPype/OpenPype.param b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPype/OpenPype.param
rename to openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.param
diff --git a/vendor/deadline/custom/plugins/OpenPype/OpenPype.py b/openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPype/OpenPype.py
rename to openpype/modules/deadline/repository/custom/plugins/OpenPype/OpenPype.py
diff --git a/vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico
rename to openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.ico
diff --git a/vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options
rename to openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.options
diff --git a/vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param
rename to openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.param
diff --git a/vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py b/openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py
similarity index 100%
rename from vendor/deadline/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py
rename to openpype/modules/deadline/repository/custom/plugins/OpenPypeTileAssembler/OpenPypeTileAssembler.py
diff --git a/vendor/deadline/readme.md b/openpype/modules/deadline/repository/readme.md
similarity index 100%
rename from vendor/deadline/readme.md
rename to openpype/modules/deadline/repository/readme.md
diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/arrow b/openpype/modules/default_modules/ftrack/python2_vendor/arrow
deleted file mode 160000
index b746fedf72..0000000000
--- a/openpype/modules/default_modules/ftrack/python2_vendor/arrow
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit b746fedf7286c3755a46f07ab72f4c414cd41fc0
diff --git a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api b/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
deleted file mode 160000
index d277f474ab..0000000000
--- a/openpype/modules/default_modules/ftrack/python2_vendor/ftrack-python-api
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit d277f474ab016e7b53479c36af87cb861d0cc53e
diff --git a/openpype/modules/default_modules/ftrack/__init__.py b/openpype/modules/ftrack/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/__init__.py
rename to openpype/modules/ftrack/__init__.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_clone_review_session.py b/openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_clone_review_session.py
rename to openpype/modules/ftrack/event_handlers_server/action_clone_review_session.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_multiple_notes.py
rename to openpype/modules/ftrack/event_handlers_server/action_multiple_notes.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_server/action_prepare_project.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_prepare_project.py
rename to openpype/modules/ftrack/event_handlers_server/action_prepare_project.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_private_project_detection.py b/openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_private_project_detection.py
rename to openpype/modules/ftrack/event_handlers_server/action_private_project_detection.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py b/openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py
rename to openpype/modules/ftrack/event_handlers_server/action_push_frame_values_to_task.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py
rename to openpype/modules/ftrack/event_handlers_server/action_sync_to_avalon.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_del_avalon_id_from_new.py b/openpype/modules/ftrack/event_handlers_server/event_del_avalon_id_from_new.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_del_avalon_id_from_new.py
rename to openpype/modules/ftrack/event_handlers_server/event_del_avalon_id_from_new.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_first_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_first_version_status.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_first_version_status.py
rename to openpype/modules/ftrack/event_handlers_server/event_first_version_status.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_next_task_update.py b/openpype/modules/ftrack/event_handlers_server/event_next_task_update.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_next_task_update.py
rename to openpype/modules/ftrack/event_handlers_server/event_next_task_update.py
diff --git a/openpype/modules/default_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
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py
rename to openpype/modules/ftrack/event_handlers_server/event_push_frame_values_to_task.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_radio_buttons.py b/openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_radio_buttons.py
rename to openpype/modules/ftrack/event_handlers_server/event_radio_buttons.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py b/openpype/modules/ftrack/event_handlers_server/event_sync_links.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_links.py
rename to openpype/modules/ftrack/event_handlers_server/event_sync_links.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_sync_to_avalon.py
rename to openpype/modules/ftrack/event_handlers_server/event_sync_to_avalon.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_task_to_parent_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_task_to_parent_status.py
rename to openpype/modules/ftrack/event_handlers_server/event_task_to_parent_status.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_task_to_version_status.py b/openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_task_to_version_status.py
rename to openpype/modules/ftrack/event_handlers_server/event_task_to_version_status.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_thumbnail_updates.py b/openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_thumbnail_updates.py
rename to openpype/modules/ftrack/event_handlers_server/event_thumbnail_updates.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_user_assigment.py b/openpype/modules/ftrack/event_handlers_server/event_user_assigment.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_user_assigment.py
rename to openpype/modules/ftrack/event_handlers_server/event_user_assigment.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/event_version_to_task_statuses.py b/openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
rename to openpype/modules/ftrack/event_handlers_server/event_version_to_task_statuses.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py b/openpype/modules/ftrack/event_handlers_user/action_applications.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_applications.py
rename to openpype/modules/ftrack/event_handlers_user/action_applications.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_batch_task_creation.py b/openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_batch_task_creation.py
rename to openpype/modules/ftrack/event_handlers_user/action_batch_task_creation.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py b/openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
rename to openpype/modules/ftrack/event_handlers_user/action_clean_hierarchical_attributes.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_client_review_sort.py b/openpype/modules/ftrack/event_handlers_user/action_client_review_sort.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_client_review_sort.py
rename to openpype/modules/ftrack/event_handlers_user/action_client_review_sort.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_component_open.py b/openpype/modules/ftrack/event_handlers_user/action_component_open.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_component_open.py
rename to openpype/modules/ftrack/event_handlers_user/action_component_open.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py b/openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_create_cust_attrs.py
rename to openpype/modules/ftrack/event_handlers_user/action_create_cust_attrs.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_create_folders.py
rename to openpype/modules/ftrack/event_handlers_user/action_create_folders.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py b/openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_create_project_structure.py
rename to openpype/modules/ftrack/event_handlers_user/action_create_project_structure.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py
similarity index 96%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py
rename to openpype/modules/ftrack/event_handlers_user/action_delete_asset.py
index 676dd80e93..94385a36c5 100644
--- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_asset.py
+++ b/openpype/modules/ftrack/event_handlers_user/action_delete_asset.py
@@ -3,8 +3,9 @@ import uuid
from datetime import datetime
from bson.objectid import ObjectId
-from openpype_modules.ftrack.lib import BaseAction, statics_icon
from avalon.api import AvalonMongoDB
+from openpype_modules.ftrack.lib import BaseAction, statics_icon
+from openpype_modules.ftrack.lib.avalon_sync import create_chunks
class DeleteAssetSubset(BaseAction):
@@ -554,8 +555,8 @@ class DeleteAssetSubset(BaseAction):
ftrack_proc_txt, ", ".join(ftrack_ids_to_delete)
))
- entities_by_link_len = (
- self._filter_entities_to_delete(ftrack_ids_to_delete, session)
+ entities_by_link_len = self._prepare_entities_before_delete(
+ ftrack_ids_to_delete, session
)
for link_len in sorted(entities_by_link_len.keys(), reverse=True):
for entity in entities_by_link_len[link_len]:
@@ -609,7 +610,7 @@ class DeleteAssetSubset(BaseAction):
return self.report_handle(report_messages, project_name, event)
- def _filter_entities_to_delete(self, ftrack_ids_to_delete, session):
+ def _prepare_entities_before_delete(self, ftrack_ids_to_delete, session):
"""Filter children entities to avoid CircularDependencyError."""
joined_ids_to_delete = ", ".join(
["\"{}\"".format(id) for id in ftrack_ids_to_delete]
@@ -638,6 +639,21 @@ class DeleteAssetSubset(BaseAction):
parent_ids_to_delete.append(entity["id"])
to_delete_entities.append(entity)
+ # Unset 'task_id' from AssetVersion entities
+ # - when task is deleted the asset version is not marked for deletion
+ task_ids = set(
+ entity["id"]
+ for entity in to_delete_entities
+ if entity.entity_type.lower() == "task"
+ )
+ for chunk in create_chunks(task_ids):
+ asset_versions = session.query((
+ "select id, task_id from AssetVersion where task_id in ({})"
+ ).format(self.join_query_keys(chunk))).all()
+ for asset_version in asset_versions:
+ asset_version["task_id"] = None
+ session.commit()
+
entities_by_link_len = collections.defaultdict(list)
for entity in to_delete_entities:
entities_by_link_len[len(entity["link"])].append(entity)
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py b/openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_delete_old_versions.py
rename to openpype/modules/ftrack/event_handlers_user/action_delete_old_versions.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_delivery.py b/openpype/modules/ftrack/event_handlers_user/action_delivery.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_delivery.py
rename to openpype/modules/ftrack/event_handlers_user/action_delivery.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/ftrack/event_handlers_user/action_djvview.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_djvview.py
rename to openpype/modules/ftrack/event_handlers_user/action_djvview.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_job_killer.py b/openpype/modules/ftrack/event_handlers_user/action_job_killer.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_job_killer.py
rename to openpype/modules/ftrack/event_handlers_user/action_job_killer.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_multiple_notes.py b/openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_multiple_notes.py
rename to openpype/modules/ftrack/event_handlers_user/action_multiple_notes.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py b/openpype/modules/ftrack/event_handlers_user/action_prepare_project.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_prepare_project.py
rename to openpype/modules/ftrack/event_handlers_user/action_prepare_project.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_rv.py b/openpype/modules/ftrack/event_handlers_user/action_rv.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_rv.py
rename to openpype/modules/ftrack/event_handlers_user/action_rv.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_seed.py b/openpype/modules/ftrack/event_handlers_user/action_seed.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_seed.py
rename to openpype/modules/ftrack/event_handlers_user/action_seed.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py b/openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py
rename to openpype/modules/ftrack/event_handlers_user/action_store_thumbnails_to_avalon.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/ftrack/event_handlers_user/action_sync_to_avalon.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py
rename to openpype/modules/ftrack/event_handlers_user/action_sync_to_avalon.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_test.py b/openpype/modules/ftrack/event_handlers_user/action_test.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_test.py
rename to openpype/modules/ftrack/event_handlers_user/action_test.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_thumbnail_to_childern.py b/openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_childern.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_thumbnail_to_childern.py
rename to openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_childern.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_thumbnail_to_parent.py b/openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_parent.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_thumbnail_to_parent.py
rename to openpype/modules/ftrack/event_handlers_user/action_thumbnail_to_parent.py
diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_where_run_ask.py b/openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/event_handlers_user/action_where_run_ask.py
rename to openpype/modules/ftrack/event_handlers_user/action_where_run_ask.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_module.py
rename to openpype/modules/ftrack/ftrack_module.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/__init__.py b/openpype/modules/ftrack/ftrack_server/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_server/__init__.py
rename to openpype/modules/ftrack/ftrack_server/__init__.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/ftrack/ftrack_server/event_server_cli.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py
rename to openpype/modules/ftrack/ftrack_server/event_server_cli.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py b/openpype/modules/ftrack/ftrack_server/ftrack_server.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_server/ftrack_server.py
rename to openpype/modules/ftrack/ftrack_server/ftrack_server.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/lib.py b/openpype/modules/ftrack/ftrack_server/lib.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_server/lib.py
rename to openpype/modules/ftrack/ftrack_server/lib.py
diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py b/openpype/modules/ftrack/ftrack_server/socket_thread.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/ftrack_server/socket_thread.py
rename to openpype/modules/ftrack/ftrack_server/socket_thread.py
diff --git a/openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py b/openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/launch_hooks/post_ftrack_changes.py
rename to openpype/modules/ftrack/launch_hooks/post_ftrack_changes.py
diff --git a/openpype/modules/default_modules/ftrack/launch_hooks/pre_python2_vendor.py b/openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/launch_hooks/pre_python2_vendor.py
rename to openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py
diff --git a/openpype/modules/default_modules/ftrack/lib/__init__.py b/openpype/modules/ftrack/lib/__init__.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/__init__.py
rename to openpype/modules/ftrack/lib/__init__.py
diff --git a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py b/openpype/modules/ftrack/lib/avalon_sync.py
similarity index 99%
rename from openpype/modules/default_modules/ftrack/lib/avalon_sync.py
rename to openpype/modules/ftrack/lib/avalon_sync.py
index 06e8784287..db7c592c9b 100644
--- a/openpype/modules/default_modules/ftrack/lib/avalon_sync.py
+++ b/openpype/modules/ftrack/lib/avalon_sync.py
@@ -33,6 +33,30 @@ CURRENT_DOC_SCHEMAS = {
}
+def create_chunks(iterable, chunk_size=None):
+ """Separate iterable into multiple chunks by size.
+
+ Args:
+ iterable(list|tuple|set): Object that will be separated into chunks.
+ chunk_size(int): Size of one chunk. Default value is 200.
+
+ Returns:
+ list: Chunked items.
+ """
+ chunks = []
+ if not iterable:
+ return chunks
+
+ tupled_iterable = tuple(iterable)
+ iterable_size = len(tupled_iterable)
+ if chunk_size is None:
+ chunk_size = 200
+
+ for idx in range(0, iterable_size, chunk_size):
+ chunks.append(tupled_iterable[idx:idx + chunk_size])
+ return chunks
+
+
def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
schema_name = "asset-3.0"
if in_schema:
@@ -1147,10 +1171,8 @@ class SyncEntitiesFactory:
ids_len = len(tupled_ids)
chunk_size = int(5000 / ids_len)
all_links = []
- for idx in range(0, ids_len, chunk_size):
- entity_ids_joined = join_query_keys(
- tupled_ids[idx:idx + chunk_size]
- )
+ for chunk in create_chunks(ftrack_ids, chunk_size):
+ entity_ids_joined = join_query_keys(chunk)
all_links.extend(self.session.query((
"select from_id, to_id from"
diff --git a/openpype/modules/default_modules/ftrack/lib/constants.py b/openpype/modules/ftrack/lib/constants.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/constants.py
rename to openpype/modules/ftrack/lib/constants.py
diff --git a/openpype/modules/default_modules/ftrack/lib/credentials.py b/openpype/modules/ftrack/lib/credentials.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/credentials.py
rename to openpype/modules/ftrack/lib/credentials.py
diff --git a/openpype/modules/default_modules/ftrack/lib/custom_attributes.json b/openpype/modules/ftrack/lib/custom_attributes.json
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/custom_attributes.json
rename to openpype/modules/ftrack/lib/custom_attributes.json
diff --git a/openpype/modules/default_modules/ftrack/lib/custom_attributes.py b/openpype/modules/ftrack/lib/custom_attributes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/custom_attributes.py
rename to openpype/modules/ftrack/lib/custom_attributes.py
diff --git a/openpype/modules/default_modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/ftrack_action_handler.py
rename to openpype/modules/ftrack/lib/ftrack_action_handler.py
diff --git a/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/ftrack/lib/ftrack_base_handler.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py
rename to openpype/modules/ftrack/lib/ftrack_base_handler.py
diff --git a/openpype/modules/default_modules/ftrack/lib/ftrack_event_handler.py b/openpype/modules/ftrack/lib/ftrack_event_handler.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/ftrack_event_handler.py
rename to openpype/modules/ftrack/lib/ftrack_event_handler.py
diff --git a/openpype/modules/default_modules/ftrack/lib/settings.py b/openpype/modules/ftrack/lib/settings.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/lib/settings.py
rename to openpype/modules/ftrack/lib/settings.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/_unused_publish/integrate_ftrack_comments.py b/openpype/modules/ftrack/plugins/_unused_publish/integrate_ftrack_comments.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/_unused_publish/integrate_ftrack_comments.py
rename to openpype/modules/ftrack/plugins/_unused_publish/integrate_ftrack_comments.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py
similarity index 84%
rename from openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_api.py
rename to openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py
index a348617cfc..07af217fb6 100644
--- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_api.py
+++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py
@@ -1,4 +1,3 @@
-import os
import logging
import pyblish.api
import avalon.api
@@ -43,37 +42,48 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
).format(project_name))
project_entity = project_entities[0]
+
self.log.debug("Project found: {0}".format(project_entity))
- # Find asset entity
- entity_query = (
- 'TypedContext where project_id is "{0}"'
- ' and name is "{1}"'
- ).format(project_entity["id"], asset_name)
- self.log.debug("Asset entity query: < {0} >".format(entity_query))
- asset_entities = []
- for entity in session.query(entity_query).all():
- # Skip tasks
- if entity.entity_type.lower() != "task":
- asset_entities.append(entity)
+ asset_entity = None
+ if asset_name:
+ # Find asset entity
+ entity_query = (
+ 'TypedContext where project_id is "{0}"'
+ ' and name is "{1}"'
+ ).format(project_entity["id"], asset_name)
+ self.log.debug("Asset entity query: < {0} >".format(entity_query))
+ asset_entities = []
+ for entity in session.query(entity_query).all():
+ # Skip tasks
+ if entity.entity_type.lower() != "task":
+ asset_entities.append(entity)
- if len(asset_entities) == 0:
- raise AssertionError((
- "Entity with name \"{0}\" not found"
- " in Ftrack project \"{1}\"."
- ).format(asset_name, project_name))
+ if len(asset_entities) == 0:
+ raise AssertionError((
+ "Entity with name \"{0}\" not found"
+ " in Ftrack project \"{1}\"."
+ ).format(asset_name, project_name))
- elif len(asset_entities) > 1:
- raise AssertionError((
- "Found more than one entity with name \"{0}\""
- " in Ftrack project \"{1}\"."
- ).format(asset_name, project_name))
+ elif len(asset_entities) > 1:
+ raise AssertionError((
+ "Found more than one entity with name \"{0}\""
+ " in Ftrack project \"{1}\"."
+ ).format(asset_name, project_name))
+
+ asset_entity = asset_entities[0]
- asset_entity = asset_entities[0]
self.log.debug("Asset found: {0}".format(asset_entity))
+ task_entity = None
# Find task entity if task is set
- if task_name:
+ if not asset_entity:
+ self.log.warning(
+ "Asset entity is not set. Skipping query of task entity."
+ )
+ elif not task_name:
+ self.log.warning("Task name is not set.")
+ else:
task_query = (
'Task where name is "{0}" and parent_id is "{1}"'
).format(task_name, asset_entity["id"])
@@ -88,10 +98,6 @@ class CollectFtrackApi(pyblish.api.ContextPlugin):
else:
self.log.debug("Task entity found: {0}".format(task_entity))
- else:
- task_entity = None
- self.log.warning("Task name is not set.")
-
context.data["ftrackSession"] = session
context.data["ftrackPythonModule"] = ftrack_api
context.data["ftrackProject"] = project_entity
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/collect_ftrack_family.py
rename to openpype/modules/ftrack/plugins/publish/collect_ftrack_family.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_local_ftrack_creds.py b/openpype/modules/ftrack/plugins/publish/collect_local_ftrack_creds.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/collect_local_ftrack_creds.py
rename to openpype/modules/ftrack/plugins/publish/collect_local_ftrack_creds.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py b/openpype/modules/ftrack/plugins/publish/collect_username.py
similarity index 88%
rename from openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py
rename to openpype/modules/ftrack/plugins/publish/collect_username.py
index 303490189b..84d7f60a3f 100644
--- a/openpype/modules/default_modules/ftrack/plugins/publish/collect_username.py
+++ b/openpype/modules/ftrack/plugins/publish/collect_username.py
@@ -33,7 +33,6 @@ class CollectUsername(pyblish.api.ContextPlugin):
def process(self, context):
self.log.info("CollectUsername")
-
os.environ["FTRACK_API_USER"] = os.environ["FTRACK_BOT_API_USER"]
os.environ["FTRACK_API_KEY"] = os.environ["FTRACK_BOT_API_KEY"]
@@ -57,7 +56,12 @@ class CollectUsername(pyblish.api.ContextPlugin):
if not user:
raise ValueError(
"Couldn't find user with {} email".format(user_email))
-
- username = user[0].get("username")
+ user = user[0]
+ username = user.get("username")
self.log.debug("Resolved ftrack username:: {}".format(username))
os.environ["FTRACK_API_USER"] = username
+
+ burnin_name = username
+ if '@' in burnin_name:
+ burnin_name = burnin_name[:burnin_name.index('@')]
+ os.environ["WEBPUBLISH_OPENPYPE_USERNAME"] = burnin_name
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_api.py
rename to openpype/modules/ftrack/plugins/publish/integrate_ftrack_api.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py
rename to openpype/modules/ftrack/plugins/publish/integrate_ftrack_component_overwrite.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_instances.py
rename to openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_note.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/integrate_ftrack_note.py
rename to openpype/modules/ftrack/plugins/publish/integrate_ftrack_note.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
rename to openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py
diff --git a/openpype/modules/default_modules/ftrack/plugins/publish/validate_custom_ftrack_attributes.py b/openpype/modules/ftrack/plugins/publish/validate_custom_ftrack_attributes.py
similarity index 100%
rename from openpype/modules/default_modules/ftrack/plugins/publish/validate_custom_ftrack_attributes.py
rename to openpype/modules/ftrack/plugins/publish/validate_custom_ftrack_attributes.py
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/.gitignore b/openpype/modules/ftrack/python2_vendor/arrow/.gitignore
new file mode 100644
index 0000000000..0448d0cf0c
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/.gitignore
@@ -0,0 +1,211 @@
+README.rst.new
+
+# Small entry point file for debugging tasks
+test.py
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+local/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Swap
+[._]*.s[a-v][a-z]
+[._]*.sw[a-p]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
+[._]sw[a-p]
+
+# Session
+Session.vim
+Sessionx.vim
+
+# Temporary
+.netrwhist
+*~
+# Auto-generated tag files
+tags
+# Persistent undo
+[._]*.un~
+
+.idea/
+.vscode/
+
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml b/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml
new file mode 100644
index 0000000000..1f5128595b
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/.pre-commit-config.yaml
@@ -0,0 +1,41 @@
+default_language_version:
+ python: python3
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v3.2.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: fix-encoding-pragma
+ exclude: ^arrow/_version.py
+ - id: requirements-txt-fixer
+ - id: check-ast
+ - id: check-yaml
+ - id: check-case-conflict
+ - id: check-docstring-first
+ - id: check-merge-conflict
+ - id: debug-statements
+ - repo: https://github.com/timothycrosley/isort
+ rev: 5.4.2
+ hooks:
+ - id: isort
+ - repo: https://github.com/asottile/pyupgrade
+ rev: v2.7.2
+ hooks:
+ - id: pyupgrade
+ - repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.6.0
+ hooks:
+ - id: python-no-eval
+ - id: python-check-blanket-noqa
+ - id: rst-backticks
+ - repo: https://github.com/psf/black
+ rev: 20.8b1
+ hooks:
+ - id: black
+ args: [--safe, --quiet]
+ - repo: https://gitlab.com/pycqa/flake8
+ rev: 3.8.3
+ hooks:
+ - id: flake8
+ additional_dependencies: [flake8-bugbear]
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst b/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst
new file mode 100644
index 0000000000..0b55a4522c
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/CHANGELOG.rst
@@ -0,0 +1,598 @@
+Changelog
+=========
+
+0.17.0 (2020-10-2)
+-------------------
+
+- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. This is the last major release to support Python 2.7 and Python 3.5.
+- [NEW] Arrow now properly handles imaginary datetimes during DST shifts. For example:
+
+..code-block:: python
+ >>> just_before = arrow.get(2013, 3, 31, 1, 55, tzinfo="Europe/Paris")
+ >>> just_before.shift(minutes=+10)
+
+
+..code-block:: python
+ >>> before = arrow.get("2018-03-10 23:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific")
+ >>> after = arrow.get("2018-03-11 04:00:00", "YYYY-MM-DD HH:mm:ss", tzinfo="US/Pacific")
+ >>> result=[(t, t.to("utc")) for t in arrow.Arrow.range("hour", before, after)]
+ >>> for r in result:
+ ... print(r)
+ ...
+ (, )
+ (, )
+ (, )
+ (, )
+ (, )
+
+- [NEW] Added ``humanize`` week granularity translation for Tagalog.
+- [CHANGE] Calls to the ``timestamp`` property now emit a ``DeprecationWarning``. In a future release, ``timestamp`` will be changed to a method to align with Python's datetime module. If you would like to continue using the property, please change your code to use the ``int_timestamp`` or ``float_timestamp`` properties instead.
+- [CHANGE] Expanded and improved Catalan locale.
+- [FIX] Fixed a bug that caused ``Arrow.range()`` to incorrectly cut off ranges in certain scenarios when using month, quarter, or year endings.
+- [FIX] Fixed a bug that caused day of week token parsing to be case sensitive.
+- [INTERNAL] A number of functions were reordered in arrow.py for better organization and grouping of related methods. This change will have no impact on usage.
+- [INTERNAL] A minimum tox version is now enforced for compatibility reasons. Contributors must use tox >3.18.0 going forward.
+
+0.16.0 (2020-08-23)
+-------------------
+
+- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.16.x and 0.17.x releases are the last to support Python 2.7 and 3.5.
+- [NEW] Implemented `PEP 495 `_ to handle ambiguous datetimes. This is achieved by the addition of the ``fold`` attribute for Arrow objects. For example:
+
+.. code-block:: python
+
+ >>> before = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm')
+
+ >>> before.fold
+ 0
+ >>> before.ambiguous
+ True
+ >>> after = Arrow(2017, 10, 29, 2, 0, tzinfo='Europe/Stockholm', fold=1)
+
+ >>> after = before.replace(fold=1)
+
+
+- [NEW] Added ``normalize_whitespace`` flag to ``arrow.get``. This is useful for parsing log files and/or any files that may contain inconsistent spacing. For example:
+
+.. code-block:: python
+
+ >>> arrow.get("Jun 1 2005 1:33PM", "MMM D YYYY H:mmA", normalize_whitespace=True)
+
+ >>> arrow.get("2013-036 \t 04:05:06Z", normalize_whitespace=True)
+
+
+0.15.8 (2020-07-23)
+-------------------
+
+- [WARN] Arrow will **drop support** for Python 2.7 and 3.5 in the upcoming 1.0.0 release. The 0.15.x, 0.16.x, and 0.17.x releases are the last to support Python 2.7 and 3.5.
+- [NEW] Added ``humanize`` week granularity translation for Czech.
+- [FIX] ``arrow.get`` will now pick sane defaults when weekdays are passed with particular token combinations, see `#446 `_.
+- [INTERNAL] Moved arrow to an organization. The repo can now be found `here `_.
+- [INTERNAL] Started issuing deprecation warnings for Python 2.7 and 3.5.
+- [INTERNAL] Added Python 3.9 to CI pipeline.
+
+0.15.7 (2020-06-19)
+-------------------
+
+- [NEW] Added a number of built-in format strings. See the `docs `_ for a complete list of supported formats. For example:
+
+.. code-block:: python
+
+ >>> arw = arrow.utcnow()
+ >>> arw.format(arrow.FORMAT_COOKIE)
+ 'Wednesday, 27-May-2020 10:30:35 UTC'
+
+- [NEW] Arrow is now fully compatible with Python 3.9 and PyPy3.
+- [NEW] Added Makefile, tox.ini, and requirements.txt files to the distribution bundle.
+- [NEW] Added French Canadian and Swahili locales.
+- [NEW] Added ``humanize`` week granularity translation for Hebrew, Greek, Macedonian, Swedish, Slovak.
+- [FIX] ms and μs timestamps are now normalized in ``arrow.get()``, ``arrow.fromtimestamp()``, and ``arrow.utcfromtimestamp()``. For example:
+
+.. code-block:: python
+
+ >>> ts = 1591161115194556
+ >>> arw = arrow.get(ts)
+
+ >>> arw.timestamp
+ 1591161115
+
+- [FIX] Refactored and updated Macedonian, Hebrew, Korean, and Portuguese locales.
+
+0.15.6 (2020-04-29)
+-------------------
+
+- [NEW] Added support for parsing and formatting `ISO 8601 week dates `_ via a new token ``W``, for example:
+
+.. code-block:: python
+
+ >>> arrow.get("2013-W29-6", "W")
+
+ >>> utc=arrow.utcnow()
+ >>> utc
+
+ >>> utc.format("W")
+ '2020-W04-4'
+
+- [NEW] Formatting with ``x`` token (microseconds) is now possible, for example:
+
+.. code-block:: python
+
+ >>> dt = arrow.utcnow()
+ >>> dt.format("x")
+ '1585669870688329'
+ >>> dt.format("X")
+ '1585669870'
+
+- [NEW] Added ``humanize`` week granularity translation for German, Italian, Polish & Taiwanese locales.
+- [FIX] Consolidated and simplified German locales.
+- [INTERNAL] Moved testing suite from nosetest/Chai to pytest/pytest-mock.
+- [INTERNAL] Converted xunit-style setup and teardown functions in tests to pytest fixtures.
+- [INTERNAL] Setup Github Actions for CI alongside Travis.
+- [INTERNAL] Help support Arrow's future development by donating to the project on `Open Collective `_.
+
+0.15.5 (2020-01-03)
+-------------------
+
+- [WARN] Python 2 reached EOL on 2020-01-01. arrow will **drop support** for Python 2 in a future release to be decided (see `#739 `_).
+- [NEW] Added bounds parameter to ``span_range``, ``interval`` and ``span`` methods. This allows you to include or exclude the start and end values.
+- [NEW] ``arrow.get()`` can now create arrow objects from a timestamp with a timezone, for example:
+
+.. code-block:: python
+
+ >>> arrow.get(1367900664, tzinfo=tz.gettz('US/Pacific'))
+
+
+- [NEW] ``humanize`` can now combine multiple levels of granularity, for example:
+
+.. code-block:: python
+
+ >>> later140 = arrow.utcnow().shift(seconds=+8400)
+ >>> later140.humanize(granularity="minute")
+ 'in 139 minutes'
+ >>> later140.humanize(granularity=["hour", "minute"])
+ 'in 2 hours and 19 minutes'
+
+- [NEW] Added Hong Kong locale (``zh_hk``).
+- [NEW] Added ``humanize`` week granularity translation for Dutch.
+- [NEW] Numbers are now displayed when using the seconds granularity in ``humanize``.
+- [CHANGE] ``range`` now supports both the singular and plural forms of the ``frames`` argument (e.g. day and days).
+- [FIX] Improved parsing of strings that contain punctuation.
+- [FIX] Improved behaviour of ``humanize`` when singular seconds are involved.
+
+0.15.4 (2019-11-02)
+-------------------
+
+- [FIX] Fixed an issue that caused package installs to fail on Conda Forge.
+
+0.15.3 (2019-11-02)
+-------------------
+
+- [NEW] ``factory.get()`` can now create arrow objects from a ISO calendar tuple, for example:
+
+.. code-block:: python
+
+ >>> arrow.get((2013, 18, 7))
+
+
+- [NEW] Added a new token ``x`` to allow parsing of integer timestamps with milliseconds and microseconds.
+- [NEW] Formatting now supports escaping of characters using the same syntax as parsing, for example:
+
+.. code-block:: python
+
+ >>> arw = arrow.now()
+ >>> fmt = "YYYY-MM-DD h [h] m"
+ >>> arw.format(fmt)
+ '2019-11-02 3 h 32'
+
+- [NEW] Added ``humanize`` week granularity translations for Chinese, Spanish and Vietnamese.
+- [CHANGE] Added ``ParserError`` to module exports.
+- [FIX] Added support for midnight at end of day. See `#703 `_ for details.
+- [INTERNAL] Created Travis build for macOS.
+- [INTERNAL] Test parsing and formatting against full timezone database.
+
+0.15.2 (2019-09-14)
+-------------------
+
+- [NEW] Added ``humanize`` week granularity translations for Portuguese and Brazilian Portuguese.
+- [NEW] Embedded changelog within docs and added release dates to versions.
+- [FIX] Fixed a bug that caused test failures on Windows only, see `#668 `_ for details.
+
+0.15.1 (2019-09-10)
+-------------------
+
+- [NEW] Added ``humanize`` week granularity translations for Japanese.
+- [FIX] Fixed a bug that caused Arrow to fail when passed a negative timestamp string.
+- [FIX] Fixed a bug that caused Arrow to fail when passed a datetime object with ``tzinfo`` of type ``StaticTzInfo``.
+
+0.15.0 (2019-09-08)
+-------------------
+
+- [NEW] Added support for DDD and DDDD ordinal date tokens. The following functionality is now possible: ``arrow.get("1998-045")``, ``arrow.get("1998-45", "YYYY-DDD")``, ``arrow.get("1998-045", "YYYY-DDDD")``.
+- [NEW] ISO 8601 basic format for dates and times is now supported (e.g. ``YYYYMMDDTHHmmssZ``).
+- [NEW] Added ``humanize`` week granularity translations for French, Russian and Swiss German locales.
+- [CHANGE] Timestamps of type ``str`` are no longer supported **without a format string** in the ``arrow.get()`` method. This change was made to support the ISO 8601 basic format and to address bugs such as `#447 `_.
+
+The following will NOT work in v0.15.0:
+
+.. code-block:: python
+
+ >>> arrow.get("1565358758")
+ >>> arrow.get("1565358758.123413")
+
+The following will work in v0.15.0:
+
+.. code-block:: python
+
+ >>> arrow.get("1565358758", "X")
+ >>> arrow.get("1565358758.123413", "X")
+ >>> arrow.get(1565358758)
+ >>> arrow.get(1565358758.123413)
+
+- [CHANGE] When a meridian token (a|A) is passed and no meridians are available for the specified locale (e.g. unsupported or untranslated) a ``ParserError`` is raised.
+- [CHANGE] The timestamp token (``X``) will now match float timestamps of type ``str``: ``arrow.get(“1565358758.123415”, “X”)``.
+- [CHANGE] Strings with leading and/or trailing whitespace will no longer be parsed without a format string. Please see `the docs `_ for ways to handle this.
+- [FIX] The timestamp token (``X``) will now only match on strings that **strictly contain integers and floats**, preventing incorrect matches.
+- [FIX] Most instances of ``arrow.get()`` returning an incorrect ``Arrow`` object from a partial parsing match have been eliminated. The following issue have been addressed: `#91 `_, `#196 `_, `#396 `_, `#434 `_, `#447 `_, `#456 `_, `#519 `_, `#538 `_, `#560 `_.
+
+0.14.7 (2019-09-04)
+-------------------
+
+- [CHANGE] ``ArrowParseWarning`` will no longer be printed on every call to ``arrow.get()`` with a datetime string. The purpose of the warning was to start a conversation about the upcoming 0.15.0 changes and we appreciate all the feedback that the community has given us!
+
+0.14.6 (2019-08-28)
+-------------------
+
+- [NEW] Added support for ``week`` granularity in ``Arrow.humanize()``. For example, ``arrow.utcnow().shift(weeks=-1).humanize(granularity="week")`` outputs "a week ago". This change introduced two new untranslated words, ``week`` and ``weeks``, to all locale dictionaries, so locale contributions are welcome!
+- [NEW] Fully translated the Brazilian Portugese locale.
+- [CHANGE] Updated the Macedonian locale to inherit from a Slavic base.
+- [FIX] Fixed a bug that caused ``arrow.get()`` to ignore tzinfo arguments of type string (e.g. ``arrow.get(tzinfo="Europe/Paris")``).
+- [FIX] Fixed a bug that occurred when ``arrow.Arrow()`` was instantiated with a ``pytz`` tzinfo object.
+- [FIX] Fixed a bug that caused Arrow to fail when passed a sub-second token, that when rounded, had a value greater than 999999 (e.g. ``arrow.get("2015-01-12T01:13:15.9999995")``). Arrow should now accurately propagate the rounding for large sub-second tokens.
+
+0.14.5 (2019-08-09)
+-------------------
+
+- [NEW] Added Afrikaans locale.
+- [CHANGE] Removed deprecated ``replace`` shift functionality. Users looking to pass plural properties to the ``replace`` function to shift values should use ``shift`` instead.
+- [FIX] Fixed bug that occurred when ``factory.get()`` was passed a locale kwarg.
+
+0.14.4 (2019-07-30)
+-------------------
+
+- [FIX] Fixed a regression in 0.14.3 that prevented a tzinfo argument of type string to be passed to the ``get()`` function. Functionality such as ``arrow.get("2019072807", "YYYYMMDDHH", tzinfo="UTC")`` should work as normal again.
+- [CHANGE] Moved ``backports.functools_lru_cache`` dependency from ``extra_requires`` to ``install_requires`` for ``Python 2.7`` installs to fix `#495 `_.
+
+0.14.3 (2019-07-28)
+-------------------
+
+- [NEW] Added full support for Python 3.8.
+- [CHANGE] Added warnings for upcoming factory.get() parsing changes in 0.15.0. Please see `#612 `_ for full details.
+- [FIX] Extensive refactor and update of documentation.
+- [FIX] factory.get() can now construct from kwargs.
+- [FIX] Added meridians to Spanish Locale.
+
+0.14.2 (2019-06-06)
+-------------------
+
+- [CHANGE] Travis CI builds now use tox to lint and run tests.
+- [FIX] Fixed UnicodeDecodeError on certain locales (#600).
+
+0.14.1 (2019-06-06)
+-------------------
+
+- [FIX] Fixed ``ImportError: No module named 'dateutil'`` (#598).
+
+0.14.0 (2019-06-06)
+-------------------
+
+- [NEW] Added provisional support for Python 3.8.
+- [CHANGE] Removed support for EOL Python 3.4.
+- [FIX] Updated setup.py with modern Python standards.
+- [FIX] Upgraded dependencies to latest versions.
+- [FIX] Enabled flake8 and black on travis builds.
+- [FIX] Formatted code using black and isort.
+
+0.13.2 (2019-05-30)
+-------------------
+
+- [NEW] Add is_between method.
+- [FIX] Improved humanize behaviour for near zero durations (#416).
+- [FIX] Correct humanize behaviour with future days (#541).
+- [FIX] Documentation updates.
+- [FIX] Improvements to German Locale.
+
+0.13.1 (2019-02-17)
+-------------------
+
+- [NEW] Add support for Python 3.7.
+- [CHANGE] Remove deprecation decorators for Arrow.range(), Arrow.span_range() and Arrow.interval(), all now return generators, wrap with list() to get old behavior.
+- [FIX] Documentation and docstring updates.
+
+0.13.0 (2019-01-09)
+-------------------
+
+- [NEW] Added support for Python 3.6.
+- [CHANGE] Drop support for Python 2.6/3.3.
+- [CHANGE] Return generator instead of list for Arrow.range(), Arrow.span_range() and Arrow.interval().
+- [FIX] Make arrow.get() work with str & tzinfo combo.
+- [FIX] Make sure special RegEx characters are escaped in format string.
+- [NEW] Added support for ZZZ when formatting.
+- [FIX] Stop using datetime.utcnow() in internals, use datetime.now(UTC) instead.
+- [FIX] Return NotImplemented instead of TypeError in arrow math internals.
+- [NEW] Added Estonian Locale.
+- [FIX] Small fixes to Greek locale.
+- [FIX] TagalogLocale improvements.
+- [FIX] Added test requirements to setup.
+- [FIX] Improve docs for get, now and utcnow methods.
+- [FIX] Correct typo in depreciation warning.
+
+0.12.1
+------
+
+- [FIX] Allow universal wheels to be generated and reliably installed.
+- [FIX] Make humanize respect only_distance when granularity argument is also given.
+
+0.12.0
+------
+
+- [FIX] Compatibility fix for Python 2.x
+
+0.11.0
+------
+
+- [FIX] Fix grammar of ArabicLocale
+- [NEW] Add Nepali Locale
+- [FIX] Fix month name + rename AustriaLocale -> AustrianLocale
+- [FIX] Fix typo in Basque Locale
+- [FIX] Fix grammar in PortugueseBrazilian locale
+- [FIX] Remove pip --user-mirrors flag
+- [NEW] Add Indonesian Locale
+
+0.10.0
+------
+
+- [FIX] Fix getattr off by one for quarter
+- [FIX] Fix negative offset for UTC
+- [FIX] Update arrow.py
+
+0.9.0
+-----
+
+- [NEW] Remove duplicate code
+- [NEW] Support gnu date iso 8601
+- [NEW] Add support for universal wheels
+- [NEW] Slovenian locale
+- [NEW] Slovak locale
+- [NEW] Romanian locale
+- [FIX] respect limit even if end is defined range
+- [FIX] Separate replace & shift functions
+- [NEW] Added tox
+- [FIX] Fix supported Python versions in documentation
+- [NEW] Azerbaijani locale added, locale issue fixed in Turkish.
+- [FIX] Format ParserError's raise message
+
+0.8.0
+-----
+
+- []
+
+0.7.1
+-----
+
+- [NEW] Esperanto locale (batisteo)
+
+0.7.0
+-----
+
+- [FIX] Parse localized strings #228 (swistakm)
+- [FIX] Modify tzinfo parameter in ``get`` api #221 (bottleimp)
+- [FIX] Fix Czech locale (PrehistoricTeam)
+- [FIX] Raise TypeError when adding/subtracting non-dates (itsmeolivia)
+- [FIX] Fix pytz conversion error (Kudo)
+- [FIX] Fix overzealous time truncation in span_range (kdeldycke)
+- [NEW] Humanize for time duration #232 (ybrs)
+- [NEW] Add Thai locale (sipp11)
+- [NEW] Adding Belarusian (be) locale (oire)
+- [NEW] Search date in strings (beenje)
+- [NEW] Note that arrow's tokens differ from strptime's. (offby1)
+
+0.6.0
+-----
+
+- [FIX] Added support for Python 3
+- [FIX] Avoid truncating oversized epoch timestamps. Fixes #216.
+- [FIX] Fixed month abbreviations for Ukrainian
+- [FIX] Fix typo timezone
+- [FIX] A couple of dialect fixes and two new languages
+- [FIX] Spanish locale: ``Miercoles`` should have acute accent
+- [Fix] Fix Finnish grammar
+- [FIX] Fix typo in 'Arrow.floor' docstring
+- [FIX] Use read() utility to open README
+- [FIX] span_range for week frame
+- [NEW] Add minimal support for fractional seconds longer than six digits.
+- [NEW] Adding locale support for Marathi (mr)
+- [NEW] Add count argument to span method
+- [NEW] Improved docs
+
+0.5.1 - 0.5.4
+-------------
+
+- [FIX] test the behavior of simplejson instead of calling for_json directly (tonyseek)
+- [FIX] Add Hebrew Locale (doodyparizada)
+- [FIX] Update documentation location (andrewelkins)
+- [FIX] Update setup.py Development Status level (andrewelkins)
+- [FIX] Case insensitive month match (cshowe)
+
+0.5.0
+-----
+
+- [NEW] struct_time addition. (mhworth)
+- [NEW] Version grep (eirnym)
+- [NEW] Default to ISO 8601 format (emonty)
+- [NEW] Raise TypeError on comparison (sniekamp)
+- [NEW] Adding Macedonian(mk) locale (krisfremen)
+- [FIX] Fix for ISO seconds and fractional seconds (sdispater) (andrewelkins)
+- [FIX] Use correct Dutch wording for "hours" (wbolster)
+- [FIX] Complete the list of english locales (indorilftw)
+- [FIX] Change README to reStructuredText (nyuszika7h)
+- [FIX] Parse lower-cased 'h' (tamentis)
+- [FIX] Slight modifications to Dutch locale (nvie)
+
+0.4.4
+-----
+
+- [NEW] Include the docs in the released tarball
+- [NEW] Czech localization Czech localization for Arrow
+- [NEW] Add fa_ir to locales
+- [FIX] Fixes parsing of time strings with a final Z
+- [FIX] Fixes ISO parsing and formatting for fractional seconds
+- [FIX] test_fromtimestamp sp
+- [FIX] some typos fixed
+- [FIX] removed an unused import statement
+- [FIX] docs table fix
+- [FIX] Issue with specify 'X' template and no template at all to arrow.get
+- [FIX] Fix "import" typo in docs/index.rst
+- [FIX] Fix unit tests for zero passed
+- [FIX] Update layout.html
+- [FIX] In Norwegian and new Norwegian months and weekdays should not be capitalized
+- [FIX] Fixed discrepancy between specifying 'X' to arrow.get and specifying no template
+
+0.4.3
+-----
+
+- [NEW] Turkish locale (Emre)
+- [NEW] Arabic locale (Mosab Ahmad)
+- [NEW] Danish locale (Holmars)
+- [NEW] Icelandic locale (Holmars)
+- [NEW] Hindi locale (Atmb4u)
+- [NEW] Malayalam locale (Atmb4u)
+- [NEW] Finnish locale (Stormpat)
+- [NEW] Portuguese locale (Danielcorreia)
+- [NEW] ``h`` and ``hh`` strings are now supported (Averyonghub)
+- [FIX] An incorrect inflection in the Polish locale has been fixed (Avalanchy)
+- [FIX] ``arrow.get`` now properly handles ``Date`` (Jaapz)
+- [FIX] Tests are now declared in ``setup.py`` and the manifest (Pypingou)
+- [FIX] ``__version__`` has been added to ``__init__.py`` (Sametmax)
+- [FIX] ISO 8601 strings can be parsed without a separator (Ivandiguisto / Root)
+- [FIX] Documentation is now more clear regarding some inputs on ``arrow.get`` (Eriktaubeneck)
+- [FIX] Some documentation links have been fixed (Vrutsky)
+- [FIX] Error messages for parse errors are now more descriptive (Maciej Albin)
+- [FIX] The parser now correctly checks for separators in strings (Mschwager)
+
+0.4.2
+-----
+
+- [NEW] Factory ``get`` method now accepts a single ``Arrow`` argument.
+- [NEW] Tokens SSSS, SSSSS and SSSSSS are supported in parsing.
+- [NEW] ``Arrow`` objects have a ``float_timestamp`` property.
+- [NEW] Vietnamese locale (Iu1nguoi)
+- [NEW] Factory ``get`` method now accepts a list of format strings (Dgilland)
+- [NEW] A MANIFEST.in file has been added (Pypingou)
+- [NEW] Tests can be run directly from ``setup.py`` (Pypingou)
+- [FIX] Arrow docs now list 'day of week' format tokens correctly (Rudolphfroger)
+- [FIX] Several issues with the Korean locale have been resolved (Yoloseem)
+- [FIX] ``humanize`` now correctly returns unicode (Shvechikov)
+- [FIX] ``Arrow`` objects now pickle / unpickle correctly (Yoloseem)
+
+0.4.1
+-----
+
+- [NEW] Table / explanation of formatting & parsing tokens in docs
+- [NEW] Brazilian locale (Augusto2112)
+- [NEW] Dutch locale (OrangeTux)
+- [NEW] Italian locale (Pertux)
+- [NEW] Austrain locale (LeChewbacca)
+- [NEW] Tagalog locale (Marksteve)
+- [FIX] Corrected spelling and day numbers in German locale (LeChewbacca)
+- [FIX] Factory ``get`` method should now handle unicode strings correctly (Bwells)
+- [FIX] Midnight and noon should now parse and format correctly (Bwells)
+
+0.4.0
+-----
+
+- [NEW] Format-free ISO 8601 parsing in factory ``get`` method
+- [NEW] Support for 'week' / 'weeks' in ``span``, ``range``, ``span_range``, ``floor`` and ``ceil``
+- [NEW] Support for 'weeks' in ``replace``
+- [NEW] Norwegian locale (Martinp)
+- [NEW] Japanese locale (CortYuming)
+- [FIX] Timezones no longer show the wrong sign when formatted (Bean)
+- [FIX] Microseconds are parsed correctly from strings (Bsidhom)
+- [FIX] Locale day-of-week is no longer off by one (Cynddl)
+- [FIX] Corrected plurals of Ukrainian and Russian nouns (Catchagain)
+- [CHANGE] Old 0.1 ``arrow`` module method removed
+- [CHANGE] Dropped timestamp support in ``range`` and ``span_range`` (never worked correctly)
+- [CHANGE] Dropped parsing of single string as tz string in factory ``get`` method (replaced by ISO 8601)
+
+0.3.5
+-----
+
+- [NEW] French locale (Cynddl)
+- [NEW] Spanish locale (Slapresta)
+- [FIX] Ranges handle multiple timezones correctly (Ftobia)
+
+0.3.4
+-----
+
+- [FIX] Humanize no longer sometimes returns the wrong month delta
+- [FIX] ``__format__`` works correctly with no format string
+
+0.3.3
+-----
+
+- [NEW] Python 2.6 support
+- [NEW] Initial support for locale-based parsing and formatting
+- [NEW] ArrowFactory class, now proxied as the module API
+- [NEW] ``factory`` api method to obtain a factory for a custom type
+- [FIX] Python 3 support and tests completely ironed out
+
+0.3.2
+-----
+
+- [NEW] Python 3+ support
+
+0.3.1
+-----
+
+- [FIX] The old ``arrow`` module function handles timestamps correctly as it used to
+
+0.3.0
+-----
+
+- [NEW] ``Arrow.replace`` method
+- [NEW] Accept timestamps, datetimes and Arrows for datetime inputs, where reasonable
+- [FIX] ``range`` and ``span_range`` respect end and limit parameters correctly
+- [CHANGE] Arrow objects are no longer mutable
+- [CHANGE] Plural attribute name semantics altered: single -> absolute, plural -> relative
+- [CHANGE] Plural names no longer supported as properties (e.g. ``arrow.utcnow().years``)
+
+0.2.1
+-----
+
+- [NEW] Support for localized humanization
+- [NEW] English, Russian, Greek, Korean, Chinese locales
+
+0.2.0
+-----
+
+- **REWRITE**
+- [NEW] Date parsing
+- [NEW] Date formatting
+- [NEW] ``floor``, ``ceil`` and ``span`` methods
+- [NEW] ``datetime`` interface implementation
+- [NEW] ``clone`` method
+- [NEW] ``get``, ``now`` and ``utcnow`` API methods
+
+0.1.6
+-----
+
+- [NEW] Humanized time deltas
+- [NEW] ``__eq__`` implemented
+- [FIX] Issues with conversions related to daylight savings time resolved
+- [CHANGE] ``__str__`` uses ISO formatting
+
+0.1.5
+-----
+
+- **Started tracking changes**
+- [NEW] Parsing of ISO-formatted time zone offsets (e.g. '+02:30', '-05:00')
+- [NEW] Resolved some issues with timestamps and delta / Olson time zones
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/LICENSE b/openpype/modules/ftrack/python2_vendor/arrow/LICENSE
new file mode 100644
index 0000000000..2bef500de7
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2019 Chris Smith
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in b/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in
new file mode 100644
index 0000000000..d9955ed96a
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/MANIFEST.in
@@ -0,0 +1,3 @@
+include LICENSE CHANGELOG.rst README.rst Makefile requirements.txt tox.ini
+recursive-include tests *.py
+recursive-include docs *.py *.rst *.bat Makefile
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/Makefile b/openpype/modules/ftrack/python2_vendor/arrow/Makefile
new file mode 100644
index 0000000000..f294985dc6
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/Makefile
@@ -0,0 +1,44 @@
+.PHONY: auto test docs clean
+
+auto: build38
+
+build27: PYTHON_VER = python2.7
+build35: PYTHON_VER = python3.5
+build36: PYTHON_VER = python3.6
+build37: PYTHON_VER = python3.7
+build38: PYTHON_VER = python3.8
+build39: PYTHON_VER = python3.9
+
+build27 build35 build36 build37 build38 build39: clean
+ virtualenv venv --python=$(PYTHON_VER)
+ . venv/bin/activate; \
+ pip install -r requirements.txt; \
+ pre-commit install
+
+test:
+ rm -f .coverage coverage.xml
+ . venv/bin/activate; pytest
+
+lint:
+ . venv/bin/activate; pre-commit run --all-files --show-diff-on-failure
+
+docs:
+ rm -rf docs/_build
+ . venv/bin/activate; cd docs; make html
+
+clean: clean-dist
+ rm -rf venv .pytest_cache ./**/__pycache__
+ rm -f .coverage coverage.xml ./**/*.pyc
+
+clean-dist:
+ rm -rf dist build .egg .eggs arrow.egg-info
+
+build-dist:
+ . venv/bin/activate; \
+ pip install -U setuptools twine wheel; \
+ python setup.py sdist bdist_wheel
+
+upload-dist:
+ . venv/bin/activate; twine upload dist/*
+
+publish: test clean-dist build-dist upload-dist clean-dist
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/README.rst b/openpype/modules/ftrack/python2_vendor/arrow/README.rst
new file mode 100644
index 0000000000..69f6c50d81
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/README.rst
@@ -0,0 +1,133 @@
+Arrow: Better dates & times for Python
+======================================
+
+.. start-inclusion-marker-do-not-remove
+
+.. image:: https://github.com/arrow-py/arrow/workflows/tests/badge.svg?branch=master
+ :alt: Build Status
+ :target: https://github.com/arrow-py/arrow/actions?query=workflow%3Atests+branch%3Amaster
+
+.. image:: https://codecov.io/gh/arrow-py/arrow/branch/master/graph/badge.svg
+ :alt: Coverage
+ :target: https://codecov.io/gh/arrow-py/arrow
+
+.. image:: https://img.shields.io/pypi/v/arrow.svg
+ :alt: PyPI Version
+ :target: https://pypi.python.org/pypi/arrow
+
+.. image:: https://img.shields.io/pypi/pyversions/arrow.svg
+ :alt: Supported Python Versions
+ :target: https://pypi.python.org/pypi/arrow
+
+.. image:: https://img.shields.io/pypi/l/arrow.svg
+ :alt: License
+ :target: https://pypi.python.org/pypi/arrow
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :alt: Code Style: Black
+ :target: https://github.com/psf/black
+
+
+**Arrow** is a Python library that offers a sensible and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps. It implements and updates the datetime type, plugging gaps in functionality and providing an intelligent module API that supports many common creation scenarios. Simply put, it helps you work with dates and times with fewer imports and a lot less code.
+
+Arrow is named after the `arrow of time `_ and is heavily inspired by `moment.js `_ and `requests `_.
+
+Why use Arrow over built-in modules?
+------------------------------------
+
+Python's standard library and some other low-level modules have near-complete date, time and timezone functionality, but don't work very well from a usability perspective:
+
+- Too many modules: datetime, time, calendar, dateutil, pytz and more
+- Too many types: date, time, datetime, tzinfo, timedelta, relativedelta, etc.
+- Timezones and timestamp conversions are verbose and unpleasant
+- Timezone naivety is the norm
+- Gaps in functionality: ISO 8601 parsing, timespans, humanization
+
+Features
+--------
+
+- Fully-implemented, drop-in replacement for datetime
+- Supports Python 2.7, 3.5, 3.6, 3.7, 3.8 and 3.9
+- Timezone-aware and UTC by default
+- Provides super-simple creation options for many common input scenarios
+- :code:`shift` method with support for relative offsets, including weeks
+- Formats and parses strings automatically
+- Wide support for ISO 8601
+- Timezone conversion
+- Timestamp available as a property
+- Generates time spans, ranges, floors and ceilings for time frames ranging from microsecond to year
+- Humanizes and supports a growing list of contributed locales
+- Extensible for your own Arrow-derived types
+
+Quick Start
+-----------
+
+Installation
+~~~~~~~~~~~~
+
+To install Arrow, use `pip `_ or `pipenv `_:
+
+.. code-block:: console
+
+ $ pip install -U arrow
+
+Example Usage
+~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ >>> import arrow
+ >>> arrow.get('2013-05-11T21:23:58.970460+07:00')
+
+
+ >>> utc = arrow.utcnow()
+ >>> utc
+
+
+ >>> utc = utc.shift(hours=-1)
+ >>> utc
+
+
+ >>> local = utc.to('US/Pacific')
+ >>> local
+
+
+ >>> local.timestamp
+ 1368303838
+
+ >>> local.format()
+ '2013-05-11 13:23:58 -07:00'
+
+ >>> local.format('YYYY-MM-DD HH:mm:ss ZZ')
+ '2013-05-11 13:23:58 -07:00'
+
+ >>> local.humanize()
+ 'an hour ago'
+
+ >>> local.humanize(locale='ko_kr')
+ '1시간 전'
+
+.. end-inclusion-marker-do-not-remove
+
+Documentation
+-------------
+
+For full documentation, please visit `arrow.readthedocs.io `_.
+
+Contributing
+------------
+
+Contributions are welcome for both code and localizations (adding and updating locales). Begin by gaining familiarity with the Arrow library and its features. Then, jump into contributing:
+
+#. Find an issue or feature to tackle on the `issue tracker `_. Issues marked with the `"good first issue" label `_ may be a great place to start!
+#. Fork `this repository `_ on GitHub and begin making changes in a branch.
+#. Add a few tests to ensure that the bug was fixed or the feature works as expected.
+#. Run the entire test suite and linting checks by running one of the following commands: :code:`tox` (if you have `tox `_ installed) **OR** :code:`make build38 && make test && make lint` (if you do not have Python 3.8 installed, replace :code:`build38` with the latest Python version on your system).
+#. Submit a pull request and await feedback 😃.
+
+If you have any questions along the way, feel free to ask them `here `_.
+
+Support Arrow
+-------------
+
+`Open Collective `_ is an online funding platform that provides tools to raise money and share your finances with full transparency. It is the platform of choice for individuals and companies to make one-time or recurring donations directly to the project. If you are interested in making a financial contribution, please visit the `Arrow collective `_.
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/__init__.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/__init__.py
new file mode 100644
index 0000000000..2883527be8
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/__init__.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+from ._version import __version__
+from .api import get, now, utcnow
+from .arrow import Arrow
+from .factory import ArrowFactory
+from .formatter import (
+ FORMAT_ATOM,
+ FORMAT_COOKIE,
+ FORMAT_RFC822,
+ FORMAT_RFC850,
+ FORMAT_RFC1036,
+ FORMAT_RFC1123,
+ FORMAT_RFC2822,
+ FORMAT_RFC3339,
+ FORMAT_RSS,
+ FORMAT_W3C,
+)
+from .parser import ParserError
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/_version.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/_version.py
new file mode 100644
index 0000000000..fd86b3ee91
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/_version.py
@@ -0,0 +1 @@
+__version__ = "0.17.0"
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/api.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/api.py
new file mode 100644
index 0000000000..a6b7be3de2
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/api.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+"""
+Provides the default implementation of :class:`ArrowFactory `
+methods for use as a module API.
+
+"""
+
+from __future__ import absolute_import
+
+from arrow.factory import ArrowFactory
+
+# internal default factory.
+_factory = ArrowFactory()
+
+
+def get(*args, **kwargs):
+ """Calls the default :class:`ArrowFactory ` ``get`` method."""
+
+ return _factory.get(*args, **kwargs)
+
+
+get.__doc__ = _factory.get.__doc__
+
+
+def utcnow():
+ """Calls the default :class:`ArrowFactory ` ``utcnow`` method."""
+
+ return _factory.utcnow()
+
+
+utcnow.__doc__ = _factory.utcnow.__doc__
+
+
+def now(tz=None):
+ """Calls the default :class:`ArrowFactory ` ``now`` method."""
+
+ return _factory.now(tz)
+
+
+now.__doc__ = _factory.now.__doc__
+
+
+def factory(type):
+ """Returns an :class:`.ArrowFactory` for the specified :class:`Arrow `
+ or derived type.
+
+ :param type: the type, :class:`Arrow ` or derived.
+
+ """
+
+ return ArrowFactory(type)
+
+
+__all__ = ["get", "utcnow", "now", "factory"]
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/arrow.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/arrow.py
new file mode 100644
index 0000000000..4fe9541789
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/arrow.py
@@ -0,0 +1,1584 @@
+# -*- coding: utf-8 -*-
+"""
+Provides the :class:`Arrow ` class, an enhanced ``datetime``
+replacement.
+
+"""
+
+from __future__ import absolute_import
+
+import calendar
+import sys
+import warnings
+from datetime import datetime, timedelta
+from datetime import tzinfo as dt_tzinfo
+from math import trunc
+
+from dateutil import tz as dateutil_tz
+from dateutil.relativedelta import relativedelta
+
+from arrow import formatter, locales, parser, util
+
+if sys.version_info[:2] < (3, 6): # pragma: no cover
+ with warnings.catch_warnings():
+ warnings.simplefilter("default", DeprecationWarning)
+ warnings.warn(
+ "Arrow will drop support for Python 2.7 and 3.5 in the upcoming v1.0.0 release. Please upgrade to "
+ "Python 3.6+ to continue receiving updates for Arrow.",
+ DeprecationWarning,
+ )
+
+
+class Arrow(object):
+ """An :class:`Arrow ` object.
+
+ Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing
+ additional functionality.
+
+ :param year: the calendar year.
+ :param month: the calendar month.
+ :param day: the calendar day.
+ :param hour: (optional) the hour. Defaults to 0.
+ :param minute: (optional) the minute, Defaults to 0.
+ :param second: (optional) the second, Defaults to 0.
+ :param microsecond: (optional) the microsecond. Defaults to 0.
+ :param tzinfo: (optional) A timezone expression. Defaults to UTC.
+ :param fold: (optional) 0 or 1, used to disambiguate repeated times. Defaults to 0.
+
+ .. _tz-expr:
+
+ Recognized timezone expressions:
+
+ - A ``tzinfo`` object.
+ - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
+ - A ``str`` in ISO 8601 style, as in '+07:00'.
+ - A ``str``, one of the following: 'local', 'utc', 'UTC'.
+
+ Usage::
+
+ >>> import arrow
+ >>> arrow.Arrow(2013, 5, 5, 12, 30, 45)
+
+
+ """
+
+ resolution = datetime.resolution
+
+ _ATTRS = ["year", "month", "day", "hour", "minute", "second", "microsecond"]
+ _ATTRS_PLURAL = ["{}s".format(a) for a in _ATTRS]
+ _MONTHS_PER_QUARTER = 3
+ _SECS_PER_MINUTE = float(60)
+ _SECS_PER_HOUR = float(60 * 60)
+ _SECS_PER_DAY = float(60 * 60 * 24)
+ _SECS_PER_WEEK = float(60 * 60 * 24 * 7)
+ _SECS_PER_MONTH = float(60 * 60 * 24 * 30.5)
+ _SECS_PER_YEAR = float(60 * 60 * 24 * 365.25)
+
+ def __init__(
+ self,
+ year,
+ month,
+ day,
+ hour=0,
+ minute=0,
+ second=0,
+ microsecond=0,
+ tzinfo=None,
+ **kwargs
+ ):
+ if tzinfo is None:
+ tzinfo = dateutil_tz.tzutc()
+ # detect that tzinfo is a pytz object (issue #626)
+ elif (
+ isinstance(tzinfo, dt_tzinfo)
+ and hasattr(tzinfo, "localize")
+ and hasattr(tzinfo, "zone")
+ and tzinfo.zone
+ ):
+ tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
+ elif util.isstr(tzinfo):
+ tzinfo = parser.TzinfoParser.parse(tzinfo)
+
+ fold = kwargs.get("fold", 0)
+
+ # use enfold here to cover direct arrow.Arrow init on 2.7/3.5
+ self._datetime = dateutil_tz.enfold(
+ datetime(year, month, day, hour, minute, second, microsecond, tzinfo),
+ fold=fold,
+ )
+
+ # factories: single object, both original and from datetime.
+
+ @classmethod
+ def now(cls, tzinfo=None):
+ """Constructs an :class:`Arrow ` object, representing "now" in the given
+ timezone.
+
+ :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.
+
+ Usage::
+
+ >>> arrow.now('Asia/Baku')
+
+
+ """
+
+ if tzinfo is None:
+ tzinfo = dateutil_tz.tzlocal()
+
+ dt = datetime.now(tzinfo)
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ dt.tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ @classmethod
+ def utcnow(cls):
+ """Constructs an :class:`Arrow ` object, representing "now" in UTC
+ time.
+
+ Usage::
+
+ >>> arrow.utcnow()
+
+
+ """
+
+ dt = datetime.now(dateutil_tz.tzutc())
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ dt.tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ @classmethod
+ def fromtimestamp(cls, timestamp, tzinfo=None):
+ """Constructs an :class:`Arrow ` object from a timestamp, converted to
+ the given timezone.
+
+ :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.
+ :param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time.
+ """
+
+ if tzinfo is None:
+ tzinfo = dateutil_tz.tzlocal()
+ elif util.isstr(tzinfo):
+ tzinfo = parser.TzinfoParser.parse(tzinfo)
+
+ if not util.is_timestamp(timestamp):
+ raise ValueError(
+ "The provided timestamp '{}' is invalid.".format(timestamp)
+ )
+
+ timestamp = util.normalize_timestamp(float(timestamp))
+ dt = datetime.fromtimestamp(timestamp, tzinfo)
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ dt.tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ @classmethod
+ def utcfromtimestamp(cls, timestamp):
+ """Constructs an :class:`Arrow ` object from a timestamp, in UTC time.
+
+ :param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either.
+
+ """
+
+ if not util.is_timestamp(timestamp):
+ raise ValueError(
+ "The provided timestamp '{}' is invalid.".format(timestamp)
+ )
+
+ timestamp = util.normalize_timestamp(float(timestamp))
+ dt = datetime.utcfromtimestamp(timestamp)
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ dateutil_tz.tzutc(),
+ fold=getattr(dt, "fold", 0),
+ )
+
+ @classmethod
+ def fromdatetime(cls, dt, tzinfo=None):
+ """Constructs an :class:`Arrow ` object from a ``datetime`` and
+ optional replacement timezone.
+
+ :param dt: the ``datetime``
+ :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to ``dt``'s
+ timezone, or UTC if naive.
+
+ If you only want to replace the timezone of naive datetimes::
+
+ >>> dt
+ datetime.datetime(2013, 5, 5, 0, 0, tzinfo=tzutc())
+ >>> arrow.Arrow.fromdatetime(dt, dt.tzinfo or 'US/Pacific')
+
+
+ """
+
+ if tzinfo is None:
+ if dt.tzinfo is None:
+ tzinfo = dateutil_tz.tzutc()
+ else:
+ tzinfo = dt.tzinfo
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ @classmethod
+ def fromdate(cls, date, tzinfo=None):
+ """Constructs an :class:`Arrow ` object from a ``date`` and optional
+ replacement timezone. Time values are set to 0.
+
+ :param date: the ``date``
+ :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to UTC.
+ """
+
+ if tzinfo is None:
+ tzinfo = dateutil_tz.tzutc()
+
+ return cls(date.year, date.month, date.day, tzinfo=tzinfo)
+
+ @classmethod
+ def strptime(cls, date_str, fmt, tzinfo=None):
+ """Constructs an :class:`Arrow ` object from a date string and format,
+ in the style of ``datetime.strptime``. Optionally replaces the parsed timezone.
+
+ :param date_str: the date string.
+ :param fmt: the format string.
+ :param tzinfo: (optional) A :ref:`timezone expression `. Defaults to the parsed
+ timezone if ``fmt`` contains a timezone directive, otherwise UTC.
+
+ Usage::
+
+ >>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S')
+
+
+ """
+
+ dt = datetime.strptime(date_str, fmt)
+ if tzinfo is None:
+ tzinfo = dt.tzinfo
+
+ return cls(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ # factories: ranges and spans
+
+ @classmethod
+ def range(cls, frame, start, end=None, tz=None, limit=None):
+ """Returns an iterator of :class:`Arrow ` objects, representing
+ points in time between two inputs.
+
+ :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...).
+ :param start: A datetime expression, the start of the range.
+ :param end: (optional) A datetime expression, the end of the range.
+ :param tz: (optional) A :ref:`timezone expression `. Defaults to
+ ``start``'s timezone, or UTC if ``start`` is naive.
+ :param limit: (optional) A maximum number of tuples to return.
+
+ **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to
+ return the entire range. Call with ``limit`` alone to return a maximum # of results from
+ the start. Call with both to cap a range at a maximum # of results.
+
+ **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before
+ iterating. As such, either call with naive objects and ``tz``, or aware objects from the
+ same timezone and no ``tz``.
+
+ Supported frame values: year, quarter, month, week, day, hour, minute, second.
+
+ Recognized datetime expressions:
+
+ - An :class:`Arrow ` object.
+ - A ``datetime`` object.
+
+ Usage::
+
+ >>> start = datetime(2013, 5, 5, 12, 30)
+ >>> end = datetime(2013, 5, 5, 17, 15)
+ >>> for r in arrow.Arrow.range('hour', start, end):
+ ... print(repr(r))
+ ...
+
+
+
+
+
+
+ **NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator::
+
+ >>> start = datetime(2013, 5, 5, 12, 30)
+ >>> end = datetime(2013, 5, 5, 13, 30)
+ >>> for r in arrow.Arrow.range('hour', start, end):
+ ... print(repr(r))
+ ...
+
+
+
+ """
+
+ _, frame_relative, relative_steps = cls._get_frames(frame)
+
+ tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
+
+ start = cls._get_datetime(start).replace(tzinfo=tzinfo)
+ end, limit = cls._get_iteration_params(end, limit)
+ end = cls._get_datetime(end).replace(tzinfo=tzinfo)
+
+ current = cls.fromdatetime(start)
+ original_day = start.day
+ day_is_clipped = False
+ i = 0
+
+ while current <= end and i < limit:
+ i += 1
+ yield current
+
+ values = [getattr(current, f) for f in cls._ATTRS]
+ current = cls(*values, tzinfo=tzinfo).shift(
+ **{frame_relative: relative_steps}
+ )
+
+ if frame in ["month", "quarter", "year"] and current.day < original_day:
+ day_is_clipped = True
+
+ if day_is_clipped and not cls._is_last_day_of_month(current):
+ current = current.replace(day=original_day)
+
+ def span(self, frame, count=1, bounds="[)"):
+ """Returns two new :class:`Arrow ` objects, representing the timespan
+ of the :class:`Arrow ` object in a given timeframe.
+
+ :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
+ :param count: (optional) the number of frames to span.
+ :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies
+ whether to include or exclude the start and end values in the span. '(' excludes
+ the start, '[' includes the start, ')' excludes the end, and ']' includes the end.
+ If the bounds are not specified, the default bound '[)' is used.
+
+ Supported frame values: year, quarter, month, week, day, hour, minute, second.
+
+ Usage::
+
+ >>> arrow.utcnow()
+
+
+ >>> arrow.utcnow().span('hour')
+ (, )
+
+ >>> arrow.utcnow().span('day')
+ (, )
+
+ >>> arrow.utcnow().span('day', count=2)
+ (, )
+
+ >>> arrow.utcnow().span('day', bounds='[]')
+ (, )
+
+ """
+
+ util.validate_bounds(bounds)
+
+ frame_absolute, frame_relative, relative_steps = self._get_frames(frame)
+
+ if frame_absolute == "week":
+ attr = "day"
+ elif frame_absolute == "quarter":
+ attr = "month"
+ else:
+ attr = frame_absolute
+
+ index = self._ATTRS.index(attr)
+ frames = self._ATTRS[: index + 1]
+
+ values = [getattr(self, f) for f in frames]
+
+ for _ in range(3 - len(values)):
+ values.append(1)
+
+ floor = self.__class__(*values, tzinfo=self.tzinfo)
+
+ if frame_absolute == "week":
+ floor = floor.shift(days=-(self.isoweekday() - 1))
+ elif frame_absolute == "quarter":
+ floor = floor.shift(months=-((self.month - 1) % 3))
+
+ ceil = floor.shift(**{frame_relative: count * relative_steps})
+
+ if bounds[0] == "(":
+ floor = floor.shift(microseconds=+1)
+
+ if bounds[1] == ")":
+ ceil = ceil.shift(microseconds=-1)
+
+ return floor, ceil
+
+ def floor(self, frame):
+ """Returns a new :class:`Arrow ` object, representing the "floor"
+ of the timespan of the :class:`Arrow ` object in a given timeframe.
+ Equivalent to the first element in the 2-tuple returned by
+ :func:`span `.
+
+ :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
+
+ Usage::
+
+ >>> arrow.utcnow().floor('hour')
+
+ """
+
+ return self.span(frame)[0]
+
+ def ceil(self, frame):
+ """Returns a new :class:`Arrow ` object, representing the "ceiling"
+ of the timespan of the :class:`Arrow ` object in a given timeframe.
+ Equivalent to the second element in the 2-tuple returned by
+ :func:`span `.
+
+ :param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
+
+ Usage::
+
+ >>> arrow.utcnow().ceil('hour')
+
+ """
+
+ return self.span(frame)[1]
+
+ @classmethod
+ def span_range(cls, frame, start, end, tz=None, limit=None, bounds="[)"):
+ """Returns an iterator of tuples, each :class:`Arrow ` objects,
+ representing a series of timespans between two inputs.
+
+ :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...).
+ :param start: A datetime expression, the start of the range.
+ :param end: (optional) A datetime expression, the end of the range.
+ :param tz: (optional) A :ref:`timezone expression `. Defaults to
+ ``start``'s timezone, or UTC if ``start`` is naive.
+ :param limit: (optional) A maximum number of tuples to return.
+ :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies
+ whether to include or exclude the start and end values in each span in the range. '(' excludes
+ the start, '[' includes the start, ')' excludes the end, and ']' includes the end.
+ If the bounds are not specified, the default bound '[)' is used.
+
+ **NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to
+ return the entire range. Call with ``limit`` alone to return a maximum # of results from
+ the start. Call with both to cap a range at a maximum # of results.
+
+ **NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before
+ iterating. As such, either call with naive objects and ``tz``, or aware objects from the
+ same timezone and no ``tz``.
+
+ Supported frame values: year, quarter, month, week, day, hour, minute, second.
+
+ Recognized datetime expressions:
+
+ - An :class:`Arrow ` object.
+ - A ``datetime`` object.
+
+ **NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned
+ iterator of timespans.
+
+ Usage:
+
+ >>> start = datetime(2013, 5, 5, 12, 30)
+ >>> end = datetime(2013, 5, 5, 17, 15)
+ >>> for r in arrow.Arrow.span_range('hour', start, end):
+ ... print(r)
+ ...
+ (, )
+ (, )
+ (, )
+ (, )
+ (, )
+ (, )
+
+ """
+
+ tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz)
+ start = cls.fromdatetime(start, tzinfo).span(frame)[0]
+ _range = cls.range(frame, start, end, tz, limit)
+ return (r.span(frame, bounds=bounds) for r in _range)
+
+ @classmethod
+ def interval(cls, frame, start, end, interval=1, tz=None, bounds="[)"):
+ """Returns an iterator of tuples, each :class:`Arrow ` objects,
+ representing a series of intervals between two inputs.
+
+ :param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...).
+ :param start: A datetime expression, the start of the range.
+ :param end: (optional) A datetime expression, the end of the range.
+ :param interval: (optional) Time interval for the given time frame.
+ :param tz: (optional) A timezone expression. Defaults to UTC.
+ :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies
+ whether to include or exclude the start and end values in the intervals. '(' excludes
+ the start, '[' includes the start, ')' excludes the end, and ']' includes the end.
+ If the bounds are not specified, the default bound '[)' is used.
+
+ Supported frame values: year, quarter, month, week, day, hour, minute, second
+
+ Recognized datetime expressions:
+
+ - An :class:`Arrow ` object.
+ - A ``datetime`` object.
+
+ Recognized timezone expressions:
+
+ - A ``tzinfo`` object.
+ - A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'.
+ - A ``str`` in ISO 8601 style, as in '+07:00'.
+ - A ``str``, one of the following: 'local', 'utc', 'UTC'.
+
+ Usage:
+
+ >>> start = datetime(2013, 5, 5, 12, 30)
+ >>> end = datetime(2013, 5, 5, 17, 15)
+ >>> for r in arrow.Arrow.interval('hour', start, end, 2):
+ ... print r
+ ...
+ (, )
+ (, )
+ (, )
+ """
+ if interval < 1:
+ raise ValueError("interval has to be a positive integer")
+
+ spanRange = iter(cls.span_range(frame, start, end, tz, bounds=bounds))
+ while True:
+ try:
+ intvlStart, intvlEnd = next(spanRange)
+ for _ in range(interval - 1):
+ _, intvlEnd = next(spanRange)
+ yield intvlStart, intvlEnd
+ except StopIteration:
+ return
+
+ # representations
+
+ def __repr__(self):
+ return "<{} [{}]>".format(self.__class__.__name__, self.__str__())
+
+ def __str__(self):
+ return self._datetime.isoformat()
+
+ def __format__(self, formatstr):
+
+ if len(formatstr) > 0:
+ return self.format(formatstr)
+
+ return str(self)
+
+ def __hash__(self):
+ return self._datetime.__hash__()
+
+ # attributes and properties
+
+ def __getattr__(self, name):
+
+ if name == "week":
+ return self.isocalendar()[1]
+
+ if name == "quarter":
+ return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1
+
+ if not name.startswith("_"):
+ value = getattr(self._datetime, name, None)
+
+ if value is not None:
+ return value
+
+ return object.__getattribute__(self, name)
+
+ @property
+ def tzinfo(self):
+ """Gets the ``tzinfo`` of the :class:`Arrow ` object.
+
+ Usage::
+
+ >>> arw=arrow.utcnow()
+ >>> arw.tzinfo
+ tzutc()
+
+ """
+
+ return self._datetime.tzinfo
+
+ @tzinfo.setter
+ def tzinfo(self, tzinfo):
+ """ Sets the ``tzinfo`` of the :class:`Arrow ` object. """
+
+ self._datetime = self._datetime.replace(tzinfo=tzinfo)
+
+ @property
+ def datetime(self):
+ """Returns a datetime representation of the :class:`Arrow ` object.
+
+ Usage::
+
+ >>> arw=arrow.utcnow()
+ >>> arw.datetime
+ datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc())
+
+ """
+
+ return self._datetime
+
+ @property
+ def naive(self):
+ """Returns a naive datetime representation of the :class:`Arrow `
+ object.
+
+ Usage::
+
+ >>> nairobi = arrow.now('Africa/Nairobi')
+ >>> nairobi
+
+ >>> nairobi.naive
+ datetime.datetime(2019, 1, 23, 19, 27, 12, 297999)
+
+ """
+
+ return self._datetime.replace(tzinfo=None)
+
+ @property
+ def timestamp(self):
+ """Returns a timestamp representation of the :class:`Arrow ` object, in
+ UTC time.
+
+ Usage::
+
+ >>> arrow.utcnow().timestamp
+ 1548260567
+
+ """
+
+ warnings.warn(
+ "For compatibility with the datetime.timestamp() method this property will be replaced with a method in "
+ "the 1.0.0 release, please switch to the .int_timestamp property for identical behaviour as soon as "
+ "possible.",
+ DeprecationWarning,
+ )
+ return calendar.timegm(self._datetime.utctimetuple())
+
+ @property
+ def int_timestamp(self):
+ """Returns a timestamp representation of the :class:`Arrow ` object, in
+ UTC time.
+
+ Usage::
+
+ >>> arrow.utcnow().int_timestamp
+ 1548260567
+
+ """
+
+ return calendar.timegm(self._datetime.utctimetuple())
+
+ @property
+ def float_timestamp(self):
+ """Returns a floating-point representation of the :class:`Arrow `
+ object, in UTC time.
+
+ Usage::
+
+ >>> arrow.utcnow().float_timestamp
+ 1548260516.830896
+
+ """
+
+ # IDEA get rid of this in 1.0.0 and wrap datetime.timestamp()
+ # Or for compatibility retain this but make it call the timestamp method
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ return self.timestamp + float(self.microsecond) / 1000000
+
+ @property
+ def fold(self):
+ """ Returns the ``fold`` value of the :class:`Arrow ` object. """
+
+ # in python < 3.6 _datetime will be a _DatetimeWithFold if fold=1 and a datetime with no fold attribute
+ # otherwise, so we need to return zero to cover the latter case
+ return getattr(self._datetime, "fold", 0)
+
+ @property
+ def ambiguous(self):
+ """ Returns a boolean indicating whether the :class:`Arrow ` object is ambiguous."""
+
+ return dateutil_tz.datetime_ambiguous(self._datetime)
+
+ @property
+ def imaginary(self):
+ """Indicates whether the :class: `Arrow ` object exists in the current timezone."""
+
+ return not dateutil_tz.datetime_exists(self._datetime)
+
+ # mutation and duplication.
+
+ def clone(self):
+ """Returns a new :class:`Arrow ` object, cloned from the current one.
+
+ Usage:
+
+ >>> arw = arrow.utcnow()
+ >>> cloned = arw.clone()
+
+ """
+
+ return self.fromdatetime(self._datetime)
+
+ def replace(self, **kwargs):
+ """Returns a new :class:`Arrow ` object with attributes updated
+ according to inputs.
+
+ Use property names to set their value absolutely::
+
+ >>> import arrow
+ >>> arw = arrow.utcnow()
+ >>> arw
+
+ >>> arw.replace(year=2014, month=6)
+
+
+ You can also replace the timezone without conversion, using a
+ :ref:`timezone expression `::
+
+ >>> arw.replace(tzinfo=tz.tzlocal())
+
+
+ """
+
+ absolute_kwargs = {}
+
+ for key, value in kwargs.items():
+
+ if key in self._ATTRS:
+ absolute_kwargs[key] = value
+ elif key in ["week", "quarter"]:
+ raise AttributeError("setting absolute {} is not supported".format(key))
+ elif key not in ["tzinfo", "fold"]:
+ raise AttributeError('unknown attribute: "{}"'.format(key))
+
+ current = self._datetime.replace(**absolute_kwargs)
+
+ tzinfo = kwargs.get("tzinfo")
+
+ if tzinfo is not None:
+ tzinfo = self._get_tzinfo(tzinfo)
+ current = current.replace(tzinfo=tzinfo)
+
+ fold = kwargs.get("fold")
+
+ # TODO revisit this once we drop support for 2.7/3.5
+ if fold is not None:
+ current = dateutil_tz.enfold(current, fold=fold)
+
+ return self.fromdatetime(current)
+
+ def shift(self, **kwargs):
+ """Returns a new :class:`Arrow ` object with attributes updated
+ according to inputs.
+
+ Use pluralized property names to relatively shift their current value:
+
+ >>> import arrow
+ >>> arw = arrow.utcnow()
+ >>> arw
+
+ >>> arw.shift(years=1, months=-1)
+
+
+ Day-of-the-week relative shifting can use either Python's weekday numbers
+ (Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's
+ day instances (MO, TU .. SU). When using weekday numbers, the returned
+ date will always be greater than or equal to the starting date.
+
+ Using the above code (which is a Saturday) and asking it to shift to Saturday:
+
+ >>> arw.shift(weekday=5)
+
+
+ While asking for a Monday:
+
+ >>> arw.shift(weekday=0)
+
+
+ """
+
+ relative_kwargs = {}
+ additional_attrs = ["weeks", "quarters", "weekday"]
+
+ for key, value in kwargs.items():
+
+ if key in self._ATTRS_PLURAL or key in additional_attrs:
+ relative_kwargs[key] = value
+ else:
+ raise AttributeError(
+ "Invalid shift time frame. Please select one of the following: {}.".format(
+ ", ".join(self._ATTRS_PLURAL + additional_attrs)
+ )
+ )
+
+ # core datetime does not support quarters, translate to months.
+ relative_kwargs.setdefault("months", 0)
+ relative_kwargs["months"] += (
+ relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER
+ )
+
+ current = self._datetime + relativedelta(**relative_kwargs)
+
+ if not dateutil_tz.datetime_exists(current):
+ current = dateutil_tz.resolve_imaginary(current)
+
+ return self.fromdatetime(current)
+
+ def to(self, tz):
+ """Returns a new :class:`Arrow ` object, converted
+ to the target timezone.
+
+ :param tz: A :ref:`timezone expression `.
+
+ Usage::
+
+ >>> utc = arrow.utcnow()
+ >>> utc
+
+
+ >>> utc.to('US/Pacific')
+
+
+ >>> utc.to(tz.tzlocal())
+
+
+ >>> utc.to('-07:00')
+
+
+ >>> utc.to('local')
+
+
+ >>> utc.to('local').to('utc')
+
+
+ """
+
+ if not isinstance(tz, dt_tzinfo):
+ tz = parser.TzinfoParser.parse(tz)
+
+ dt = self._datetime.astimezone(tz)
+
+ return self.__class__(
+ dt.year,
+ dt.month,
+ dt.day,
+ dt.hour,
+ dt.minute,
+ dt.second,
+ dt.microsecond,
+ dt.tzinfo,
+ fold=getattr(dt, "fold", 0),
+ )
+
+ # string output and formatting
+
+ def format(self, fmt="YYYY-MM-DD HH:mm:ssZZ", locale="en_us"):
+ """Returns a string representation of the :class:`Arrow ` object,
+ formatted according to a format string.
+
+ :param fmt: the format string.
+
+ Usage::
+
+ >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ')
+ '2013-05-09 03:56:47 -00:00'
+
+ >>> arrow.utcnow().format('X')
+ '1368071882'
+
+ >>> arrow.utcnow().format('MMMM DD, YYYY')
+ 'May 09, 2013'
+
+ >>> arrow.utcnow().format()
+ '2013-05-09 03:56:47 -00:00'
+
+ """
+
+ return formatter.DateTimeFormatter(locale).format(self._datetime, fmt)
+
+ def humanize(
+ self, other=None, locale="en_us", only_distance=False, granularity="auto"
+ ):
+ """Returns a localized, humanized representation of a relative difference in time.
+
+ :param other: (optional) an :class:`Arrow ` or ``datetime`` object.
+ Defaults to now in the current :class:`Arrow ` object's timezone.
+ :param locale: (optional) a ``str`` specifying a locale. Defaults to 'en_us'.
+ :param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part.
+ :param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute',
+ 'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings
+
+ Usage::
+
+ >>> earlier = arrow.utcnow().shift(hours=-2)
+ >>> earlier.humanize()
+ '2 hours ago'
+
+ >>> later = earlier.shift(hours=4)
+ >>> later.humanize(earlier)
+ 'in 4 hours'
+
+ """
+
+ locale_name = locale
+ locale = locales.get_locale(locale)
+
+ if other is None:
+ utc = datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc())
+ dt = utc.astimezone(self._datetime.tzinfo)
+
+ elif isinstance(other, Arrow):
+ dt = other._datetime
+
+ elif isinstance(other, datetime):
+ if other.tzinfo is None:
+ dt = other.replace(tzinfo=self._datetime.tzinfo)
+ else:
+ dt = other.astimezone(self._datetime.tzinfo)
+
+ else:
+ raise TypeError(
+ "Invalid 'other' argument of type '{}'. "
+ "Argument must be of type None, Arrow, or datetime.".format(
+ type(other).__name__
+ )
+ )
+
+ if isinstance(granularity, list) and len(granularity) == 1:
+ granularity = granularity[0]
+
+ delta = int(round(util.total_seconds(self._datetime - dt)))
+ sign = -1 if delta < 0 else 1
+ diff = abs(delta)
+ delta = diff
+
+ try:
+ if granularity == "auto":
+ if diff < 10:
+ return locale.describe("now", only_distance=only_distance)
+
+ if diff < 45:
+ seconds = sign * delta
+ return locale.describe(
+ "seconds", seconds, only_distance=only_distance
+ )
+
+ elif diff < 90:
+ return locale.describe("minute", sign, only_distance=only_distance)
+ elif diff < 2700:
+ minutes = sign * int(max(delta / 60, 2))
+ return locale.describe(
+ "minutes", minutes, only_distance=only_distance
+ )
+
+ elif diff < 5400:
+ return locale.describe("hour", sign, only_distance=only_distance)
+ elif diff < 79200:
+ hours = sign * int(max(delta / 3600, 2))
+ return locale.describe("hours", hours, only_distance=only_distance)
+
+ # anything less than 48 hours should be 1 day
+ elif diff < 172800:
+ return locale.describe("day", sign, only_distance=only_distance)
+ elif diff < 554400:
+ days = sign * int(max(delta / 86400, 2))
+ return locale.describe("days", days, only_distance=only_distance)
+
+ elif diff < 907200:
+ return locale.describe("week", sign, only_distance=only_distance)
+ elif diff < 2419200:
+ weeks = sign * int(max(delta / 604800, 2))
+ return locale.describe("weeks", weeks, only_distance=only_distance)
+
+ elif diff < 3888000:
+ return locale.describe("month", sign, only_distance=only_distance)
+ elif diff < 29808000:
+ self_months = self._datetime.year * 12 + self._datetime.month
+ other_months = dt.year * 12 + dt.month
+
+ months = sign * int(max(abs(other_months - self_months), 2))
+
+ return locale.describe(
+ "months", months, only_distance=only_distance
+ )
+
+ elif diff < 47260800:
+ return locale.describe("year", sign, only_distance=only_distance)
+ else:
+ years = sign * int(max(delta / 31536000, 2))
+ return locale.describe("years", years, only_distance=only_distance)
+
+ elif util.isstr(granularity):
+ if granularity == "second":
+ delta = sign * delta
+ if abs(delta) < 2:
+ return locale.describe("now", only_distance=only_distance)
+ elif granularity == "minute":
+ delta = sign * delta / self._SECS_PER_MINUTE
+ elif granularity == "hour":
+ delta = sign * delta / self._SECS_PER_HOUR
+ elif granularity == "day":
+ delta = sign * delta / self._SECS_PER_DAY
+ elif granularity == "week":
+ delta = sign * delta / self._SECS_PER_WEEK
+ elif granularity == "month":
+ delta = sign * delta / self._SECS_PER_MONTH
+ elif granularity == "year":
+ delta = sign * delta / self._SECS_PER_YEAR
+ else:
+ raise AttributeError(
+ "Invalid level of granularity. Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'"
+ )
+
+ if trunc(abs(delta)) != 1:
+ granularity += "s"
+ return locale.describe(granularity, delta, only_distance=only_distance)
+
+ else:
+ timeframes = []
+ if "year" in granularity:
+ years = sign * delta / self._SECS_PER_YEAR
+ delta %= self._SECS_PER_YEAR
+ timeframes.append(["year", years])
+
+ if "month" in granularity:
+ months = sign * delta / self._SECS_PER_MONTH
+ delta %= self._SECS_PER_MONTH
+ timeframes.append(["month", months])
+
+ if "week" in granularity:
+ weeks = sign * delta / self._SECS_PER_WEEK
+ delta %= self._SECS_PER_WEEK
+ timeframes.append(["week", weeks])
+
+ if "day" in granularity:
+ days = sign * delta / self._SECS_PER_DAY
+ delta %= self._SECS_PER_DAY
+ timeframes.append(["day", days])
+
+ if "hour" in granularity:
+ hours = sign * delta / self._SECS_PER_HOUR
+ delta %= self._SECS_PER_HOUR
+ timeframes.append(["hour", hours])
+
+ if "minute" in granularity:
+ minutes = sign * delta / self._SECS_PER_MINUTE
+ delta %= self._SECS_PER_MINUTE
+ timeframes.append(["minute", minutes])
+
+ if "second" in granularity:
+ seconds = sign * delta
+ timeframes.append(["second", seconds])
+
+ if len(timeframes) < len(granularity):
+ raise AttributeError(
+ "Invalid level of granularity. "
+ "Please select between 'second', 'minute', 'hour', 'day', 'week', 'month' or 'year'."
+ )
+
+ for tf in timeframes:
+ # Make granularity plural if the delta is not equal to 1
+ if trunc(abs(tf[1])) != 1:
+ tf[0] += "s"
+ return locale.describe_multi(timeframes, only_distance=only_distance)
+
+ except KeyError as e:
+ raise ValueError(
+ "Humanization of the {} granularity is not currently translated in the '{}' locale. "
+ "Please consider making a contribution to this locale.".format(
+ e, locale_name
+ )
+ )
+
+ # query functions
+
+ def is_between(self, start, end, bounds="()"):
+ """Returns a boolean denoting whether the specified date and time is between
+ the start and end dates and times.
+
+ :param start: an :class:`Arrow ` object.
+ :param end: an :class:`Arrow ` object.
+ :param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies
+ whether to include or exclude the start and end values in the range. '(' excludes
+ the start, '[' includes the start, ')' excludes the end, and ']' includes the end.
+ If the bounds are not specified, the default bound '()' is used.
+
+ Usage::
+
+ >>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10))
+ >>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36))
+ >>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end)
+ True
+
+ >>> start = arrow.get(datetime(2013, 5, 5))
+ >>> end = arrow.get(datetime(2013, 5, 8))
+ >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]')
+ True
+
+ >>> start = arrow.get(datetime(2013, 5, 5))
+ >>> end = arrow.get(datetime(2013, 5, 8))
+ >>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)')
+ False
+
+ """
+
+ util.validate_bounds(bounds)
+
+ if not isinstance(start, Arrow):
+ raise TypeError(
+ "Can't parse start date argument type of '{}'".format(type(start))
+ )
+
+ if not isinstance(end, Arrow):
+ raise TypeError(
+ "Can't parse end date argument type of '{}'".format(type(end))
+ )
+
+ include_start = bounds[0] == "["
+ include_end = bounds[1] == "]"
+
+ target_timestamp = self.float_timestamp
+ start_timestamp = start.float_timestamp
+ end_timestamp = end.float_timestamp
+
+ if include_start and include_end:
+ return (
+ target_timestamp >= start_timestamp
+ and target_timestamp <= end_timestamp
+ )
+ elif include_start and not include_end:
+ return (
+ target_timestamp >= start_timestamp and target_timestamp < end_timestamp
+ )
+ elif not include_start and include_end:
+ return (
+ target_timestamp > start_timestamp and target_timestamp <= end_timestamp
+ )
+ else:
+ return (
+ target_timestamp > start_timestamp and target_timestamp < end_timestamp
+ )
+
+ # datetime methods
+
+ def date(self):
+ """Returns a ``date`` object with the same year, month and day.
+
+ Usage::
+
+ >>> arrow.utcnow().date()
+ datetime.date(2019, 1, 23)
+
+ """
+
+ return self._datetime.date()
+
+ def time(self):
+ """Returns a ``time`` object with the same hour, minute, second, microsecond.
+
+ Usage::
+
+ >>> arrow.utcnow().time()
+ datetime.time(12, 15, 34, 68352)
+
+ """
+
+ return self._datetime.time()
+
+ def timetz(self):
+ """Returns a ``time`` object with the same hour, minute, second, microsecond and
+ tzinfo.
+
+ Usage::
+
+ >>> arrow.utcnow().timetz()
+ datetime.time(12, 5, 18, 298893, tzinfo=tzutc())
+
+ """
+
+ return self._datetime.timetz()
+
+ def astimezone(self, tz):
+ """Returns a ``datetime`` object, converted to the specified timezone.
+
+ :param tz: a ``tzinfo`` object.
+
+ Usage::
+
+ >>> pacific=arrow.now('US/Pacific')
+ >>> nyc=arrow.now('America/New_York').tzinfo
+ >>> pacific.astimezone(nyc)
+ datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
+
+ """
+
+ return self._datetime.astimezone(tz)
+
+ def utcoffset(self):
+ """Returns a ``timedelta`` object representing the whole number of minutes difference from
+ UTC time.
+
+ Usage::
+
+ >>> arrow.now('US/Pacific').utcoffset()
+ datetime.timedelta(-1, 57600)
+
+ """
+
+ return self._datetime.utcoffset()
+
+ def dst(self):
+ """Returns the daylight savings time adjustment.
+
+ Usage::
+
+ >>> arrow.utcnow().dst()
+ datetime.timedelta(0)
+
+ """
+
+ return self._datetime.dst()
+
+ def timetuple(self):
+ """Returns a ``time.struct_time``, in the current timezone.
+
+ Usage::
+
+ >>> arrow.utcnow().timetuple()
+ time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0)
+
+ """
+
+ return self._datetime.timetuple()
+
+ def utctimetuple(self):
+ """Returns a ``time.struct_time``, in UTC time.
+
+ Usage::
+
+ >>> arrow.utcnow().utctimetuple()
+ time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0)
+
+ """
+
+ return self._datetime.utctimetuple()
+
+ def toordinal(self):
+ """Returns the proleptic Gregorian ordinal of the date.
+
+ Usage::
+
+ >>> arrow.utcnow().toordinal()
+ 737078
+
+ """
+
+ return self._datetime.toordinal()
+
+ def weekday(self):
+ """Returns the day of the week as an integer (0-6).
+
+ Usage::
+
+ >>> arrow.utcnow().weekday()
+ 5
+
+ """
+
+ return self._datetime.weekday()
+
+ def isoweekday(self):
+ """Returns the ISO day of the week as an integer (1-7).
+
+ Usage::
+
+ >>> arrow.utcnow().isoweekday()
+ 6
+
+ """
+
+ return self._datetime.isoweekday()
+
+ def isocalendar(self):
+ """Returns a 3-tuple, (ISO year, ISO week number, ISO weekday).
+
+ Usage::
+
+ >>> arrow.utcnow().isocalendar()
+ (2019, 3, 6)
+
+ """
+
+ return self._datetime.isocalendar()
+
+ def isoformat(self, sep="T"):
+ """Returns an ISO 8601 formatted representation of the date and time.
+
+ Usage::
+
+ >>> arrow.utcnow().isoformat()
+ '2019-01-19T18:30:52.442118+00:00'
+
+ """
+
+ return self._datetime.isoformat(sep)
+
+ def ctime(self):
+ """Returns a ctime formatted representation of the date and time.
+
+ Usage::
+
+ >>> arrow.utcnow().ctime()
+ 'Sat Jan 19 18:26:50 2019'
+
+ """
+
+ return self._datetime.ctime()
+
+ def strftime(self, format):
+ """Formats in the style of ``datetime.strftime``.
+
+ :param format: the format string.
+
+ Usage::
+
+ >>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S')
+ '23-01-2019 12:28:17'
+
+ """
+
+ return self._datetime.strftime(format)
+
+ def for_json(self):
+ """Serializes for the ``for_json`` protocol of simplejson.
+
+ Usage::
+
+ >>> arrow.utcnow().for_json()
+ '2019-01-19T18:25:36.760079+00:00'
+
+ """
+
+ return self.isoformat()
+
+ # math
+
+ def __add__(self, other):
+
+ if isinstance(other, (timedelta, relativedelta)):
+ return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)
+
+ return NotImplemented
+
+ def __radd__(self, other):
+ return self.__add__(other)
+
+ def __sub__(self, other):
+
+ if isinstance(other, (timedelta, relativedelta)):
+ return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)
+
+ elif isinstance(other, datetime):
+ return self._datetime - other
+
+ elif isinstance(other, Arrow):
+ return self._datetime - other._datetime
+
+ return NotImplemented
+
+ def __rsub__(self, other):
+
+ if isinstance(other, datetime):
+ return other - self._datetime
+
+ return NotImplemented
+
+ # comparisons
+
+ def __eq__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return False
+
+ return self._datetime == self._get_datetime(other)
+
+ def __ne__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return True
+
+ return not self.__eq__(other)
+
+ def __gt__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return NotImplemented
+
+ return self._datetime > self._get_datetime(other)
+
+ def __ge__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return NotImplemented
+
+ return self._datetime >= self._get_datetime(other)
+
+ def __lt__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return NotImplemented
+
+ return self._datetime < self._get_datetime(other)
+
+ def __le__(self, other):
+
+ if not isinstance(other, (Arrow, datetime)):
+ return NotImplemented
+
+ return self._datetime <= self._get_datetime(other)
+
+ def __cmp__(self, other):
+ if sys.version_info[0] < 3: # pragma: no cover
+ if not isinstance(other, (Arrow, datetime)):
+ raise TypeError(
+ "can't compare '{}' to '{}'".format(type(self), type(other))
+ )
+
+ # internal methods
+
+ @staticmethod
+ def _get_tzinfo(tz_expr):
+
+ if tz_expr is None:
+ return dateutil_tz.tzutc()
+ if isinstance(tz_expr, dt_tzinfo):
+ return tz_expr
+ else:
+ try:
+ return parser.TzinfoParser.parse(tz_expr)
+ except parser.ParserError:
+ raise ValueError("'{}' not recognized as a timezone".format(tz_expr))
+
+ @classmethod
+ def _get_datetime(cls, expr):
+ """Get datetime object for a specified expression."""
+ if isinstance(expr, Arrow):
+ return expr.datetime
+ elif isinstance(expr, datetime):
+ return expr
+ elif util.is_timestamp(expr):
+ timestamp = float(expr)
+ return cls.utcfromtimestamp(timestamp).datetime
+ else:
+ raise ValueError(
+ "'{}' not recognized as a datetime or timestamp.".format(expr)
+ )
+
+ @classmethod
+ def _get_frames(cls, name):
+
+ if name in cls._ATTRS:
+ return name, "{}s".format(name), 1
+ elif name[-1] == "s" and name[:-1] in cls._ATTRS:
+ return name[:-1], name, 1
+ elif name in ["week", "weeks"]:
+ return "week", "weeks", 1
+ elif name in ["quarter", "quarters"]:
+ return "quarter", "months", 3
+
+ supported = ", ".join(
+ [
+ "year(s)",
+ "month(s)",
+ "day(s)",
+ "hour(s)",
+ "minute(s)",
+ "second(s)",
+ "microsecond(s)",
+ "week(s)",
+ "quarter(s)",
+ ]
+ )
+ raise AttributeError(
+ "range/span over frame {} not supported. Supported frames: {}".format(
+ name, supported
+ )
+ )
+
+ @classmethod
+ def _get_iteration_params(cls, end, limit):
+
+ if end is None:
+
+ if limit is None:
+ raise ValueError("one of 'end' or 'limit' is required")
+
+ return cls.max, limit
+
+ else:
+ if limit is None:
+ return end, sys.maxsize
+ return end, limit
+
+ @staticmethod
+ def _is_last_day_of_month(date):
+ return date.day == calendar.monthrange(date.year, date.month)[1]
+
+
+Arrow.min = Arrow.fromdatetime(datetime.min)
+Arrow.max = Arrow.fromdatetime(datetime.max)
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/constants.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/constants.py
new file mode 100644
index 0000000000..81e37b26de
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/constants.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+# Output of time.mktime(datetime.max.timetuple()) on macOS
+# This value must be hardcoded for compatibility with Windows
+# Platform-independent max timestamps are hard to form
+# https://stackoverflow.com/q/46133223
+MAX_TIMESTAMP = 253402318799.0
+MAX_TIMESTAMP_MS = MAX_TIMESTAMP * 1000
+MAX_TIMESTAMP_US = MAX_TIMESTAMP * 1000000
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/factory.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/factory.py
new file mode 100644
index 0000000000..05933e8151
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/factory.py
@@ -0,0 +1,301 @@
+# -*- coding: utf-8 -*-
+"""
+Implements the :class:`ArrowFactory ` class,
+providing factory methods for common :class:`Arrow `
+construction scenarios.
+
+"""
+
+from __future__ import absolute_import
+
+import calendar
+from datetime import date, datetime
+from datetime import tzinfo as dt_tzinfo
+from time import struct_time
+
+from dateutil import tz as dateutil_tz
+
+from arrow import parser
+from arrow.arrow import Arrow
+from arrow.util import is_timestamp, iso_to_gregorian, isstr
+
+
+class ArrowFactory(object):
+ """A factory for generating :class:`Arrow ` objects.
+
+ :param type: (optional) the :class:`Arrow `-based class to construct from.
+ Defaults to :class:`Arrow `.
+
+ """
+
+ def __init__(self, type=Arrow):
+ self.type = type
+
+ def get(self, *args, **kwargs):
+ """Returns an :class:`Arrow ` object based on flexible inputs.
+
+ :param locale: (optional) a ``str`` specifying a locale for the parser. Defaults to 'en_us'.
+ :param tzinfo: (optional) a :ref:`timezone expression ` or tzinfo object.
+ Replaces the timezone unless using an input form that is explicitly UTC or specifies
+ the timezone in a positional argument. Defaults to UTC.
+ :param normalize_whitespace: (optional) a ``bool`` specifying whether or not to normalize
+ redundant whitespace (spaces, tabs, and newlines) in a datetime string before parsing.
+ Defaults to false.
+
+ Usage::
+
+ >>> import arrow
+
+ **No inputs** to get current UTC time::
+
+ >>> arrow.get()
+
+
+ **None** to also get current UTC time::
+
+ >>> arrow.get(None)
+
+
+ **One** :class:`Arrow ` object, to get a copy.
+
+ >>> arw = arrow.utcnow()
+ >>> arrow.get(arw)
+
+
+ **One** ``float`` or ``int``, convertible to a floating-point timestamp, to get
+ that timestamp in UTC::
+
+ >>> arrow.get(1367992474.293378)
+
+
+ >>> arrow.get(1367992474)
+
+
+ **One** ISO 8601-formatted ``str``, to parse it::
+
+ >>> arrow.get('2013-09-29T01:26:43.830580')
+
+
+ **One** ISO 8601-formatted ``str``, in basic format, to parse it::
+
+ >>> arrow.get('20160413T133656.456289')
+
+
+ **One** ``tzinfo``, to get the current time **converted** to that timezone::
+
+ >>> arrow.get(tz.tzlocal())
+
+
+ **One** naive ``datetime``, to get that datetime in UTC::
+
+ >>> arrow.get(datetime(2013, 5, 5))
+
+
+ **One** aware ``datetime``, to get that datetime::
+
+ >>> arrow.get(datetime(2013, 5, 5, tzinfo=tz.tzlocal()))
+
+
+ **One** naive ``date``, to get that date in UTC::
+
+ >>> arrow.get(date(2013, 5, 5))
+
+
+ **One** time.struct time::
+
+ >>> arrow.get(gmtime(0))
+
+
+ **One** iso calendar ``tuple``, to get that week date in UTC::
+
+ >>> arrow.get((2013, 18, 7))
+
+
+ **Two** arguments, a naive or aware ``datetime``, and a replacement
+ :ref:`timezone expression `::
+
+ >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific')
+
+
+ **Two** arguments, a naive ``date``, and a replacement
+ :ref:`timezone expression `::
+
+ >>> arrow.get(date(2013, 5, 5), 'US/Pacific')
+
+
+ **Two** arguments, both ``str``, to parse the first according to the format of the second::
+
+ >>> arrow.get('2013-05-05 12:30:45 America/Chicago', 'YYYY-MM-DD HH:mm:ss ZZZ')
+
+
+ **Two** arguments, first a ``str`` to parse and second a ``list`` of formats to try::
+
+ >>> arrow.get('2013-05-05 12:30:45', ['MM/DD/YYYY', 'YYYY-MM-DD HH:mm:ss'])
+
+
+ **Three or more** arguments, as for the constructor of a ``datetime``::
+
+ >>> arrow.get(2013, 5, 5, 12, 30, 45)
+
+
+ """
+
+ arg_count = len(args)
+ locale = kwargs.pop("locale", "en_us")
+ tz = kwargs.get("tzinfo", None)
+ normalize_whitespace = kwargs.pop("normalize_whitespace", False)
+
+ # if kwargs given, send to constructor unless only tzinfo provided
+ if len(kwargs) > 1:
+ arg_count = 3
+
+ # tzinfo kwarg is not provided
+ if len(kwargs) == 1 and tz is None:
+ arg_count = 3
+
+ # () -> now, @ utc.
+ if arg_count == 0:
+ if isstr(tz):
+ tz = parser.TzinfoParser.parse(tz)
+ return self.type.now(tz)
+
+ if isinstance(tz, dt_tzinfo):
+ return self.type.now(tz)
+
+ return self.type.utcnow()
+
+ if arg_count == 1:
+ arg = args[0]
+
+ # (None) -> now, @ utc.
+ if arg is None:
+ return self.type.utcnow()
+
+ # try (int, float) -> from timestamp with tz
+ elif not isstr(arg) and is_timestamp(arg):
+ if tz is None:
+ # set to UTC by default
+ tz = dateutil_tz.tzutc()
+ return self.type.fromtimestamp(arg, tzinfo=tz)
+
+ # (Arrow) -> from the object's datetime.
+ elif isinstance(arg, Arrow):
+ return self.type.fromdatetime(arg.datetime)
+
+ # (datetime) -> from datetime.
+ elif isinstance(arg, datetime):
+ return self.type.fromdatetime(arg)
+
+ # (date) -> from date.
+ elif isinstance(arg, date):
+ return self.type.fromdate(arg)
+
+ # (tzinfo) -> now, @ tzinfo.
+ elif isinstance(arg, dt_tzinfo):
+ return self.type.now(arg)
+
+ # (str) -> parse.
+ elif isstr(arg):
+ dt = parser.DateTimeParser(locale).parse_iso(arg, normalize_whitespace)
+ return self.type.fromdatetime(dt, tz)
+
+ # (struct_time) -> from struct_time
+ elif isinstance(arg, struct_time):
+ return self.type.utcfromtimestamp(calendar.timegm(arg))
+
+ # (iso calendar) -> convert then from date
+ elif isinstance(arg, tuple) and len(arg) == 3:
+ dt = iso_to_gregorian(*arg)
+ return self.type.fromdate(dt)
+
+ else:
+ raise TypeError(
+ "Can't parse single argument of type '{}'".format(type(arg))
+ )
+
+ elif arg_count == 2:
+
+ arg_1, arg_2 = args[0], args[1]
+
+ if isinstance(arg_1, datetime):
+
+ # (datetime, tzinfo/str) -> fromdatetime replace tzinfo.
+ if isinstance(arg_2, dt_tzinfo) or isstr(arg_2):
+ return self.type.fromdatetime(arg_1, arg_2)
+ else:
+ raise TypeError(
+ "Can't parse two arguments of types 'datetime', '{}'".format(
+ type(arg_2)
+ )
+ )
+
+ elif isinstance(arg_1, date):
+
+ # (date, tzinfo/str) -> fromdate replace tzinfo.
+ if isinstance(arg_2, dt_tzinfo) or isstr(arg_2):
+ return self.type.fromdate(arg_1, tzinfo=arg_2)
+ else:
+ raise TypeError(
+ "Can't parse two arguments of types 'date', '{}'".format(
+ type(arg_2)
+ )
+ )
+
+ # (str, format) -> parse.
+ elif isstr(arg_1) and (isstr(arg_2) or isinstance(arg_2, list)):
+ dt = parser.DateTimeParser(locale).parse(
+ args[0], args[1], normalize_whitespace
+ )
+ return self.type.fromdatetime(dt, tzinfo=tz)
+
+ else:
+ raise TypeError(
+ "Can't parse two arguments of types '{}' and '{}'".format(
+ type(arg_1), type(arg_2)
+ )
+ )
+
+ # 3+ args -> datetime-like via constructor.
+ else:
+ return self.type(*args, **kwargs)
+
+ def utcnow(self):
+ """Returns an :class:`Arrow ` object, representing "now" in UTC time.
+
+ Usage::
+
+ >>> import arrow
+ >>> arrow.utcnow()
+
+ """
+
+ return self.type.utcnow()
+
+ def now(self, tz=None):
+ """Returns an :class:`Arrow ` object, representing "now" in the given
+ timezone.
+
+ :param tz: (optional) A :ref:`timezone expression `. Defaults to local time.
+
+ Usage::
+
+ >>> import arrow
+ >>> arrow.now()
+
+
+ >>> arrow.now('US/Pacific')
+
+
+ >>> arrow.now('+02:00')
+
+
+ >>> arrow.now('local')
+
+ """
+
+ if tz is None:
+ tz = dateutil_tz.tzlocal()
+ elif not isinstance(tz, dt_tzinfo):
+ tz = parser.TzinfoParser.parse(tz)
+
+ return self.type.now(tz)
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/formatter.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/formatter.py
new file mode 100644
index 0000000000..9f9d7a44da
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/formatter.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, division
+
+import calendar
+import re
+
+from dateutil import tz as dateutil_tz
+
+from arrow import locales, util
+
+FORMAT_ATOM = "YYYY-MM-DD HH:mm:ssZZ"
+FORMAT_COOKIE = "dddd, DD-MMM-YYYY HH:mm:ss ZZZ"
+FORMAT_RFC822 = "ddd, DD MMM YY HH:mm:ss Z"
+FORMAT_RFC850 = "dddd, DD-MMM-YY HH:mm:ss ZZZ"
+FORMAT_RFC1036 = "ddd, DD MMM YY HH:mm:ss Z"
+FORMAT_RFC1123 = "ddd, DD MMM YYYY HH:mm:ss Z"
+FORMAT_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss Z"
+FORMAT_RFC3339 = "YYYY-MM-DD HH:mm:ssZZ"
+FORMAT_RSS = "ddd, DD MMM YYYY HH:mm:ss Z"
+FORMAT_W3C = "YYYY-MM-DD HH:mm:ssZZ"
+
+
+class DateTimeFormatter(object):
+
+ # This pattern matches characters enclosed in square brackets are matched as
+ # an atomic group. For more info on atomic groups and how to they are
+ # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578
+
+ _FORMAT_RE = re.compile(
+ r"(\[(?:(?=(?P[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|x|W)"
+ )
+
+ def __init__(self, locale="en_us"):
+
+ self.locale = locales.get_locale(locale)
+
+ def format(cls, dt, fmt):
+
+ return cls._FORMAT_RE.sub(lambda m: cls._format_token(dt, m.group(0)), fmt)
+
+ def _format_token(self, dt, token):
+
+ if token and token.startswith("[") and token.endswith("]"):
+ return token[1:-1]
+
+ if token == "YYYY":
+ return self.locale.year_full(dt.year)
+ if token == "YY":
+ return self.locale.year_abbreviation(dt.year)
+
+ if token == "MMMM":
+ return self.locale.month_name(dt.month)
+ if token == "MMM":
+ return self.locale.month_abbreviation(dt.month)
+ if token == "MM":
+ return "{:02d}".format(dt.month)
+ if token == "M":
+ return str(dt.month)
+
+ if token == "DDDD":
+ return "{:03d}".format(dt.timetuple().tm_yday)
+ if token == "DDD":
+ return str(dt.timetuple().tm_yday)
+ if token == "DD":
+ return "{:02d}".format(dt.day)
+ if token == "D":
+ return str(dt.day)
+
+ if token == "Do":
+ return self.locale.ordinal_number(dt.day)
+
+ if token == "dddd":
+ return self.locale.day_name(dt.isoweekday())
+ if token == "ddd":
+ return self.locale.day_abbreviation(dt.isoweekday())
+ if token == "d":
+ return str(dt.isoweekday())
+
+ if token == "HH":
+ return "{:02d}".format(dt.hour)
+ if token == "H":
+ return str(dt.hour)
+ if token == "hh":
+ return "{:02d}".format(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))
+ if token == "h":
+ return str(dt.hour if 0 < dt.hour < 13 else abs(dt.hour - 12))
+
+ if token == "mm":
+ return "{:02d}".format(dt.minute)
+ if token == "m":
+ return str(dt.minute)
+
+ if token == "ss":
+ return "{:02d}".format(dt.second)
+ if token == "s":
+ return str(dt.second)
+
+ if token == "SSSSSS":
+ return str("{:06d}".format(int(dt.microsecond)))
+ if token == "SSSSS":
+ return str("{:05d}".format(int(dt.microsecond / 10)))
+ if token == "SSSS":
+ return str("{:04d}".format(int(dt.microsecond / 100)))
+ if token == "SSS":
+ return str("{:03d}".format(int(dt.microsecond / 1000)))
+ if token == "SS":
+ return str("{:02d}".format(int(dt.microsecond / 10000)))
+ if token == "S":
+ return str(int(dt.microsecond / 100000))
+
+ if token == "X":
+ # TODO: replace with a call to dt.timestamp() when we drop Python 2.7
+ return str(calendar.timegm(dt.utctimetuple()))
+
+ if token == "x":
+ # TODO: replace with a call to dt.timestamp() when we drop Python 2.7
+ ts = calendar.timegm(dt.utctimetuple()) + (dt.microsecond / 1000000)
+ return str(int(ts * 1000000))
+
+ if token == "ZZZ":
+ return dt.tzname()
+
+ if token in ["ZZ", "Z"]:
+ separator = ":" if token == "ZZ" else ""
+ tz = dateutil_tz.tzutc() if dt.tzinfo is None else dt.tzinfo
+ total_minutes = int(util.total_seconds(tz.utcoffset(dt)) / 60)
+
+ sign = "+" if total_minutes >= 0 else "-"
+ total_minutes = abs(total_minutes)
+ hour, minute = divmod(total_minutes, 60)
+
+ return "{}{:02d}{}{:02d}".format(sign, hour, separator, minute)
+
+ if token in ("a", "A"):
+ return self.locale.meridian(dt.hour, token)
+
+ if token == "W":
+ year, week, day = dt.isocalendar()
+ return "{}-W{:02d}-{}".format(year, week, day)
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/locales.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/locales.py
new file mode 100644
index 0000000000..6833da5a78
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/locales.py
@@ -0,0 +1,4267 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import inspect
+import sys
+from math import trunc
+
+
+def get_locale(name):
+ """Returns an appropriate :class:`Locale `
+ corresponding to an inpute locale name.
+
+ :param name: the name of the locale.
+
+ """
+
+ locale_cls = _locales.get(name.lower())
+
+ if locale_cls is None:
+ raise ValueError("Unsupported locale '{}'".format(name))
+
+ return locale_cls()
+
+
+def get_locale_by_class_name(name):
+ """Returns an appropriate :class:`Locale `
+ corresponding to an locale class name.
+
+ :param name: the name of the locale class.
+
+ """
+ locale_cls = globals().get(name)
+
+ if locale_cls is None:
+ raise ValueError("Unsupported locale '{}'".format(name))
+
+ return locale_cls()
+
+
+# base locale type.
+
+
+class Locale(object):
+ """ Represents locale-specific data and functionality. """
+
+ names = []
+
+ timeframes = {
+ "now": "",
+ "second": "",
+ "seconds": "",
+ "minute": "",
+ "minutes": "",
+ "hour": "",
+ "hours": "",
+ "day": "",
+ "days": "",
+ "week": "",
+ "weeks": "",
+ "month": "",
+ "months": "",
+ "year": "",
+ "years": "",
+ }
+
+ meridians = {"am": "", "pm": "", "AM": "", "PM": ""}
+
+ past = None
+ future = None
+ and_word = None
+
+ month_names = []
+ month_abbreviations = []
+
+ day_names = []
+ day_abbreviations = []
+
+ ordinal_day_re = r"(\d+)"
+
+ def __init__(self):
+
+ self._month_name_to_ordinal = None
+
+ def describe(self, timeframe, delta=0, only_distance=False):
+ """Describes a delta within a timeframe in plain language.
+
+ :param timeframe: a string representing a timeframe.
+ :param delta: a quantity representing a delta in a timeframe.
+ :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords
+ """
+
+ humanized = self._format_timeframe(timeframe, delta)
+ if not only_distance:
+ humanized = self._format_relative(humanized, timeframe, delta)
+
+ return humanized
+
+ def describe_multi(self, timeframes, only_distance=False):
+ """Describes a delta within multiple timeframes in plain language.
+
+ :param timeframes: a list of string, quantity pairs each representing a timeframe and delta.
+ :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords
+ """
+
+ humanized = ""
+ for index, (timeframe, delta) in enumerate(timeframes):
+ humanized += self._format_timeframe(timeframe, delta)
+ if index == len(timeframes) - 2 and self.and_word:
+ humanized += " " + self.and_word + " "
+ elif index < len(timeframes) - 1:
+ humanized += " "
+
+ if not only_distance:
+ humanized = self._format_relative(humanized, timeframe, delta)
+
+ return humanized
+
+ def day_name(self, day):
+ """Returns the day name for a specified day of the week.
+
+ :param day: the ``int`` day of the week (1-7).
+
+ """
+
+ return self.day_names[day]
+
+ def day_abbreviation(self, day):
+ """Returns the day abbreviation for a specified day of the week.
+
+ :param day: the ``int`` day of the week (1-7).
+
+ """
+
+ return self.day_abbreviations[day]
+
+ def month_name(self, month):
+ """Returns the month name for a specified month of the year.
+
+ :param month: the ``int`` month of the year (1-12).
+
+ """
+
+ return self.month_names[month]
+
+ def month_abbreviation(self, month):
+ """Returns the month abbreviation for a specified month of the year.
+
+ :param month: the ``int`` month of the year (1-12).
+
+ """
+
+ return self.month_abbreviations[month]
+
+ def month_number(self, name):
+ """Returns the month number for a month specified by name or abbreviation.
+
+ :param name: the month name or abbreviation.
+
+ """
+
+ if self._month_name_to_ordinal is None:
+ self._month_name_to_ordinal = self._name_to_ordinal(self.month_names)
+ self._month_name_to_ordinal.update(
+ self._name_to_ordinal(self.month_abbreviations)
+ )
+
+ return self._month_name_to_ordinal.get(name)
+
+ def year_full(self, year):
+ """Returns the year for specific locale if available
+
+ :param name: the ``int`` year (4-digit)
+ """
+ return "{:04d}".format(year)
+
+ def year_abbreviation(self, year):
+ """Returns the year for specific locale if available
+
+ :param name: the ``int`` year (4-digit)
+ """
+ return "{:04d}".format(year)[2:]
+
+ def meridian(self, hour, token):
+ """Returns the meridian indicator for a specified hour and format token.
+
+ :param hour: the ``int`` hour of the day.
+ :param token: the format token.
+ """
+
+ if token == "a":
+ return self.meridians["am"] if hour < 12 else self.meridians["pm"]
+ if token == "A":
+ return self.meridians["AM"] if hour < 12 else self.meridians["PM"]
+
+ def ordinal_number(self, n):
+ """Returns the ordinal format of a given integer
+
+ :param n: an integer
+ """
+ return self._ordinal_number(n)
+
+ def _ordinal_number(self, n):
+ return "{}".format(n)
+
+ def _name_to_ordinal(self, lst):
+ return dict(map(lambda i: (i[1].lower(), i[0] + 1), enumerate(lst[1:])))
+
+ def _format_timeframe(self, timeframe, delta):
+ return self.timeframes[timeframe].format(trunc(abs(delta)))
+
+ def _format_relative(self, humanized, timeframe, delta):
+
+ if timeframe == "now":
+ return humanized
+
+ direction = self.past if delta < 0 else self.future
+
+ return direction.format(humanized)
+
+
+# base locale type implementations.
+
+
+class EnglishLocale(Locale):
+
+ names = [
+ "en",
+ "en_us",
+ "en_gb",
+ "en_au",
+ "en_be",
+ "en_jp",
+ "en_za",
+ "en_ca",
+ "en_ph",
+ ]
+
+ past = "{0} ago"
+ future = "in {0}"
+ and_word = "and"
+
+ timeframes = {
+ "now": "just now",
+ "second": "a second",
+ "seconds": "{0} seconds",
+ "minute": "a minute",
+ "minutes": "{0} minutes",
+ "hour": "an hour",
+ "hours": "{0} hours",
+ "day": "a day",
+ "days": "{0} days",
+ "week": "a week",
+ "weeks": "{0} weeks",
+ "month": "a month",
+ "months": "{0} months",
+ "year": "a year",
+ "years": "{0} years",
+ }
+
+ meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"}
+
+ month_names = [
+ "",
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ]
+
+ day_names = [
+ "",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+ ]
+ day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+
+ ordinal_day_re = r"((?P[2-3]?1(?=st)|[2-3]?2(?=nd)|[2-3]?3(?=rd)|[1-3]?[04-9](?=th)|1[1-3](?=th))(st|nd|rd|th))"
+
+ def _ordinal_number(self, n):
+ if n % 100 not in (11, 12, 13):
+ remainder = abs(n) % 10
+ if remainder == 1:
+ return "{}st".format(n)
+ elif remainder == 2:
+ return "{}nd".format(n)
+ elif remainder == 3:
+ return "{}rd".format(n)
+ return "{}th".format(n)
+
+ def describe(self, timeframe, delta=0, only_distance=False):
+ """Describes a delta within a timeframe in plain language.
+
+ :param timeframe: a string representing a timeframe.
+ :param delta: a quantity representing a delta in a timeframe.
+ :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords
+ """
+
+ humanized = super(EnglishLocale, self).describe(timeframe, delta, only_distance)
+ if only_distance and timeframe == "now":
+ humanized = "instantly"
+
+ return humanized
+
+
+class ItalianLocale(Locale):
+ names = ["it", "it_it"]
+ past = "{0} fa"
+ future = "tra {0}"
+ and_word = "e"
+
+ timeframes = {
+ "now": "adesso",
+ "second": "un secondo",
+ "seconds": "{0} qualche secondo",
+ "minute": "un minuto",
+ "minutes": "{0} minuti",
+ "hour": "un'ora",
+ "hours": "{0} ore",
+ "day": "un giorno",
+ "days": "{0} giorni",
+ "week": "una settimana,",
+ "weeks": "{0} settimane",
+ "month": "un mese",
+ "months": "{0} mesi",
+ "year": "un anno",
+ "years": "{0} anni",
+ }
+
+ month_names = [
+ "",
+ "gennaio",
+ "febbraio",
+ "marzo",
+ "aprile",
+ "maggio",
+ "giugno",
+ "luglio",
+ "agosto",
+ "settembre",
+ "ottobre",
+ "novembre",
+ "dicembre",
+ ]
+ month_abbreviations = [
+ "",
+ "gen",
+ "feb",
+ "mar",
+ "apr",
+ "mag",
+ "giu",
+ "lug",
+ "ago",
+ "set",
+ "ott",
+ "nov",
+ "dic",
+ ]
+
+ day_names = [
+ "",
+ "lunedì",
+ "martedì",
+ "mercoledì",
+ "giovedì",
+ "venerdì",
+ "sabato",
+ "domenica",
+ ]
+ day_abbreviations = ["", "lun", "mar", "mer", "gio", "ven", "sab", "dom"]
+
+ ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])"
+
+ def _ordinal_number(self, n):
+ return "{}º".format(n)
+
+
+class SpanishLocale(Locale):
+ names = ["es", "es_es"]
+ past = "hace {0}"
+ future = "en {0}"
+ and_word = "y"
+
+ timeframes = {
+ "now": "ahora",
+ "second": "un segundo",
+ "seconds": "{0} segundos",
+ "minute": "un minuto",
+ "minutes": "{0} minutos",
+ "hour": "una hora",
+ "hours": "{0} horas",
+ "day": "un día",
+ "days": "{0} días",
+ "week": "una semana",
+ "weeks": "{0} semanas",
+ "month": "un mes",
+ "months": "{0} meses",
+ "year": "un año",
+ "years": "{0} años",
+ }
+
+ meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"}
+
+ month_names = [
+ "",
+ "enero",
+ "febrero",
+ "marzo",
+ "abril",
+ "mayo",
+ "junio",
+ "julio",
+ "agosto",
+ "septiembre",
+ "octubre",
+ "noviembre",
+ "diciembre",
+ ]
+ month_abbreviations = [
+ "",
+ "ene",
+ "feb",
+ "mar",
+ "abr",
+ "may",
+ "jun",
+ "jul",
+ "ago",
+ "sep",
+ "oct",
+ "nov",
+ "dic",
+ ]
+
+ day_names = [
+ "",
+ "lunes",
+ "martes",
+ "miércoles",
+ "jueves",
+ "viernes",
+ "sábado",
+ "domingo",
+ ]
+ day_abbreviations = ["", "lun", "mar", "mie", "jue", "vie", "sab", "dom"]
+
+ ordinal_day_re = r"((?P[1-3]?[0-9](?=[ºª]))[ºª])"
+
+ def _ordinal_number(self, n):
+ return "{}º".format(n)
+
+
+class FrenchBaseLocale(Locale):
+
+ past = "il y a {0}"
+ future = "dans {0}"
+ and_word = "et"
+
+ timeframes = {
+ "now": "maintenant",
+ "second": "une seconde",
+ "seconds": "{0} quelques secondes",
+ "minute": "une minute",
+ "minutes": "{0} minutes",
+ "hour": "une heure",
+ "hours": "{0} heures",
+ "day": "un jour",
+ "days": "{0} jours",
+ "week": "une semaine",
+ "weeks": "{0} semaines",
+ "month": "un mois",
+ "months": "{0} mois",
+ "year": "un an",
+ "years": "{0} ans",
+ }
+
+ month_names = [
+ "",
+ "janvier",
+ "février",
+ "mars",
+ "avril",
+ "mai",
+ "juin",
+ "juillet",
+ "août",
+ "septembre",
+ "octobre",
+ "novembre",
+ "décembre",
+ ]
+
+ day_names = [
+ "",
+ "lundi",
+ "mardi",
+ "mercredi",
+ "jeudi",
+ "vendredi",
+ "samedi",
+ "dimanche",
+ ]
+ day_abbreviations = ["", "lun", "mar", "mer", "jeu", "ven", "sam", "dim"]
+
+ ordinal_day_re = (
+ r"((?P\b1(?=er\b)|[1-3]?[02-9](?=e\b)|[1-3]1(?=e\b))(er|e)\b)"
+ )
+
+ def _ordinal_number(self, n):
+ if abs(n) == 1:
+ return "{}er".format(n)
+ return "{}e".format(n)
+
+
+class FrenchLocale(FrenchBaseLocale, Locale):
+
+ names = ["fr", "fr_fr"]
+
+ month_abbreviations = [
+ "",
+ "janv",
+ "févr",
+ "mars",
+ "avr",
+ "mai",
+ "juin",
+ "juil",
+ "août",
+ "sept",
+ "oct",
+ "nov",
+ "déc",
+ ]
+
+
+class FrenchCanadianLocale(FrenchBaseLocale, Locale):
+
+ names = ["fr_ca"]
+
+ month_abbreviations = [
+ "",
+ "janv",
+ "févr",
+ "mars",
+ "avr",
+ "mai",
+ "juin",
+ "juill",
+ "août",
+ "sept",
+ "oct",
+ "nov",
+ "déc",
+ ]
+
+
+class GreekLocale(Locale):
+
+ names = ["el", "el_gr"]
+
+ past = "{0} πριν"
+ future = "σε {0}"
+ and_word = "και"
+
+ timeframes = {
+ "now": "τώρα",
+ "second": "ένα δεύτερο",
+ "seconds": "{0} δευτερόλεπτα",
+ "minute": "ένα λεπτό",
+ "minutes": "{0} λεπτά",
+ "hour": "μία ώρα",
+ "hours": "{0} ώρες",
+ "day": "μία μέρα",
+ "days": "{0} μέρες",
+ "month": "ένα μήνα",
+ "months": "{0} μήνες",
+ "year": "ένα χρόνο",
+ "years": "{0} χρόνια",
+ }
+
+ month_names = [
+ "",
+ "Ιανουαρίου",
+ "Φεβρουαρίου",
+ "Μαρτίου",
+ "Απριλίου",
+ "Μαΐου",
+ "Ιουνίου",
+ "Ιουλίου",
+ "Αυγούστου",
+ "Σεπτεμβρίου",
+ "Οκτωβρίου",
+ "Νοεμβρίου",
+ "Δεκεμβρίου",
+ ]
+ month_abbreviations = [
+ "",
+ "Ιαν",
+ "Φεβ",
+ "Μαρ",
+ "Απρ",
+ "Μαϊ",
+ "Ιον",
+ "Ιολ",
+ "Αυγ",
+ "Σεπ",
+ "Οκτ",
+ "Νοε",
+ "Δεκ",
+ ]
+
+ day_names = [
+ "",
+ "Δευτέρα",
+ "Τρίτη",
+ "Τετάρτη",
+ "Πέμπτη",
+ "Παρασκευή",
+ "Σάββατο",
+ "Κυριακή",
+ ]
+ day_abbreviations = ["", "Δευ", "Τρι", "Τετ", "Πεμ", "Παρ", "Σαβ", "Κυρ"]
+
+
+class JapaneseLocale(Locale):
+
+ names = ["ja", "ja_jp"]
+
+ past = "{0}前"
+ future = "{0}後"
+
+ timeframes = {
+ "now": "現在",
+ "second": "二番目の",
+ "seconds": "{0}数秒",
+ "minute": "1分",
+ "minutes": "{0}分",
+ "hour": "1時間",
+ "hours": "{0}時間",
+ "day": "1日",
+ "days": "{0}日",
+ "week": "1週間",
+ "weeks": "{0}週間",
+ "month": "1ヶ月",
+ "months": "{0}ヶ月",
+ "year": "1年",
+ "years": "{0}年",
+ }
+
+ month_names = [
+ "",
+ "1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月",
+ ]
+ month_abbreviations = [
+ "",
+ " 1",
+ " 2",
+ " 3",
+ " 4",
+ " 5",
+ " 6",
+ " 7",
+ " 8",
+ " 9",
+ "10",
+ "11",
+ "12",
+ ]
+
+ day_names = ["", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日"]
+ day_abbreviations = ["", "月", "火", "水", "木", "金", "土", "日"]
+
+
+class SwedishLocale(Locale):
+
+ names = ["sv", "sv_se"]
+
+ past = "för {0} sen"
+ future = "om {0}"
+ and_word = "och"
+
+ timeframes = {
+ "now": "just nu",
+ "second": "en sekund",
+ "seconds": "{0} några sekunder",
+ "minute": "en minut",
+ "minutes": "{0} minuter",
+ "hour": "en timme",
+ "hours": "{0} timmar",
+ "day": "en dag",
+ "days": "{0} dagar",
+ "week": "en vecka",
+ "weeks": "{0} veckor",
+ "month": "en månad",
+ "months": "{0} månader",
+ "year": "ett år",
+ "years": "{0} år",
+ }
+
+ month_names = [
+ "",
+ "januari",
+ "februari",
+ "mars",
+ "april",
+ "maj",
+ "juni",
+ "juli",
+ "augusti",
+ "september",
+ "oktober",
+ "november",
+ "december",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "maj",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "måndag",
+ "tisdag",
+ "onsdag",
+ "torsdag",
+ "fredag",
+ "lördag",
+ "söndag",
+ ]
+ day_abbreviations = ["", "mån", "tis", "ons", "tor", "fre", "lör", "sön"]
+
+
+class FinnishLocale(Locale):
+
+ names = ["fi", "fi_fi"]
+
+ # The finnish grammar is very complex, and its hard to convert
+ # 1-to-1 to something like English.
+
+ past = "{0} sitten"
+ future = "{0} kuluttua"
+
+ timeframes = {
+ "now": ["juuri nyt", "juuri nyt"],
+ "second": ["sekunti", "sekunti"],
+ "seconds": ["{0} muutama sekunti", "{0} muutaman sekunnin"],
+ "minute": ["minuutti", "minuutin"],
+ "minutes": ["{0} minuuttia", "{0} minuutin"],
+ "hour": ["tunti", "tunnin"],
+ "hours": ["{0} tuntia", "{0} tunnin"],
+ "day": ["päivä", "päivä"],
+ "days": ["{0} päivää", "{0} päivän"],
+ "month": ["kuukausi", "kuukauden"],
+ "months": ["{0} kuukautta", "{0} kuukauden"],
+ "year": ["vuosi", "vuoden"],
+ "years": ["{0} vuotta", "{0} vuoden"],
+ }
+
+ # Months and days are lowercase in Finnish
+ month_names = [
+ "",
+ "tammikuu",
+ "helmikuu",
+ "maaliskuu",
+ "huhtikuu",
+ "toukokuu",
+ "kesäkuu",
+ "heinäkuu",
+ "elokuu",
+ "syyskuu",
+ "lokakuu",
+ "marraskuu",
+ "joulukuu",
+ ]
+
+ month_abbreviations = [
+ "",
+ "tammi",
+ "helmi",
+ "maalis",
+ "huhti",
+ "touko",
+ "kesä",
+ "heinä",
+ "elo",
+ "syys",
+ "loka",
+ "marras",
+ "joulu",
+ ]
+
+ day_names = [
+ "",
+ "maanantai",
+ "tiistai",
+ "keskiviikko",
+ "torstai",
+ "perjantai",
+ "lauantai",
+ "sunnuntai",
+ ]
+
+ day_abbreviations = ["", "ma", "ti", "ke", "to", "pe", "la", "su"]
+
+ def _format_timeframe(self, timeframe, delta):
+ return (
+ self.timeframes[timeframe][0].format(abs(delta)),
+ self.timeframes[timeframe][1].format(abs(delta)),
+ )
+
+ def _format_relative(self, humanized, timeframe, delta):
+ if timeframe == "now":
+ return humanized[0]
+
+ direction = self.past if delta < 0 else self.future
+ which = 0 if delta < 0 else 1
+
+ return direction.format(humanized[which])
+
+ def _ordinal_number(self, n):
+ return "{}.".format(n)
+
+
+class ChineseCNLocale(Locale):
+
+ names = ["zh", "zh_cn"]
+
+ past = "{0}前"
+ future = "{0}后"
+
+ timeframes = {
+ "now": "刚才",
+ "second": "一秒",
+ "seconds": "{0}秒",
+ "minute": "1分钟",
+ "minutes": "{0}分钟",
+ "hour": "1小时",
+ "hours": "{0}小时",
+ "day": "1天",
+ "days": "{0}天",
+ "week": "一周",
+ "weeks": "{0}周",
+ "month": "1个月",
+ "months": "{0}个月",
+ "year": "1年",
+ "years": "{0}年",
+ }
+
+ month_names = [
+ "",
+ "一月",
+ "二月",
+ "三月",
+ "四月",
+ "五月",
+ "六月",
+ "七月",
+ "八月",
+ "九月",
+ "十月",
+ "十一月",
+ "十二月",
+ ]
+ month_abbreviations = [
+ "",
+ " 1",
+ " 2",
+ " 3",
+ " 4",
+ " 5",
+ " 6",
+ " 7",
+ " 8",
+ " 9",
+ "10",
+ "11",
+ "12",
+ ]
+
+ day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
+ day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"]
+
+
+class ChineseTWLocale(Locale):
+
+ names = ["zh_tw"]
+
+ past = "{0}前"
+ future = "{0}後"
+ and_word = "和"
+
+ timeframes = {
+ "now": "剛才",
+ "second": "1秒",
+ "seconds": "{0}秒",
+ "minute": "1分鐘",
+ "minutes": "{0}分鐘",
+ "hour": "1小時",
+ "hours": "{0}小時",
+ "day": "1天",
+ "days": "{0}天",
+ "week": "1週",
+ "weeks": "{0}週",
+ "month": "1個月",
+ "months": "{0}個月",
+ "year": "1年",
+ "years": "{0}年",
+ }
+
+ month_names = [
+ "",
+ "1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月",
+ ]
+ month_abbreviations = [
+ "",
+ " 1",
+ " 2",
+ " 3",
+ " 4",
+ " 5",
+ " 6",
+ " 7",
+ " 8",
+ " 9",
+ "10",
+ "11",
+ "12",
+ ]
+
+ day_names = ["", "週一", "週二", "週三", "週四", "週五", "週六", "週日"]
+ day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"]
+
+
+class HongKongLocale(Locale):
+
+ names = ["zh_hk"]
+
+ past = "{0}前"
+ future = "{0}後"
+
+ timeframes = {
+ "now": "剛才",
+ "second": "1秒",
+ "seconds": "{0}秒",
+ "minute": "1分鐘",
+ "minutes": "{0}分鐘",
+ "hour": "1小時",
+ "hours": "{0}小時",
+ "day": "1天",
+ "days": "{0}天",
+ "week": "1星期",
+ "weeks": "{0}星期",
+ "month": "1個月",
+ "months": "{0}個月",
+ "year": "1年",
+ "years": "{0}年",
+ }
+
+ month_names = [
+ "",
+ "1月",
+ "2月",
+ "3月",
+ "4月",
+ "5月",
+ "6月",
+ "7月",
+ "8月",
+ "9月",
+ "10月",
+ "11月",
+ "12月",
+ ]
+ month_abbreviations = [
+ "",
+ " 1",
+ " 2",
+ " 3",
+ " 4",
+ " 5",
+ " 6",
+ " 7",
+ " 8",
+ " 9",
+ "10",
+ "11",
+ "12",
+ ]
+
+ day_names = ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
+ day_abbreviations = ["", "一", "二", "三", "四", "五", "六", "日"]
+
+
+class KoreanLocale(Locale):
+
+ names = ["ko", "ko_kr"]
+
+ past = "{0} 전"
+ future = "{0} 후"
+
+ timeframes = {
+ "now": "지금",
+ "second": "1초",
+ "seconds": "{0}초",
+ "minute": "1분",
+ "minutes": "{0}분",
+ "hour": "한시간",
+ "hours": "{0}시간",
+ "day": "하루",
+ "days": "{0}일",
+ "week": "1주",
+ "weeks": "{0}주",
+ "month": "한달",
+ "months": "{0}개월",
+ "year": "1년",
+ "years": "{0}년",
+ }
+
+ special_dayframes = {
+ -3: "그끄제",
+ -2: "그제",
+ -1: "어제",
+ 1: "내일",
+ 2: "모레",
+ 3: "글피",
+ 4: "그글피",
+ }
+
+ special_yearframes = {-2: "제작년", -1: "작년", 1: "내년", 2: "내후년"}
+
+ month_names = [
+ "",
+ "1월",
+ "2월",
+ "3월",
+ "4월",
+ "5월",
+ "6월",
+ "7월",
+ "8월",
+ "9월",
+ "10월",
+ "11월",
+ "12월",
+ ]
+ month_abbreviations = [
+ "",
+ " 1",
+ " 2",
+ " 3",
+ " 4",
+ " 5",
+ " 6",
+ " 7",
+ " 8",
+ " 9",
+ "10",
+ "11",
+ "12",
+ ]
+
+ day_names = ["", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
+ day_abbreviations = ["", "월", "화", "수", "목", "금", "토", "일"]
+
+ def _ordinal_number(self, n):
+ ordinals = ["0", "첫", "두", "세", "네", "다섯", "여섯", "일곱", "여덟", "아홉", "열"]
+ if n < len(ordinals):
+ return "{}번째".format(ordinals[n])
+ return "{}번째".format(n)
+
+ def _format_relative(self, humanized, timeframe, delta):
+ if timeframe in ("day", "days"):
+ special = self.special_dayframes.get(delta)
+ if special:
+ return special
+ elif timeframe in ("year", "years"):
+ special = self.special_yearframes.get(delta)
+ if special:
+ return special
+
+ return super(KoreanLocale, self)._format_relative(humanized, timeframe, delta)
+
+
+# derived locale types & implementations.
+class DutchLocale(Locale):
+
+ names = ["nl", "nl_nl"]
+
+ past = "{0} geleden"
+ future = "over {0}"
+
+ timeframes = {
+ "now": "nu",
+ "second": "een seconde",
+ "seconds": "{0} seconden",
+ "minute": "een minuut",
+ "minutes": "{0} minuten",
+ "hour": "een uur",
+ "hours": "{0} uur",
+ "day": "een dag",
+ "days": "{0} dagen",
+ "week": "een week",
+ "weeks": "{0} weken",
+ "month": "een maand",
+ "months": "{0} maanden",
+ "year": "een jaar",
+ "years": "{0} jaar",
+ }
+
+ # In Dutch names of months and days are not starting with a capital letter
+ # like in the English language.
+ month_names = [
+ "",
+ "januari",
+ "februari",
+ "maart",
+ "april",
+ "mei",
+ "juni",
+ "juli",
+ "augustus",
+ "september",
+ "oktober",
+ "november",
+ "december",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mrt",
+ "apr",
+ "mei",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "maandag",
+ "dinsdag",
+ "woensdag",
+ "donderdag",
+ "vrijdag",
+ "zaterdag",
+ "zondag",
+ ]
+ day_abbreviations = ["", "ma", "di", "wo", "do", "vr", "za", "zo"]
+
+
+class SlavicBaseLocale(Locale):
+ def _format_timeframe(self, timeframe, delta):
+
+ form = self.timeframes[timeframe]
+ delta = abs(delta)
+
+ if isinstance(form, list):
+
+ if delta % 10 == 1 and delta % 100 != 11:
+ form = form[0]
+ elif 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):
+ form = form[1]
+ else:
+ form = form[2]
+
+ return form.format(delta)
+
+
+class BelarusianLocale(SlavicBaseLocale):
+
+ names = ["be", "be_by"]
+
+ past = "{0} таму"
+ future = "праз {0}"
+
+ timeframes = {
+ "now": "зараз",
+ "second": "секунду",
+ "seconds": "{0} некалькі секунд",
+ "minute": "хвіліну",
+ "minutes": ["{0} хвіліну", "{0} хвіліны", "{0} хвілін"],
+ "hour": "гадзіну",
+ "hours": ["{0} гадзіну", "{0} гадзіны", "{0} гадзін"],
+ "day": "дзень",
+ "days": ["{0} дзень", "{0} дні", "{0} дзён"],
+ "month": "месяц",
+ "months": ["{0} месяц", "{0} месяцы", "{0} месяцаў"],
+ "year": "год",
+ "years": ["{0} год", "{0} гады", "{0} гадоў"],
+ }
+
+ month_names = [
+ "",
+ "студзеня",
+ "лютага",
+ "сакавіка",
+ "красавіка",
+ "траўня",
+ "чэрвеня",
+ "ліпеня",
+ "жніўня",
+ "верасня",
+ "кастрычніка",
+ "лістапада",
+ "снежня",
+ ]
+ month_abbreviations = [
+ "",
+ "студ",
+ "лют",
+ "сак",
+ "крас",
+ "трав",
+ "чэрв",
+ "ліп",
+ "жнів",
+ "вер",
+ "каст",
+ "ліст",
+ "снеж",
+ ]
+
+ day_names = [
+ "",
+ "панядзелак",
+ "аўторак",
+ "серада",
+ "чацвер",
+ "пятніца",
+ "субота",
+ "нядзеля",
+ ]
+ day_abbreviations = ["", "пн", "ат", "ср", "чц", "пт", "сб", "нд"]
+
+
+class PolishLocale(SlavicBaseLocale):
+
+ names = ["pl", "pl_pl"]
+
+ past = "{0} temu"
+ future = "za {0}"
+
+ # The nouns should be in genitive case (Polish: "dopełniacz")
+ # in order to correctly form `past` & `future` expressions.
+ timeframes = {
+ "now": "teraz",
+ "second": "sekundę",
+ "seconds": ["{0} sekund", "{0} sekundy", "{0} sekund"],
+ "minute": "minutę",
+ "minutes": ["{0} minut", "{0} minuty", "{0} minut"],
+ "hour": "godzinę",
+ "hours": ["{0} godzin", "{0} godziny", "{0} godzin"],
+ "day": "dzień",
+ "days": "{0} dni",
+ "week": "tydzień",
+ "weeks": ["{0} tygodni", "{0} tygodnie", "{0} tygodni"],
+ "month": "miesiąc",
+ "months": ["{0} miesięcy", "{0} miesiące", "{0} miesięcy"],
+ "year": "rok",
+ "years": ["{0} lat", "{0} lata", "{0} lat"],
+ }
+
+ month_names = [
+ "",
+ "styczeń",
+ "luty",
+ "marzec",
+ "kwiecień",
+ "maj",
+ "czerwiec",
+ "lipiec",
+ "sierpień",
+ "wrzesień",
+ "październik",
+ "listopad",
+ "grudzień",
+ ]
+ month_abbreviations = [
+ "",
+ "sty",
+ "lut",
+ "mar",
+ "kwi",
+ "maj",
+ "cze",
+ "lip",
+ "sie",
+ "wrz",
+ "paź",
+ "lis",
+ "gru",
+ ]
+
+ day_names = [
+ "",
+ "poniedziałek",
+ "wtorek",
+ "środa",
+ "czwartek",
+ "piątek",
+ "sobota",
+ "niedziela",
+ ]
+ day_abbreviations = ["", "Pn", "Wt", "Śr", "Czw", "Pt", "So", "Nd"]
+
+
+class RussianLocale(SlavicBaseLocale):
+
+ names = ["ru", "ru_ru"]
+
+ past = "{0} назад"
+ future = "через {0}"
+
+ timeframes = {
+ "now": "сейчас",
+ "second": "Второй",
+ "seconds": "{0} несколько секунд",
+ "minute": "минуту",
+ "minutes": ["{0} минуту", "{0} минуты", "{0} минут"],
+ "hour": "час",
+ "hours": ["{0} час", "{0} часа", "{0} часов"],
+ "day": "день",
+ "days": ["{0} день", "{0} дня", "{0} дней"],
+ "week": "неделю",
+ "weeks": ["{0} неделю", "{0} недели", "{0} недель"],
+ "month": "месяц",
+ "months": ["{0} месяц", "{0} месяца", "{0} месяцев"],
+ "year": "год",
+ "years": ["{0} год", "{0} года", "{0} лет"],
+ }
+
+ month_names = [
+ "",
+ "января",
+ "февраля",
+ "марта",
+ "апреля",
+ "мая",
+ "июня",
+ "июля",
+ "августа",
+ "сентября",
+ "октября",
+ "ноября",
+ "декабря",
+ ]
+ month_abbreviations = [
+ "",
+ "янв",
+ "фев",
+ "мар",
+ "апр",
+ "май",
+ "июн",
+ "июл",
+ "авг",
+ "сен",
+ "окт",
+ "ноя",
+ "дек",
+ ]
+
+ day_names = [
+ "",
+ "понедельник",
+ "вторник",
+ "среда",
+ "четверг",
+ "пятница",
+ "суббота",
+ "воскресенье",
+ ]
+ day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "вс"]
+
+
+class AfrikaansLocale(Locale):
+
+ names = ["af", "af_nl"]
+
+ past = "{0} gelede"
+ future = "in {0}"
+
+ timeframes = {
+ "now": "nou",
+ "second": "n sekonde",
+ "seconds": "{0} sekondes",
+ "minute": "minuut",
+ "minutes": "{0} minute",
+ "hour": "uur",
+ "hours": "{0} ure",
+ "day": "een dag",
+ "days": "{0} dae",
+ "month": "een maand",
+ "months": "{0} maande",
+ "year": "een jaar",
+ "years": "{0} jaar",
+ }
+
+ month_names = [
+ "",
+ "Januarie",
+ "Februarie",
+ "Maart",
+ "April",
+ "Mei",
+ "Junie",
+ "Julie",
+ "Augustus",
+ "September",
+ "Oktober",
+ "November",
+ "Desember",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mrt",
+ "Apr",
+ "Mei",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Des",
+ ]
+
+ day_names = [
+ "",
+ "Maandag",
+ "Dinsdag",
+ "Woensdag",
+ "Donderdag",
+ "Vrydag",
+ "Saterdag",
+ "Sondag",
+ ]
+ day_abbreviations = ["", "Ma", "Di", "Wo", "Do", "Vr", "Za", "So"]
+
+
+class BulgarianLocale(SlavicBaseLocale):
+
+ names = ["bg", "bg_BG"]
+
+ past = "{0} назад"
+ future = "напред {0}"
+
+ timeframes = {
+ "now": "сега",
+ "second": "секунда",
+ "seconds": "{0} няколко секунди",
+ "minute": "минута",
+ "minutes": ["{0} минута", "{0} минути", "{0} минути"],
+ "hour": "час",
+ "hours": ["{0} час", "{0} часа", "{0} часа"],
+ "day": "ден",
+ "days": ["{0} ден", "{0} дни", "{0} дни"],
+ "month": "месец",
+ "months": ["{0} месец", "{0} месеца", "{0} месеца"],
+ "year": "година",
+ "years": ["{0} година", "{0} години", "{0} години"],
+ }
+
+ month_names = [
+ "",
+ "януари",
+ "февруари",
+ "март",
+ "април",
+ "май",
+ "юни",
+ "юли",
+ "август",
+ "септември",
+ "октомври",
+ "ноември",
+ "декември",
+ ]
+ month_abbreviations = [
+ "",
+ "ян",
+ "февр",
+ "март",
+ "апр",
+ "май",
+ "юни",
+ "юли",
+ "авг",
+ "септ",
+ "окт",
+ "ноем",
+ "дек",
+ ]
+
+ day_names = [
+ "",
+ "понеделник",
+ "вторник",
+ "сряда",
+ "четвъртък",
+ "петък",
+ "събота",
+ "неделя",
+ ]
+ day_abbreviations = ["", "пон", "вт", "ср", "четв", "пет", "съб", "нед"]
+
+
+class UkrainianLocale(SlavicBaseLocale):
+
+ names = ["ua", "uk_ua"]
+
+ past = "{0} тому"
+ future = "за {0}"
+
+ timeframes = {
+ "now": "зараз",
+ "second": "секунда",
+ "seconds": "{0} кілька секунд",
+ "minute": "хвилину",
+ "minutes": ["{0} хвилину", "{0} хвилини", "{0} хвилин"],
+ "hour": "годину",
+ "hours": ["{0} годину", "{0} години", "{0} годин"],
+ "day": "день",
+ "days": ["{0} день", "{0} дні", "{0} днів"],
+ "month": "місяць",
+ "months": ["{0} місяць", "{0} місяці", "{0} місяців"],
+ "year": "рік",
+ "years": ["{0} рік", "{0} роки", "{0} років"],
+ }
+
+ month_names = [
+ "",
+ "січня",
+ "лютого",
+ "березня",
+ "квітня",
+ "травня",
+ "червня",
+ "липня",
+ "серпня",
+ "вересня",
+ "жовтня",
+ "листопада",
+ "грудня",
+ ]
+ month_abbreviations = [
+ "",
+ "січ",
+ "лют",
+ "бер",
+ "квіт",
+ "трав",
+ "черв",
+ "лип",
+ "серп",
+ "вер",
+ "жовт",
+ "лист",
+ "груд",
+ ]
+
+ day_names = [
+ "",
+ "понеділок",
+ "вівторок",
+ "середа",
+ "четвер",
+ "п’ятниця",
+ "субота",
+ "неділя",
+ ]
+ day_abbreviations = ["", "пн", "вт", "ср", "чт", "пт", "сб", "нд"]
+
+
+class MacedonianLocale(SlavicBaseLocale):
+ names = ["mk", "mk_mk"]
+
+ past = "пред {0}"
+ future = "за {0}"
+
+ timeframes = {
+ "now": "сега",
+ "second": "една секунда",
+ "seconds": ["{0} секунда", "{0} секунди", "{0} секунди"],
+ "minute": "една минута",
+ "minutes": ["{0} минута", "{0} минути", "{0} минути"],
+ "hour": "еден саат",
+ "hours": ["{0} саат", "{0} саати", "{0} саати"],
+ "day": "еден ден",
+ "days": ["{0} ден", "{0} дена", "{0} дена"],
+ "week": "една недела",
+ "weeks": ["{0} недела", "{0} недели", "{0} недели"],
+ "month": "еден месец",
+ "months": ["{0} месец", "{0} месеци", "{0} месеци"],
+ "year": "една година",
+ "years": ["{0} година", "{0} години", "{0} години"],
+ }
+
+ meridians = {"am": "дп", "pm": "пп", "AM": "претпладне", "PM": "попладне"}
+
+ month_names = [
+ "",
+ "Јануари",
+ "Февруари",
+ "Март",
+ "Април",
+ "Мај",
+ "Јуни",
+ "Јули",
+ "Август",
+ "Септември",
+ "Октомври",
+ "Ноември",
+ "Декември",
+ ]
+ month_abbreviations = [
+ "",
+ "Јан",
+ "Фев",
+ "Мар",
+ "Апр",
+ "Мај",
+ "Јун",
+ "Јул",
+ "Авг",
+ "Септ",
+ "Окт",
+ "Ноем",
+ "Декем",
+ ]
+
+ day_names = [
+ "",
+ "Понеделник",
+ "Вторник",
+ "Среда",
+ "Четврток",
+ "Петок",
+ "Сабота",
+ "Недела",
+ ]
+ day_abbreviations = [
+ "",
+ "Пон",
+ "Вт",
+ "Сре",
+ "Чет",
+ "Пет",
+ "Саб",
+ "Нед",
+ ]
+
+
+class GermanBaseLocale(Locale):
+
+ past = "vor {0}"
+ future = "in {0}"
+ and_word = "und"
+
+ timeframes = {
+ "now": "gerade eben",
+ "second": "eine Sekunde",
+ "seconds": "{0} Sekunden",
+ "minute": "einer Minute",
+ "minutes": "{0} Minuten",
+ "hour": "einer Stunde",
+ "hours": "{0} Stunden",
+ "day": "einem Tag",
+ "days": "{0} Tagen",
+ "week": "einer Woche",
+ "weeks": "{0} Wochen",
+ "month": "einem Monat",
+ "months": "{0} Monaten",
+ "year": "einem Jahr",
+ "years": "{0} Jahren",
+ }
+
+ timeframes_only_distance = timeframes.copy()
+ timeframes_only_distance["minute"] = "eine Minute"
+ timeframes_only_distance["hour"] = "eine Stunde"
+ timeframes_only_distance["day"] = "ein Tag"
+ timeframes_only_distance["week"] = "eine Woche"
+ timeframes_only_distance["month"] = "ein Monat"
+ timeframes_only_distance["year"] = "ein Jahr"
+
+ month_names = [
+ "",
+ "Januar",
+ "Februar",
+ "März",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember",
+ ]
+
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mär",
+ "Apr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dez",
+ ]
+
+ day_names = [
+ "",
+ "Montag",
+ "Dienstag",
+ "Mittwoch",
+ "Donnerstag",
+ "Freitag",
+ "Samstag",
+ "Sonntag",
+ ]
+
+ day_abbreviations = ["", "Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
+
+ def _ordinal_number(self, n):
+ return "{}.".format(n)
+
+ def describe(self, timeframe, delta=0, only_distance=False):
+ """Describes a delta within a timeframe in plain language.
+
+ :param timeframe: a string representing a timeframe.
+ :param delta: a quantity representing a delta in a timeframe.
+ :param only_distance: return only distance eg: "11 seconds" without "in" or "ago" keywords
+ """
+
+ if not only_distance:
+ return super(GermanBaseLocale, self).describe(
+ timeframe, delta, only_distance
+ )
+
+ # German uses a different case without 'in' or 'ago'
+ humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))
+
+ return humanized
+
+
+class GermanLocale(GermanBaseLocale, Locale):
+
+ names = ["de", "de_de"]
+
+
+class SwissLocale(GermanBaseLocale, Locale):
+
+ names = ["de_ch"]
+
+
+class AustrianLocale(GermanBaseLocale, Locale):
+
+ names = ["de_at"]
+
+ month_names = [
+ "",
+ "Jänner",
+ "Februar",
+ "März",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember",
+ ]
+
+
+class NorwegianLocale(Locale):
+
+ names = ["nb", "nb_no"]
+
+ past = "for {0} siden"
+ future = "om {0}"
+
+ timeframes = {
+ "now": "nå nettopp",
+ "second": "et sekund",
+ "seconds": "{0} noen sekunder",
+ "minute": "ett minutt",
+ "minutes": "{0} minutter",
+ "hour": "en time",
+ "hours": "{0} timer",
+ "day": "en dag",
+ "days": "{0} dager",
+ "month": "en måned",
+ "months": "{0} måneder",
+ "year": "ett år",
+ "years": "{0} år",
+ }
+
+ month_names = [
+ "",
+ "januar",
+ "februar",
+ "mars",
+ "april",
+ "mai",
+ "juni",
+ "juli",
+ "august",
+ "september",
+ "oktober",
+ "november",
+ "desember",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "mai",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "des",
+ ]
+
+ day_names = [
+ "",
+ "mandag",
+ "tirsdag",
+ "onsdag",
+ "torsdag",
+ "fredag",
+ "lørdag",
+ "søndag",
+ ]
+ day_abbreviations = ["", "ma", "ti", "on", "to", "fr", "lø", "sø"]
+
+
+class NewNorwegianLocale(Locale):
+
+ names = ["nn", "nn_no"]
+
+ past = "for {0} sidan"
+ future = "om {0}"
+
+ timeframes = {
+ "now": "no nettopp",
+ "second": "et sekund",
+ "seconds": "{0} nokre sekund",
+ "minute": "ett minutt",
+ "minutes": "{0} minutt",
+ "hour": "ein time",
+ "hours": "{0} timar",
+ "day": "ein dag",
+ "days": "{0} dagar",
+ "month": "en månad",
+ "months": "{0} månader",
+ "year": "eit år",
+ "years": "{0} år",
+ }
+
+ month_names = [
+ "",
+ "januar",
+ "februar",
+ "mars",
+ "april",
+ "mai",
+ "juni",
+ "juli",
+ "august",
+ "september",
+ "oktober",
+ "november",
+ "desember",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "mai",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "des",
+ ]
+
+ day_names = [
+ "",
+ "måndag",
+ "tysdag",
+ "onsdag",
+ "torsdag",
+ "fredag",
+ "laurdag",
+ "sundag",
+ ]
+ day_abbreviations = ["", "må", "ty", "on", "to", "fr", "la", "su"]
+
+
+class PortugueseLocale(Locale):
+ names = ["pt", "pt_pt"]
+
+ past = "há {0}"
+ future = "em {0}"
+ and_word = "e"
+
+ timeframes = {
+ "now": "agora",
+ "second": "um segundo",
+ "seconds": "{0} segundos",
+ "minute": "um minuto",
+ "minutes": "{0} minutos",
+ "hour": "uma hora",
+ "hours": "{0} horas",
+ "day": "um dia",
+ "days": "{0} dias",
+ "week": "uma semana",
+ "weeks": "{0} semanas",
+ "month": "um mês",
+ "months": "{0} meses",
+ "year": "um ano",
+ "years": "{0} anos",
+ }
+
+ month_names = [
+ "",
+ "Janeiro",
+ "Fevereiro",
+ "Março",
+ "Abril",
+ "Maio",
+ "Junho",
+ "Julho",
+ "Agosto",
+ "Setembro",
+ "Outubro",
+ "Novembro",
+ "Dezembro",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Fev",
+ "Mar",
+ "Abr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Set",
+ "Out",
+ "Nov",
+ "Dez",
+ ]
+
+ day_names = [
+ "",
+ "Segunda-feira",
+ "Terça-feira",
+ "Quarta-feira",
+ "Quinta-feira",
+ "Sexta-feira",
+ "Sábado",
+ "Domingo",
+ ]
+ day_abbreviations = ["", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab", "Dom"]
+
+
+class BrazilianPortugueseLocale(PortugueseLocale):
+ names = ["pt_br"]
+
+ past = "faz {0}"
+
+
+class TagalogLocale(Locale):
+
+ names = ["tl", "tl_ph"]
+
+ past = "nakaraang {0}"
+ future = "{0} mula ngayon"
+
+ timeframes = {
+ "now": "ngayon lang",
+ "second": "isang segundo",
+ "seconds": "{0} segundo",
+ "minute": "isang minuto",
+ "minutes": "{0} minuto",
+ "hour": "isang oras",
+ "hours": "{0} oras",
+ "day": "isang araw",
+ "days": "{0} araw",
+ "week": "isang linggo",
+ "weeks": "{0} linggo",
+ "month": "isang buwan",
+ "months": "{0} buwan",
+ "year": "isang taon",
+ "years": "{0} taon",
+ }
+
+ month_names = [
+ "",
+ "Enero",
+ "Pebrero",
+ "Marso",
+ "Abril",
+ "Mayo",
+ "Hunyo",
+ "Hulyo",
+ "Agosto",
+ "Setyembre",
+ "Oktubre",
+ "Nobyembre",
+ "Disyembre",
+ ]
+ month_abbreviations = [
+ "",
+ "Ene",
+ "Peb",
+ "Mar",
+ "Abr",
+ "May",
+ "Hun",
+ "Hul",
+ "Ago",
+ "Set",
+ "Okt",
+ "Nob",
+ "Dis",
+ ]
+
+ day_names = [
+ "",
+ "Lunes",
+ "Martes",
+ "Miyerkules",
+ "Huwebes",
+ "Biyernes",
+ "Sabado",
+ "Linggo",
+ ]
+ day_abbreviations = ["", "Lun", "Mar", "Miy", "Huw", "Biy", "Sab", "Lin"]
+
+ meridians = {"am": "nu", "pm": "nh", "AM": "ng umaga", "PM": "ng hapon"}
+
+ def _ordinal_number(self, n):
+ return "ika-{}".format(n)
+
+
+class VietnameseLocale(Locale):
+
+ names = ["vi", "vi_vn"]
+
+ past = "{0} trước"
+ future = "{0} nữa"
+
+ timeframes = {
+ "now": "hiện tại",
+ "second": "một giây",
+ "seconds": "{0} giây",
+ "minute": "một phút",
+ "minutes": "{0} phút",
+ "hour": "một giờ",
+ "hours": "{0} giờ",
+ "day": "một ngày",
+ "days": "{0} ngày",
+ "week": "một tuần",
+ "weeks": "{0} tuần",
+ "month": "một tháng",
+ "months": "{0} tháng",
+ "year": "một năm",
+ "years": "{0} năm",
+ }
+
+ month_names = [
+ "",
+ "Tháng Một",
+ "Tháng Hai",
+ "Tháng Ba",
+ "Tháng Tư",
+ "Tháng Năm",
+ "Tháng Sáu",
+ "Tháng Bảy",
+ "Tháng Tám",
+ "Tháng Chín",
+ "Tháng Mười",
+ "Tháng Mười Một",
+ "Tháng Mười Hai",
+ ]
+ month_abbreviations = [
+ "",
+ "Tháng 1",
+ "Tháng 2",
+ "Tháng 3",
+ "Tháng 4",
+ "Tháng 5",
+ "Tháng 6",
+ "Tháng 7",
+ "Tháng 8",
+ "Tháng 9",
+ "Tháng 10",
+ "Tháng 11",
+ "Tháng 12",
+ ]
+
+ day_names = [
+ "",
+ "Thứ Hai",
+ "Thứ Ba",
+ "Thứ Tư",
+ "Thứ Năm",
+ "Thứ Sáu",
+ "Thứ Bảy",
+ "Chủ Nhật",
+ ]
+ day_abbreviations = ["", "Thứ 2", "Thứ 3", "Thứ 4", "Thứ 5", "Thứ 6", "Thứ 7", "CN"]
+
+
+class TurkishLocale(Locale):
+
+ names = ["tr", "tr_tr"]
+
+ past = "{0} önce"
+ future = "{0} sonra"
+
+ timeframes = {
+ "now": "şimdi",
+ "second": "bir saniye",
+ "seconds": "{0} saniye",
+ "minute": "bir dakika",
+ "minutes": "{0} dakika",
+ "hour": "bir saat",
+ "hours": "{0} saat",
+ "day": "bir gün",
+ "days": "{0} gün",
+ "month": "bir ay",
+ "months": "{0} ay",
+ "year": "yıl",
+ "years": "{0} yıl",
+ }
+
+ month_names = [
+ "",
+ "Ocak",
+ "Şubat",
+ "Mart",
+ "Nisan",
+ "Mayıs",
+ "Haziran",
+ "Temmuz",
+ "Ağustos",
+ "Eylül",
+ "Ekim",
+ "Kasım",
+ "Aralık",
+ ]
+ month_abbreviations = [
+ "",
+ "Oca",
+ "Şub",
+ "Mar",
+ "Nis",
+ "May",
+ "Haz",
+ "Tem",
+ "Ağu",
+ "Eyl",
+ "Eki",
+ "Kas",
+ "Ara",
+ ]
+
+ day_names = [
+ "",
+ "Pazartesi",
+ "Salı",
+ "Çarşamba",
+ "Perşembe",
+ "Cuma",
+ "Cumartesi",
+ "Pazar",
+ ]
+ day_abbreviations = ["", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt", "Paz"]
+
+
+class AzerbaijaniLocale(Locale):
+
+ names = ["az", "az_az"]
+
+ past = "{0} əvvəl"
+ future = "{0} sonra"
+
+ timeframes = {
+ "now": "indi",
+ "second": "saniyə",
+ "seconds": "{0} saniyə",
+ "minute": "bir dəqiqə",
+ "minutes": "{0} dəqiqə",
+ "hour": "bir saat",
+ "hours": "{0} saat",
+ "day": "bir gün",
+ "days": "{0} gün",
+ "month": "bir ay",
+ "months": "{0} ay",
+ "year": "il",
+ "years": "{0} il",
+ }
+
+ month_names = [
+ "",
+ "Yanvar",
+ "Fevral",
+ "Mart",
+ "Aprel",
+ "May",
+ "İyun",
+ "İyul",
+ "Avqust",
+ "Sentyabr",
+ "Oktyabr",
+ "Noyabr",
+ "Dekabr",
+ ]
+ month_abbreviations = [
+ "",
+ "Yan",
+ "Fev",
+ "Mar",
+ "Apr",
+ "May",
+ "İyn",
+ "İyl",
+ "Avq",
+ "Sen",
+ "Okt",
+ "Noy",
+ "Dek",
+ ]
+
+ day_names = [
+ "",
+ "Bazar ertəsi",
+ "Çərşənbə axşamı",
+ "Çərşənbə",
+ "Cümə axşamı",
+ "Cümə",
+ "Şənbə",
+ "Bazar",
+ ]
+ day_abbreviations = ["", "Ber", "Çax", "Çər", "Cax", "Cüm", "Şnb", "Bzr"]
+
+
+class ArabicLocale(Locale):
+ names = [
+ "ar",
+ "ar_ae",
+ "ar_bh",
+ "ar_dj",
+ "ar_eg",
+ "ar_eh",
+ "ar_er",
+ "ar_km",
+ "ar_kw",
+ "ar_ly",
+ "ar_om",
+ "ar_qa",
+ "ar_sa",
+ "ar_sd",
+ "ar_so",
+ "ar_ss",
+ "ar_td",
+ "ar_ye",
+ ]
+
+ past = "منذ {0}"
+ future = "خلال {0}"
+
+ timeframes = {
+ "now": "الآن",
+ "second": "ثانية",
+ "seconds": {"double": "ثانيتين", "ten": "{0} ثوان", "higher": "{0} ثانية"},
+ "minute": "دقيقة",
+ "minutes": {"double": "دقيقتين", "ten": "{0} دقائق", "higher": "{0} دقيقة"},
+ "hour": "ساعة",
+ "hours": {"double": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"},
+ "day": "يوم",
+ "days": {"double": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"},
+ "month": "شهر",
+ "months": {"double": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"},
+ "year": "سنة",
+ "years": {"double": "سنتين", "ten": "{0} سنوات", "higher": "{0} سنة"},
+ }
+
+ month_names = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "أبريل",
+ "مايو",
+ "يونيو",
+ "يوليو",
+ "أغسطس",
+ "سبتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "ديسمبر",
+ ]
+ month_abbreviations = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "أبريل",
+ "مايو",
+ "يونيو",
+ "يوليو",
+ "أغسطس",
+ "سبتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "ديسمبر",
+ ]
+
+ day_names = [
+ "",
+ "الإثنين",
+ "الثلاثاء",
+ "الأربعاء",
+ "الخميس",
+ "الجمعة",
+ "السبت",
+ "الأحد",
+ ]
+ day_abbreviations = ["", "إثنين", "ثلاثاء", "أربعاء", "خميس", "جمعة", "سبت", "أحد"]
+
+ def _format_timeframe(self, timeframe, delta):
+ form = self.timeframes[timeframe]
+ delta = abs(delta)
+ if isinstance(form, dict):
+ if delta == 2:
+ form = form["double"]
+ elif delta > 2 and delta <= 10:
+ form = form["ten"]
+ else:
+ form = form["higher"]
+
+ return form.format(delta)
+
+
+class LevantArabicLocale(ArabicLocale):
+ names = ["ar_iq", "ar_jo", "ar_lb", "ar_ps", "ar_sy"]
+ month_names = [
+ "",
+ "كانون الثاني",
+ "شباط",
+ "آذار",
+ "نيسان",
+ "أيار",
+ "حزيران",
+ "تموز",
+ "آب",
+ "أيلول",
+ "تشرين الأول",
+ "تشرين الثاني",
+ "كانون الأول",
+ ]
+ month_abbreviations = [
+ "",
+ "كانون الثاني",
+ "شباط",
+ "آذار",
+ "نيسان",
+ "أيار",
+ "حزيران",
+ "تموز",
+ "آب",
+ "أيلول",
+ "تشرين الأول",
+ "تشرين الثاني",
+ "كانون الأول",
+ ]
+
+
+class AlgeriaTunisiaArabicLocale(ArabicLocale):
+ names = ["ar_tn", "ar_dz"]
+ month_names = [
+ "",
+ "جانفي",
+ "فيفري",
+ "مارس",
+ "أفريل",
+ "ماي",
+ "جوان",
+ "جويلية",
+ "أوت",
+ "سبتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "ديسمبر",
+ ]
+ month_abbreviations = [
+ "",
+ "جانفي",
+ "فيفري",
+ "مارس",
+ "أفريل",
+ "ماي",
+ "جوان",
+ "جويلية",
+ "أوت",
+ "سبتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "ديسمبر",
+ ]
+
+
+class MauritaniaArabicLocale(ArabicLocale):
+ names = ["ar_mr"]
+ month_names = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "إبريل",
+ "مايو",
+ "يونيو",
+ "يوليو",
+ "أغشت",
+ "شتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "دجمبر",
+ ]
+ month_abbreviations = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "إبريل",
+ "مايو",
+ "يونيو",
+ "يوليو",
+ "أغشت",
+ "شتمبر",
+ "أكتوبر",
+ "نوفمبر",
+ "دجمبر",
+ ]
+
+
+class MoroccoArabicLocale(ArabicLocale):
+ names = ["ar_ma"]
+ month_names = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "أبريل",
+ "ماي",
+ "يونيو",
+ "يوليوز",
+ "غشت",
+ "شتنبر",
+ "أكتوبر",
+ "نونبر",
+ "دجنبر",
+ ]
+ month_abbreviations = [
+ "",
+ "يناير",
+ "فبراير",
+ "مارس",
+ "أبريل",
+ "ماي",
+ "يونيو",
+ "يوليوز",
+ "غشت",
+ "شتنبر",
+ "أكتوبر",
+ "نونبر",
+ "دجنبر",
+ ]
+
+
+class IcelandicLocale(Locale):
+ def _format_timeframe(self, timeframe, delta):
+
+ timeframe = self.timeframes[timeframe]
+ if delta < 0:
+ timeframe = timeframe[0]
+ elif delta > 0:
+ timeframe = timeframe[1]
+
+ return timeframe.format(abs(delta))
+
+ names = ["is", "is_is"]
+
+ past = "fyrir {0} síðan"
+ future = "eftir {0}"
+
+ timeframes = {
+ "now": "rétt í þessu",
+ "second": ("sekúndu", "sekúndu"),
+ "seconds": ("{0} nokkrum sekúndum", "nokkrar sekúndur"),
+ "minute": ("einni mínútu", "eina mínútu"),
+ "minutes": ("{0} mínútum", "{0} mínútur"),
+ "hour": ("einum tíma", "einn tíma"),
+ "hours": ("{0} tímum", "{0} tíma"),
+ "day": ("einum degi", "einn dag"),
+ "days": ("{0} dögum", "{0} daga"),
+ "month": ("einum mánuði", "einn mánuð"),
+ "months": ("{0} mánuðum", "{0} mánuði"),
+ "year": ("einu ári", "eitt ár"),
+ "years": ("{0} árum", "{0} ár"),
+ }
+
+ meridians = {"am": "f.h.", "pm": "e.h.", "AM": "f.h.", "PM": "e.h."}
+
+ month_names = [
+ "",
+ "janúar",
+ "febrúar",
+ "mars",
+ "apríl",
+ "maí",
+ "júní",
+ "júlí",
+ "ágúst",
+ "september",
+ "október",
+ "nóvember",
+ "desember",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "maí",
+ "jún",
+ "júl",
+ "ágú",
+ "sep",
+ "okt",
+ "nóv",
+ "des",
+ ]
+
+ day_names = [
+ "",
+ "mánudagur",
+ "þriðjudagur",
+ "miðvikudagur",
+ "fimmtudagur",
+ "föstudagur",
+ "laugardagur",
+ "sunnudagur",
+ ]
+ day_abbreviations = ["", "mán", "þri", "mið", "fim", "fös", "lau", "sun"]
+
+
+class DanishLocale(Locale):
+
+ names = ["da", "da_dk"]
+
+ past = "for {0} siden"
+ future = "efter {0}"
+ and_word = "og"
+
+ timeframes = {
+ "now": "lige nu",
+ "second": "et sekund",
+ "seconds": "{0} et par sekunder",
+ "minute": "et minut",
+ "minutes": "{0} minutter",
+ "hour": "en time",
+ "hours": "{0} timer",
+ "day": "en dag",
+ "days": "{0} dage",
+ "month": "en måned",
+ "months": "{0} måneder",
+ "year": "et år",
+ "years": "{0} år",
+ }
+
+ month_names = [
+ "",
+ "januar",
+ "februar",
+ "marts",
+ "april",
+ "maj",
+ "juni",
+ "juli",
+ "august",
+ "september",
+ "oktober",
+ "november",
+ "december",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "maj",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "mandag",
+ "tirsdag",
+ "onsdag",
+ "torsdag",
+ "fredag",
+ "lørdag",
+ "søndag",
+ ]
+ day_abbreviations = ["", "man", "tir", "ons", "tor", "fre", "lør", "søn"]
+
+
+class MalayalamLocale(Locale):
+
+ names = ["ml"]
+
+ past = "{0} മുമ്പ്"
+ future = "{0} ശേഷം"
+
+ timeframes = {
+ "now": "ഇപ്പോൾ",
+ "second": "ഒരു നിമിഷം",
+ "seconds": "{0} സെക്കന്റ്",
+ "minute": "ഒരു മിനിറ്റ്",
+ "minutes": "{0} മിനിറ്റ്",
+ "hour": "ഒരു മണിക്കൂർ",
+ "hours": "{0} മണിക്കൂർ",
+ "day": "ഒരു ദിവസം ",
+ "days": "{0} ദിവസം ",
+ "month": "ഒരു മാസം ",
+ "months": "{0} മാസം ",
+ "year": "ഒരു വർഷം ",
+ "years": "{0} വർഷം ",
+ }
+
+ meridians = {
+ "am": "രാവിലെ",
+ "pm": "ഉച്ചക്ക് ശേഷം",
+ "AM": "രാവിലെ",
+ "PM": "ഉച്ചക്ക് ശേഷം",
+ }
+
+ month_names = [
+ "",
+ "ജനുവരി",
+ "ഫെബ്രുവരി",
+ "മാർച്ച്",
+ "ഏപ്രിൽ ",
+ "മെയ് ",
+ "ജൂണ്",
+ "ജൂലൈ",
+ "ഓഗസ്റ്റ്",
+ "സെപ്റ്റംബർ",
+ "ഒക്ടോബർ",
+ "നവംബർ",
+ "ഡിസംബർ",
+ ]
+ month_abbreviations = [
+ "",
+ "ജനു",
+ "ഫെബ് ",
+ "മാർ",
+ "ഏപ്രിൽ",
+ "മേയ്",
+ "ജൂണ്",
+ "ജൂലൈ",
+ "ഓഗസ്റ",
+ "സെപ്റ്റ",
+ "ഒക്ടോ",
+ "നവം",
+ "ഡിസം",
+ ]
+
+ day_names = ["", "തിങ്കള്", "ചൊവ്വ", "ബുധന്", "വ്യാഴം", "വെള്ളി", "ശനി", "ഞായര്"]
+ day_abbreviations = [
+ "",
+ "തിങ്കള്",
+ "ചൊവ്വ",
+ "ബുധന്",
+ "വ്യാഴം",
+ "വെള്ളി",
+ "ശനി",
+ "ഞായര്",
+ ]
+
+
+class HindiLocale(Locale):
+
+ names = ["hi"]
+
+ past = "{0} पहले"
+ future = "{0} बाद"
+
+ timeframes = {
+ "now": "अभी",
+ "second": "एक पल",
+ "seconds": "{0} सेकंड्",
+ "minute": "एक मिनट ",
+ "minutes": "{0} मिनट ",
+ "hour": "एक घंटा",
+ "hours": "{0} घंटे",
+ "day": "एक दिन",
+ "days": "{0} दिन",
+ "month": "एक माह ",
+ "months": "{0} महीने ",
+ "year": "एक वर्ष ",
+ "years": "{0} साल ",
+ }
+
+ meridians = {"am": "सुबह", "pm": "शाम", "AM": "सुबह", "PM": "शाम"}
+
+ month_names = [
+ "",
+ "जनवरी",
+ "फरवरी",
+ "मार्च",
+ "अप्रैल ",
+ "मई",
+ "जून",
+ "जुलाई",
+ "अगस्त",
+ "सितंबर",
+ "अक्टूबर",
+ "नवंबर",
+ "दिसंबर",
+ ]
+ month_abbreviations = [
+ "",
+ "जन",
+ "फ़र",
+ "मार्च",
+ "अप्रै",
+ "मई",
+ "जून",
+ "जुलाई",
+ "आग",
+ "सित",
+ "अकत",
+ "नवे",
+ "दिस",
+ ]
+
+ day_names = [
+ "",
+ "सोमवार",
+ "मंगलवार",
+ "बुधवार",
+ "गुरुवार",
+ "शुक्रवार",
+ "शनिवार",
+ "रविवार",
+ ]
+ day_abbreviations = ["", "सोम", "मंगल", "बुध", "गुरुवार", "शुक्र", "शनि", "रवि"]
+
+
+class CzechLocale(Locale):
+ names = ["cs", "cs_cz"]
+
+ timeframes = {
+ "now": "Teď",
+ "second": {"past": "vteřina", "future": "vteřina", "zero": "vteřina"},
+ "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekund"]},
+ "minute": {"past": "minutou", "future": "minutu", "zero": "{0} minut"},
+ "minutes": {"past": "{0} minutami", "future": ["{0} minuty", "{0} minut"]},
+ "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodin"},
+ "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodin"]},
+ "day": {"past": "dnem", "future": "den", "zero": "{0} dnů"},
+ "days": {"past": "{0} dny", "future": ["{0} dny", "{0} dnů"]},
+ "week": {"past": "týdnem", "future": "týden", "zero": "{0} týdnů"},
+ "weeks": {"past": "{0} týdny", "future": ["{0} týdny", "{0} týdnů"]},
+ "month": {"past": "měsícem", "future": "měsíc", "zero": "{0} měsíců"},
+ "months": {"past": "{0} měsíci", "future": ["{0} měsíce", "{0} měsíců"]},
+ "year": {"past": "rokem", "future": "rok", "zero": "{0} let"},
+ "years": {"past": "{0} lety", "future": ["{0} roky", "{0} let"]},
+ }
+
+ past = "Před {0}"
+ future = "Za {0}"
+
+ month_names = [
+ "",
+ "leden",
+ "únor",
+ "březen",
+ "duben",
+ "květen",
+ "červen",
+ "červenec",
+ "srpen",
+ "září",
+ "říjen",
+ "listopad",
+ "prosinec",
+ ]
+ month_abbreviations = [
+ "",
+ "led",
+ "úno",
+ "bře",
+ "dub",
+ "kvě",
+ "čvn",
+ "čvc",
+ "srp",
+ "zář",
+ "říj",
+ "lis",
+ "pro",
+ ]
+
+ day_names = [
+ "",
+ "pondělí",
+ "úterý",
+ "středa",
+ "čtvrtek",
+ "pátek",
+ "sobota",
+ "neděle",
+ ]
+ day_abbreviations = ["", "po", "út", "st", "čt", "pá", "so", "ne"]
+
+ def _format_timeframe(self, timeframe, delta):
+ """Czech aware time frame format function, takes into account
+ the differences between past and future forms."""
+ form = self.timeframes[timeframe]
+ if isinstance(form, dict):
+ if delta == 0:
+ form = form["zero"] # And *never* use 0 in the singular!
+ elif delta > 0:
+ form = form["future"]
+ else:
+ form = form["past"]
+ delta = abs(delta)
+
+ if isinstance(form, list):
+ if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):
+ form = form[0]
+ else:
+ form = form[1]
+
+ return form.format(delta)
+
+
+class SlovakLocale(Locale):
+ names = ["sk", "sk_sk"]
+
+ timeframes = {
+ "now": "Teraz",
+ "second": {"past": "sekundou", "future": "sekundu", "zero": "{0} sekúnd"},
+ "seconds": {"past": "{0} sekundami", "future": ["{0} sekundy", "{0} sekúnd"]},
+ "minute": {"past": "minútou", "future": "minútu", "zero": "{0} minút"},
+ "minutes": {"past": "{0} minútami", "future": ["{0} minúty", "{0} minút"]},
+ "hour": {"past": "hodinou", "future": "hodinu", "zero": "{0} hodín"},
+ "hours": {"past": "{0} hodinami", "future": ["{0} hodiny", "{0} hodín"]},
+ "day": {"past": "dňom", "future": "deň", "zero": "{0} dní"},
+ "days": {"past": "{0} dňami", "future": ["{0} dni", "{0} dní"]},
+ "week": {"past": "týždňom", "future": "týždeň", "zero": "{0} týždňov"},
+ "weeks": {"past": "{0} týždňami", "future": ["{0} týždne", "{0} týždňov"]},
+ "month": {"past": "mesiacom", "future": "mesiac", "zero": "{0} mesiacov"},
+ "months": {"past": "{0} mesiacmi", "future": ["{0} mesiace", "{0} mesiacov"]},
+ "year": {"past": "rokom", "future": "rok", "zero": "{0} rokov"},
+ "years": {"past": "{0} rokmi", "future": ["{0} roky", "{0} rokov"]},
+ }
+
+ past = "Pred {0}"
+ future = "O {0}"
+ and_word = "a"
+
+ month_names = [
+ "",
+ "január",
+ "február",
+ "marec",
+ "apríl",
+ "máj",
+ "jún",
+ "júl",
+ "august",
+ "september",
+ "október",
+ "november",
+ "december",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "máj",
+ "jún",
+ "júl",
+ "aug",
+ "sep",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "pondelok",
+ "utorok",
+ "streda",
+ "štvrtok",
+ "piatok",
+ "sobota",
+ "nedeľa",
+ ]
+ day_abbreviations = ["", "po", "ut", "st", "št", "pi", "so", "ne"]
+
+ def _format_timeframe(self, timeframe, delta):
+ """Slovak aware time frame format function, takes into account
+ the differences between past and future forms."""
+ form = self.timeframes[timeframe]
+ if isinstance(form, dict):
+ if delta == 0:
+ form = form["zero"] # And *never* use 0 in the singular!
+ elif delta > 0:
+ form = form["future"]
+ else:
+ form = form["past"]
+ delta = abs(delta)
+
+ if isinstance(form, list):
+ if 2 <= delta % 10 <= 4 and (delta % 100 < 10 or delta % 100 >= 20):
+ form = form[0]
+ else:
+ form = form[1]
+
+ return form.format(delta)
+
+
+class FarsiLocale(Locale):
+
+ names = ["fa", "fa_ir"]
+
+ past = "{0} قبل"
+ future = "در {0}"
+
+ timeframes = {
+ "now": "اکنون",
+ "second": "یک لحظه",
+ "seconds": "{0} ثانیه",
+ "minute": "یک دقیقه",
+ "minutes": "{0} دقیقه",
+ "hour": "یک ساعت",
+ "hours": "{0} ساعت",
+ "day": "یک روز",
+ "days": "{0} روز",
+ "month": "یک ماه",
+ "months": "{0} ماه",
+ "year": "یک سال",
+ "years": "{0} سال",
+ }
+
+ meridians = {
+ "am": "قبل از ظهر",
+ "pm": "بعد از ظهر",
+ "AM": "قبل از ظهر",
+ "PM": "بعد از ظهر",
+ }
+
+ month_names = [
+ "",
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ]
+
+ day_names = [
+ "",
+ "دو شنبه",
+ "سه شنبه",
+ "چهارشنبه",
+ "پنجشنبه",
+ "جمعه",
+ "شنبه",
+ "یکشنبه",
+ ]
+ day_abbreviations = ["", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
+
+
+class HebrewLocale(Locale):
+
+ names = ["he", "he_IL"]
+
+ past = "לפני {0}"
+ future = "בעוד {0}"
+ and_word = "ו"
+
+ timeframes = {
+ "now": "הרגע",
+ "second": "שנייה",
+ "seconds": "{0} שניות",
+ "minute": "דקה",
+ "minutes": "{0} דקות",
+ "hour": "שעה",
+ "hours": "{0} שעות",
+ "2-hours": "שעתיים",
+ "day": "יום",
+ "days": "{0} ימים",
+ "2-days": "יומיים",
+ "week": "שבוע",
+ "weeks": "{0} שבועות",
+ "2-weeks": "שבועיים",
+ "month": "חודש",
+ "months": "{0} חודשים",
+ "2-months": "חודשיים",
+ "year": "שנה",
+ "years": "{0} שנים",
+ "2-years": "שנתיים",
+ }
+
+ meridians = {
+ "am": 'לפנ"צ',
+ "pm": 'אחר"צ',
+ "AM": "לפני הצהריים",
+ "PM": "אחרי הצהריים",
+ }
+
+ month_names = [
+ "",
+ "ינואר",
+ "פברואר",
+ "מרץ",
+ "אפריל",
+ "מאי",
+ "יוני",
+ "יולי",
+ "אוגוסט",
+ "ספטמבר",
+ "אוקטובר",
+ "נובמבר",
+ "דצמבר",
+ ]
+ month_abbreviations = [
+ "",
+ "ינו׳",
+ "פבר׳",
+ "מרץ",
+ "אפר׳",
+ "מאי",
+ "יוני",
+ "יולי",
+ "אוג׳",
+ "ספט׳",
+ "אוק׳",
+ "נוב׳",
+ "דצמ׳",
+ ]
+
+ day_names = ["", "שני", "שלישי", "רביעי", "חמישי", "שישי", "שבת", "ראשון"]
+ day_abbreviations = ["", "ב׳", "ג׳", "ד׳", "ה׳", "ו׳", "ש׳", "א׳"]
+
+ def _format_timeframe(self, timeframe, delta):
+ """Hebrew couple of aware"""
+ couple = "2-{}".format(timeframe)
+ single = timeframe.rstrip("s")
+ if abs(delta) == 2 and couple in self.timeframes:
+ key = couple
+ elif abs(delta) == 1 and single in self.timeframes:
+ key = single
+ else:
+ key = timeframe
+
+ return self.timeframes[key].format(trunc(abs(delta)))
+
+ def describe_multi(self, timeframes, only_distance=False):
+ """Describes a delta within multiple timeframes in plain language.
+ In Hebrew, the and word behaves a bit differently.
+
+ :param timeframes: a list of string, quantity pairs each representing a timeframe and delta.
+ :param only_distance: return only distance eg: "2 hours and 11 seconds" without "in" or "ago" keywords
+ """
+
+ humanized = ""
+ for index, (timeframe, delta) in enumerate(timeframes):
+ last_humanized = self._format_timeframe(timeframe, delta)
+ if index == 0:
+ humanized = last_humanized
+ elif index == len(timeframes) - 1: # Must have at least 2 items
+ humanized += " " + self.and_word
+ if last_humanized[0].isdecimal():
+ humanized += "־"
+ humanized += last_humanized
+ else: # Don't add for the last one
+ humanized += ", " + last_humanized
+
+ if not only_distance:
+ humanized = self._format_relative(humanized, timeframe, delta)
+
+ return humanized
+
+
+class MarathiLocale(Locale):
+
+ names = ["mr"]
+
+ past = "{0} आधी"
+ future = "{0} नंतर"
+
+ timeframes = {
+ "now": "सद्य",
+ "second": "एक सेकंद",
+ "seconds": "{0} सेकंद",
+ "minute": "एक मिनिट ",
+ "minutes": "{0} मिनिट ",
+ "hour": "एक तास",
+ "hours": "{0} तास",
+ "day": "एक दिवस",
+ "days": "{0} दिवस",
+ "month": "एक महिना ",
+ "months": "{0} महिने ",
+ "year": "एक वर्ष ",
+ "years": "{0} वर्ष ",
+ }
+
+ meridians = {"am": "सकाळ", "pm": "संध्याकाळ", "AM": "सकाळ", "PM": "संध्याकाळ"}
+
+ month_names = [
+ "",
+ "जानेवारी",
+ "फेब्रुवारी",
+ "मार्च",
+ "एप्रिल",
+ "मे",
+ "जून",
+ "जुलै",
+ "अॉगस्ट",
+ "सप्टेंबर",
+ "अॉक्टोबर",
+ "नोव्हेंबर",
+ "डिसेंबर",
+ ]
+ month_abbreviations = [
+ "",
+ "जान",
+ "फेब्रु",
+ "मार्च",
+ "एप्रि",
+ "मे",
+ "जून",
+ "जुलै",
+ "अॉग",
+ "सप्टें",
+ "अॉक्टो",
+ "नोव्हें",
+ "डिसें",
+ ]
+
+ day_names = [
+ "",
+ "सोमवार",
+ "मंगळवार",
+ "बुधवार",
+ "गुरुवार",
+ "शुक्रवार",
+ "शनिवार",
+ "रविवार",
+ ]
+ day_abbreviations = ["", "सोम", "मंगळ", "बुध", "गुरु", "शुक्र", "शनि", "रवि"]
+
+
+def _map_locales():
+
+ locales = {}
+
+ for _, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass):
+ if issubclass(cls, Locale): # pragma: no branch
+ for name in cls.names:
+ locales[name.lower()] = cls
+
+ return locales
+
+
+class CatalanLocale(Locale):
+ names = ["ca", "ca_es", "ca_ad", "ca_fr", "ca_it"]
+ past = "Fa {0}"
+ future = "En {0}"
+ and_word = "i"
+
+ timeframes = {
+ "now": "Ara mateix",
+ "second": "un segon",
+ "seconds": "{0} segons",
+ "minute": "1 minut",
+ "minutes": "{0} minuts",
+ "hour": "una hora",
+ "hours": "{0} hores",
+ "day": "un dia",
+ "days": "{0} dies",
+ "month": "un mes",
+ "months": "{0} mesos",
+ "year": "un any",
+ "years": "{0} anys",
+ }
+
+ month_names = [
+ "",
+ "gener",
+ "febrer",
+ "març",
+ "abril",
+ "maig",
+ "juny",
+ "juliol",
+ "agost",
+ "setembre",
+ "octubre",
+ "novembre",
+ "desembre",
+ ]
+ month_abbreviations = [
+ "",
+ "gen.",
+ "febr.",
+ "març",
+ "abr.",
+ "maig",
+ "juny",
+ "jul.",
+ "ag.",
+ "set.",
+ "oct.",
+ "nov.",
+ "des.",
+ ]
+ day_names = [
+ "",
+ "dilluns",
+ "dimarts",
+ "dimecres",
+ "dijous",
+ "divendres",
+ "dissabte",
+ "diumenge",
+ ]
+ day_abbreviations = [
+ "",
+ "dl.",
+ "dt.",
+ "dc.",
+ "dj.",
+ "dv.",
+ "ds.",
+ "dg.",
+ ]
+
+
+class BasqueLocale(Locale):
+ names = ["eu", "eu_eu"]
+ past = "duela {0}"
+ future = "{0}" # I don't know what's the right phrase in Basque for the future.
+
+ timeframes = {
+ "now": "Orain",
+ "second": "segundo bat",
+ "seconds": "{0} segundu",
+ "minute": "minutu bat",
+ "minutes": "{0} minutu",
+ "hour": "ordu bat",
+ "hours": "{0} ordu",
+ "day": "egun bat",
+ "days": "{0} egun",
+ "month": "hilabete bat",
+ "months": "{0} hilabet",
+ "year": "urte bat",
+ "years": "{0} urte",
+ }
+
+ month_names = [
+ "",
+ "urtarrilak",
+ "otsailak",
+ "martxoak",
+ "apirilak",
+ "maiatzak",
+ "ekainak",
+ "uztailak",
+ "abuztuak",
+ "irailak",
+ "urriak",
+ "azaroak",
+ "abenduak",
+ ]
+ month_abbreviations = [
+ "",
+ "urt",
+ "ots",
+ "mar",
+ "api",
+ "mai",
+ "eka",
+ "uzt",
+ "abu",
+ "ira",
+ "urr",
+ "aza",
+ "abe",
+ ]
+ day_names = [
+ "",
+ "astelehena",
+ "asteartea",
+ "asteazkena",
+ "osteguna",
+ "ostirala",
+ "larunbata",
+ "igandea",
+ ]
+ day_abbreviations = ["", "al", "ar", "az", "og", "ol", "lr", "ig"]
+
+
+class HungarianLocale(Locale):
+
+ names = ["hu", "hu_hu"]
+
+ past = "{0} ezelőtt"
+ future = "{0} múlva"
+
+ timeframes = {
+ "now": "éppen most",
+ "second": {"past": "egy második", "future": "egy második"},
+ "seconds": {"past": "{0} másodpercekkel", "future": "{0} pár másodperc"},
+ "minute": {"past": "egy perccel", "future": "egy perc"},
+ "minutes": {"past": "{0} perccel", "future": "{0} perc"},
+ "hour": {"past": "egy órával", "future": "egy óra"},
+ "hours": {"past": "{0} órával", "future": "{0} óra"},
+ "day": {"past": "egy nappal", "future": "egy nap"},
+ "days": {"past": "{0} nappal", "future": "{0} nap"},
+ "month": {"past": "egy hónappal", "future": "egy hónap"},
+ "months": {"past": "{0} hónappal", "future": "{0} hónap"},
+ "year": {"past": "egy évvel", "future": "egy év"},
+ "years": {"past": "{0} évvel", "future": "{0} év"},
+ }
+
+ month_names = [
+ "",
+ "január",
+ "február",
+ "március",
+ "április",
+ "május",
+ "június",
+ "július",
+ "augusztus",
+ "szeptember",
+ "október",
+ "november",
+ "december",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "febr",
+ "márc",
+ "ápr",
+ "máj",
+ "jún",
+ "júl",
+ "aug",
+ "szept",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "hétfő",
+ "kedd",
+ "szerda",
+ "csütörtök",
+ "péntek",
+ "szombat",
+ "vasárnap",
+ ]
+ day_abbreviations = ["", "hét", "kedd", "szer", "csüt", "pént", "szom", "vas"]
+
+ meridians = {"am": "de", "pm": "du", "AM": "DE", "PM": "DU"}
+
+ def _format_timeframe(self, timeframe, delta):
+ form = self.timeframes[timeframe]
+
+ if isinstance(form, dict):
+ if delta > 0:
+ form = form["future"]
+ else:
+ form = form["past"]
+
+ return form.format(abs(delta))
+
+
+class EsperantoLocale(Locale):
+ names = ["eo", "eo_xx"]
+ past = "antaŭ {0}"
+ future = "post {0}"
+
+ timeframes = {
+ "now": "nun",
+ "second": "sekundo",
+ "seconds": "{0} kelkaj sekundoj",
+ "minute": "unu minuto",
+ "minutes": "{0} minutoj",
+ "hour": "un horo",
+ "hours": "{0} horoj",
+ "day": "unu tago",
+ "days": "{0} tagoj",
+ "month": "unu monato",
+ "months": "{0} monatoj",
+ "year": "unu jaro",
+ "years": "{0} jaroj",
+ }
+
+ month_names = [
+ "",
+ "januaro",
+ "februaro",
+ "marto",
+ "aprilo",
+ "majo",
+ "junio",
+ "julio",
+ "aŭgusto",
+ "septembro",
+ "oktobro",
+ "novembro",
+ "decembro",
+ ]
+ month_abbreviations = [
+ "",
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "maj",
+ "jun",
+ "jul",
+ "aŭg",
+ "sep",
+ "okt",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "lundo",
+ "mardo",
+ "merkredo",
+ "ĵaŭdo",
+ "vendredo",
+ "sabato",
+ "dimanĉo",
+ ]
+ day_abbreviations = ["", "lun", "mar", "mer", "ĵaŭ", "ven", "sab", "dim"]
+
+ meridians = {"am": "atm", "pm": "ptm", "AM": "ATM", "PM": "PTM"}
+
+ ordinal_day_re = r"((?P[1-3]?[0-9](?=a))a)"
+
+ def _ordinal_number(self, n):
+ return "{}a".format(n)
+
+
+class ThaiLocale(Locale):
+
+ names = ["th", "th_th"]
+
+ past = "{0}{1}ที่ผ่านมา"
+ future = "ในอีก{1}{0}"
+
+ timeframes = {
+ "now": "ขณะนี้",
+ "second": "วินาที",
+ "seconds": "{0} ไม่กี่วินาที",
+ "minute": "1 นาที",
+ "minutes": "{0} นาที",
+ "hour": "1 ชั่วโมง",
+ "hours": "{0} ชั่วโมง",
+ "day": "1 วัน",
+ "days": "{0} วัน",
+ "month": "1 เดือน",
+ "months": "{0} เดือน",
+ "year": "1 ปี",
+ "years": "{0} ปี",
+ }
+
+ month_names = [
+ "",
+ "มกราคม",
+ "กุมภาพันธ์",
+ "มีนาคม",
+ "เมษายน",
+ "พฤษภาคม",
+ "มิถุนายน",
+ "กรกฎาคม",
+ "สิงหาคม",
+ "กันยายน",
+ "ตุลาคม",
+ "พฤศจิกายน",
+ "ธันวาคม",
+ ]
+ month_abbreviations = [
+ "",
+ "ม.ค.",
+ "ก.พ.",
+ "มี.ค.",
+ "เม.ย.",
+ "พ.ค.",
+ "มิ.ย.",
+ "ก.ค.",
+ "ส.ค.",
+ "ก.ย.",
+ "ต.ค.",
+ "พ.ย.",
+ "ธ.ค.",
+ ]
+
+ day_names = ["", "จันทร์", "อังคาร", "พุธ", "พฤหัสบดี", "ศุกร์", "เสาร์", "อาทิตย์"]
+ day_abbreviations = ["", "จ", "อ", "พ", "พฤ", "ศ", "ส", "อา"]
+
+ meridians = {"am": "am", "pm": "pm", "AM": "AM", "PM": "PM"}
+
+ BE_OFFSET = 543
+
+ def year_full(self, year):
+ """Thai always use Buddhist Era (BE) which is CE + 543"""
+ year += self.BE_OFFSET
+ return "{:04d}".format(year)
+
+ def year_abbreviation(self, year):
+ """Thai always use Buddhist Era (BE) which is CE + 543"""
+ year += self.BE_OFFSET
+ return "{:04d}".format(year)[2:]
+
+ def _format_relative(self, humanized, timeframe, delta):
+ """Thai normally doesn't have any space between words"""
+ if timeframe == "now":
+ return humanized
+ space = "" if timeframe == "seconds" else " "
+ direction = self.past if delta < 0 else self.future
+
+ return direction.format(humanized, space)
+
+
+class BengaliLocale(Locale):
+
+ names = ["bn", "bn_bd", "bn_in"]
+
+ past = "{0} আগে"
+ future = "{0} পরে"
+
+ timeframes = {
+ "now": "এখন",
+ "second": "একটি দ্বিতীয়",
+ "seconds": "{0} সেকেন্ড",
+ "minute": "এক মিনিট",
+ "minutes": "{0} মিনিট",
+ "hour": "এক ঘণ্টা",
+ "hours": "{0} ঘণ্টা",
+ "day": "এক দিন",
+ "days": "{0} দিন",
+ "month": "এক মাস",
+ "months": "{0} মাস ",
+ "year": "এক বছর",
+ "years": "{0} বছর",
+ }
+
+ meridians = {"am": "সকাল", "pm": "বিকাল", "AM": "সকাল", "PM": "বিকাল"}
+
+ month_names = [
+ "",
+ "জানুয়ারি",
+ "ফেব্রুয়ারি",
+ "মার্চ",
+ "এপ্রিল",
+ "মে",
+ "জুন",
+ "জুলাই",
+ "আগস্ট",
+ "সেপ্টেম্বর",
+ "অক্টোবর",
+ "নভেম্বর",
+ "ডিসেম্বর",
+ ]
+ month_abbreviations = [
+ "",
+ "জানু",
+ "ফেব",
+ "মার্চ",
+ "এপ্রি",
+ "মে",
+ "জুন",
+ "জুল",
+ "অগা",
+ "সেপ্ট",
+ "অক্টো",
+ "নভে",
+ "ডিসে",
+ ]
+
+ day_names = [
+ "",
+ "সোমবার",
+ "মঙ্গলবার",
+ "বুধবার",
+ "বৃহস্পতিবার",
+ "শুক্রবার",
+ "শনিবার",
+ "রবিবার",
+ ]
+ day_abbreviations = ["", "সোম", "মঙ্গল", "বুধ", "বৃহঃ", "শুক্র", "শনি", "রবি"]
+
+ def _ordinal_number(self, n):
+ if n > 10 or n == 0:
+ return "{}তম".format(n)
+ if n in [1, 5, 7, 8, 9, 10]:
+ return "{}ম".format(n)
+ if n in [2, 3]:
+ return "{}য়".format(n)
+ if n == 4:
+ return "{}র্থ".format(n)
+ if n == 6:
+ return "{}ষ্ঠ".format(n)
+
+
+class RomanshLocale(Locale):
+
+ names = ["rm", "rm_ch"]
+
+ past = "avant {0}"
+ future = "en {0}"
+
+ timeframes = {
+ "now": "en quest mument",
+ "second": "in secunda",
+ "seconds": "{0} secundas",
+ "minute": "ina minuta",
+ "minutes": "{0} minutas",
+ "hour": "in'ura",
+ "hours": "{0} ura",
+ "day": "in di",
+ "days": "{0} dis",
+ "month": "in mais",
+ "months": "{0} mais",
+ "year": "in onn",
+ "years": "{0} onns",
+ }
+
+ month_names = [
+ "",
+ "schaner",
+ "favrer",
+ "mars",
+ "avrigl",
+ "matg",
+ "zercladur",
+ "fanadur",
+ "avust",
+ "settember",
+ "october",
+ "november",
+ "december",
+ ]
+
+ month_abbreviations = [
+ "",
+ "schan",
+ "fav",
+ "mars",
+ "avr",
+ "matg",
+ "zer",
+ "fan",
+ "avu",
+ "set",
+ "oct",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "glindesdi",
+ "mardi",
+ "mesemna",
+ "gievgia",
+ "venderdi",
+ "sonda",
+ "dumengia",
+ ]
+
+ day_abbreviations = ["", "gli", "ma", "me", "gie", "ve", "so", "du"]
+
+
+class RomanianLocale(Locale):
+ names = ["ro", "ro_ro"]
+
+ past = "{0} în urmă"
+ future = "peste {0}"
+ and_word = "și"
+
+ timeframes = {
+ "now": "acum",
+ "second": "o secunda",
+ "seconds": "{0} câteva secunde",
+ "minute": "un minut",
+ "minutes": "{0} minute",
+ "hour": "o oră",
+ "hours": "{0} ore",
+ "day": "o zi",
+ "days": "{0} zile",
+ "month": "o lună",
+ "months": "{0} luni",
+ "year": "un an",
+ "years": "{0} ani",
+ }
+
+ month_names = [
+ "",
+ "ianuarie",
+ "februarie",
+ "martie",
+ "aprilie",
+ "mai",
+ "iunie",
+ "iulie",
+ "august",
+ "septembrie",
+ "octombrie",
+ "noiembrie",
+ "decembrie",
+ ]
+ month_abbreviations = [
+ "",
+ "ian",
+ "febr",
+ "mart",
+ "apr",
+ "mai",
+ "iun",
+ "iul",
+ "aug",
+ "sept",
+ "oct",
+ "nov",
+ "dec",
+ ]
+
+ day_names = [
+ "",
+ "luni",
+ "marți",
+ "miercuri",
+ "joi",
+ "vineri",
+ "sâmbătă",
+ "duminică",
+ ]
+ day_abbreviations = ["", "Lun", "Mar", "Mie", "Joi", "Vin", "Sâm", "Dum"]
+
+
+class SlovenianLocale(Locale):
+ names = ["sl", "sl_si"]
+
+ past = "pred {0}"
+ future = "čez {0}"
+ and_word = "in"
+
+ timeframes = {
+ "now": "zdaj",
+ "second": "sekundo",
+ "seconds": "{0} sekund",
+ "minute": "minuta",
+ "minutes": "{0} minutami",
+ "hour": "uro",
+ "hours": "{0} ur",
+ "day": "dan",
+ "days": "{0} dni",
+ "month": "mesec",
+ "months": "{0} mesecev",
+ "year": "leto",
+ "years": "{0} let",
+ }
+
+ meridians = {"am": "", "pm": "", "AM": "", "PM": ""}
+
+ month_names = [
+ "",
+ "Januar",
+ "Februar",
+ "Marec",
+ "April",
+ "Maj",
+ "Junij",
+ "Julij",
+ "Avgust",
+ "September",
+ "Oktober",
+ "November",
+ "December",
+ ]
+
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Maj",
+ "Jun",
+ "Jul",
+ "Avg",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dec",
+ ]
+
+ day_names = [
+ "",
+ "Ponedeljek",
+ "Torek",
+ "Sreda",
+ "Četrtek",
+ "Petek",
+ "Sobota",
+ "Nedelja",
+ ]
+
+ day_abbreviations = ["", "Pon", "Tor", "Sre", "Čet", "Pet", "Sob", "Ned"]
+
+
+class IndonesianLocale(Locale):
+
+ names = ["id", "id_id"]
+
+ past = "{0} yang lalu"
+ future = "dalam {0}"
+ and_word = "dan"
+
+ timeframes = {
+ "now": "baru saja",
+ "second": "1 sebentar",
+ "seconds": "{0} detik",
+ "minute": "1 menit",
+ "minutes": "{0} menit",
+ "hour": "1 jam",
+ "hours": "{0} jam",
+ "day": "1 hari",
+ "days": "{0} hari",
+ "month": "1 bulan",
+ "months": "{0} bulan",
+ "year": "1 tahun",
+ "years": "{0} tahun",
+ }
+
+ meridians = {"am": "", "pm": "", "AM": "", "PM": ""}
+
+ month_names = [
+ "",
+ "Januari",
+ "Februari",
+ "Maret",
+ "April",
+ "Mei",
+ "Juni",
+ "Juli",
+ "Agustus",
+ "September",
+ "Oktober",
+ "November",
+ "Desember",
+ ]
+
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "Mei",
+ "Jun",
+ "Jul",
+ "Ags",
+ "Sept",
+ "Okt",
+ "Nov",
+ "Des",
+ ]
+
+ day_names = ["", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Minggu"]
+
+ day_abbreviations = [
+ "",
+ "Senin",
+ "Selasa",
+ "Rabu",
+ "Kamis",
+ "Jumat",
+ "Sabtu",
+ "Minggu",
+ ]
+
+
+class NepaliLocale(Locale):
+ names = ["ne", "ne_np"]
+
+ past = "{0} पहिले"
+ future = "{0} पछी"
+
+ timeframes = {
+ "now": "अहिले",
+ "second": "एक सेकेन्ड",
+ "seconds": "{0} सेकण्ड",
+ "minute": "मिनेट",
+ "minutes": "{0} मिनेट",
+ "hour": "एक घण्टा",
+ "hours": "{0} घण्टा",
+ "day": "एक दिन",
+ "days": "{0} दिन",
+ "month": "एक महिना",
+ "months": "{0} महिना",
+ "year": "एक बर्ष",
+ "years": "बर्ष",
+ }
+
+ meridians = {"am": "पूर्वाह्न", "pm": "अपरान्ह", "AM": "पूर्वाह्न", "PM": "अपरान्ह"}
+
+ month_names = [
+ "",
+ "जनवरी",
+ "फेब्रुअरी",
+ "मार्च",
+ "एप्रील",
+ "मे",
+ "जुन",
+ "जुलाई",
+ "अगष्ट",
+ "सेप्टेम्बर",
+ "अक्टोबर",
+ "नोवेम्बर",
+ "डिसेम्बर",
+ ]
+ month_abbreviations = [
+ "",
+ "जन",
+ "फेब",
+ "मार्च",
+ "एप्रील",
+ "मे",
+ "जुन",
+ "जुलाई",
+ "अग",
+ "सेप",
+ "अक्ट",
+ "नोव",
+ "डिस",
+ ]
+
+ day_names = [
+ "",
+ "सोमवार",
+ "मंगलवार",
+ "बुधवार",
+ "बिहिवार",
+ "शुक्रवार",
+ "शनिवार",
+ "आइतवार",
+ ]
+
+ day_abbreviations = ["", "सोम", "मंगल", "बुध", "बिहि", "शुक्र", "शनि", "आइत"]
+
+
+class EstonianLocale(Locale):
+ names = ["ee", "et"]
+
+ past = "{0} tagasi"
+ future = "{0} pärast"
+ and_word = "ja"
+
+ timeframes = {
+ "now": {"past": "just nüüd", "future": "just nüüd"},
+ "second": {"past": "üks sekund", "future": "ühe sekundi"},
+ "seconds": {"past": "{0} sekundit", "future": "{0} sekundi"},
+ "minute": {"past": "üks minut", "future": "ühe minuti"},
+ "minutes": {"past": "{0} minutit", "future": "{0} minuti"},
+ "hour": {"past": "tund aega", "future": "tunni aja"},
+ "hours": {"past": "{0} tundi", "future": "{0} tunni"},
+ "day": {"past": "üks päev", "future": "ühe päeva"},
+ "days": {"past": "{0} päeva", "future": "{0} päeva"},
+ "month": {"past": "üks kuu", "future": "ühe kuu"},
+ "months": {"past": "{0} kuud", "future": "{0} kuu"},
+ "year": {"past": "üks aasta", "future": "ühe aasta"},
+ "years": {"past": "{0} aastat", "future": "{0} aasta"},
+ }
+
+ month_names = [
+ "",
+ "Jaanuar",
+ "Veebruar",
+ "Märts",
+ "Aprill",
+ "Mai",
+ "Juuni",
+ "Juuli",
+ "August",
+ "September",
+ "Oktoober",
+ "November",
+ "Detsember",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Veb",
+ "Mär",
+ "Apr",
+ "Mai",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Dets",
+ ]
+
+ day_names = [
+ "",
+ "Esmaspäev",
+ "Teisipäev",
+ "Kolmapäev",
+ "Neljapäev",
+ "Reede",
+ "Laupäev",
+ "Pühapäev",
+ ]
+ day_abbreviations = ["", "Esm", "Teis", "Kolm", "Nelj", "Re", "Lau", "Püh"]
+
+ def _format_timeframe(self, timeframe, delta):
+ form = self.timeframes[timeframe]
+ if delta > 0:
+ form = form["future"]
+ else:
+ form = form["past"]
+ return form.format(abs(delta))
+
+
+class SwahiliLocale(Locale):
+
+ names = [
+ "sw",
+ "sw_ke",
+ "sw_tz",
+ ]
+
+ past = "{0} iliyopita"
+ future = "muda wa {0}"
+ and_word = "na"
+
+ timeframes = {
+ "now": "sasa hivi",
+ "second": "sekunde",
+ "seconds": "sekunde {0}",
+ "minute": "dakika moja",
+ "minutes": "dakika {0}",
+ "hour": "saa moja",
+ "hours": "saa {0}",
+ "day": "siku moja",
+ "days": "siku {0}",
+ "week": "wiki moja",
+ "weeks": "wiki {0}",
+ "month": "mwezi moja",
+ "months": "miezi {0}",
+ "year": "mwaka moja",
+ "years": "miaka {0}",
+ }
+
+ meridians = {"am": "asu", "pm": "mch", "AM": "ASU", "PM": "MCH"}
+
+ month_names = [
+ "",
+ "Januari",
+ "Februari",
+ "Machi",
+ "Aprili",
+ "Mei",
+ "Juni",
+ "Julai",
+ "Agosti",
+ "Septemba",
+ "Oktoba",
+ "Novemba",
+ "Desemba",
+ ]
+ month_abbreviations = [
+ "",
+ "Jan",
+ "Feb",
+ "Mac",
+ "Apr",
+ "Mei",
+ "Jun",
+ "Jul",
+ "Ago",
+ "Sep",
+ "Okt",
+ "Nov",
+ "Des",
+ ]
+
+ day_names = [
+ "",
+ "Jumatatu",
+ "Jumanne",
+ "Jumatano",
+ "Alhamisi",
+ "Ijumaa",
+ "Jumamosi",
+ "Jumapili",
+ ]
+ day_abbreviations = [
+ "",
+ "Jumatatu",
+ "Jumanne",
+ "Jumatano",
+ "Alhamisi",
+ "Ijumaa",
+ "Jumamosi",
+ "Jumapili",
+ ]
+
+
+_locales = _map_locales()
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/parser.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/parser.py
new file mode 100644
index 0000000000..243fd1721c
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/parser.py
@@ -0,0 +1,596 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
+import re
+from datetime import datetime, timedelta
+
+from dateutil import tz
+
+from arrow import locales
+from arrow.util import iso_to_gregorian, next_weekday, normalize_timestamp
+
+try:
+ from functools import lru_cache
+except ImportError: # pragma: no cover
+ from backports.functools_lru_cache import lru_cache # pragma: no cover
+
+
+class ParserError(ValueError):
+ pass
+
+
+# Allows for ParserErrors to be propagated from _build_datetime()
+# when day_of_year errors occur.
+# Before this, the ParserErrors were caught by the try/except in
+# _parse_multiformat() and the appropriate error message was not
+# transmitted to the user.
+class ParserMatchError(ParserError):
+ pass
+
+
+class DateTimeParser(object):
+
+ _FORMAT_RE = re.compile(
+ r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)"
+ )
+ _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]")
+
+ _ONE_OR_TWO_DIGIT_RE = re.compile(r"\d{1,2}")
+ _ONE_OR_TWO_OR_THREE_DIGIT_RE = re.compile(r"\d{1,3}")
+ _ONE_OR_MORE_DIGIT_RE = re.compile(r"\d+")
+ _TWO_DIGIT_RE = re.compile(r"\d{2}")
+ _THREE_DIGIT_RE = re.compile(r"\d{3}")
+ _FOUR_DIGIT_RE = re.compile(r"\d{4}")
+ _TZ_Z_RE = re.compile(r"([\+\-])(\d{2})(?:(\d{2}))?|Z")
+ _TZ_ZZ_RE = re.compile(r"([\+\-])(\d{2})(?:\:(\d{2}))?|Z")
+ _TZ_NAME_RE = re.compile(r"\w[\w+\-/]+")
+ # NOTE: timestamps cannot be parsed from natural language strings (by removing the ^...$) because it will
+ # break cases like "15 Jul 2000" and a format list (see issue #447)
+ _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$")
+ _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$")
+ _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$")
+ _WEEK_DATE_RE = re.compile(r"(?P\d{4})[\-]?W(?P\d{2})[\-]?(?P\d)?")
+
+ _BASE_INPUT_RE_MAP = {
+ "YYYY": _FOUR_DIGIT_RE,
+ "YY": _TWO_DIGIT_RE,
+ "MM": _TWO_DIGIT_RE,
+ "M": _ONE_OR_TWO_DIGIT_RE,
+ "DDDD": _THREE_DIGIT_RE,
+ "DDD": _ONE_OR_TWO_OR_THREE_DIGIT_RE,
+ "DD": _TWO_DIGIT_RE,
+ "D": _ONE_OR_TWO_DIGIT_RE,
+ "HH": _TWO_DIGIT_RE,
+ "H": _ONE_OR_TWO_DIGIT_RE,
+ "hh": _TWO_DIGIT_RE,
+ "h": _ONE_OR_TWO_DIGIT_RE,
+ "mm": _TWO_DIGIT_RE,
+ "m": _ONE_OR_TWO_DIGIT_RE,
+ "ss": _TWO_DIGIT_RE,
+ "s": _ONE_OR_TWO_DIGIT_RE,
+ "X": _TIMESTAMP_RE,
+ "x": _TIMESTAMP_EXPANDED_RE,
+ "ZZZ": _TZ_NAME_RE,
+ "ZZ": _TZ_ZZ_RE,
+ "Z": _TZ_Z_RE,
+ "S": _ONE_OR_MORE_DIGIT_RE,
+ "W": _WEEK_DATE_RE,
+ }
+
+ SEPARATORS = ["-", "/", "."]
+
+ def __init__(self, locale="en_us", cache_size=0):
+
+ self.locale = locales.get_locale(locale)
+ self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
+ self._input_re_map.update(
+ {
+ "MMMM": self._generate_choice_re(
+ self.locale.month_names[1:], re.IGNORECASE
+ ),
+ "MMM": self._generate_choice_re(
+ self.locale.month_abbreviations[1:], re.IGNORECASE
+ ),
+ "Do": re.compile(self.locale.ordinal_day_re),
+ "dddd": self._generate_choice_re(
+ self.locale.day_names[1:], re.IGNORECASE
+ ),
+ "ddd": self._generate_choice_re(
+ self.locale.day_abbreviations[1:], re.IGNORECASE
+ ),
+ "d": re.compile(r"[1-7]"),
+ "a": self._generate_choice_re(
+ (self.locale.meridians["am"], self.locale.meridians["pm"])
+ ),
+ # note: 'A' token accepts both 'am/pm' and 'AM/PM' formats to
+ # ensure backwards compatibility of this token
+ "A": self._generate_choice_re(self.locale.meridians.values()),
+ }
+ )
+ if cache_size > 0:
+ self._generate_pattern_re = lru_cache(maxsize=cache_size)(
+ self._generate_pattern_re
+ )
+
+ # TODO: since we support more than ISO 8601, we should rename this function
+ # IDEA: break into multiple functions
+ def parse_iso(self, datetime_string, normalize_whitespace=False):
+
+ if normalize_whitespace:
+ datetime_string = re.sub(r"\s+", " ", datetime_string.strip())
+
+ has_space_divider = " " in datetime_string
+ has_t_divider = "T" in datetime_string
+
+ num_spaces = datetime_string.count(" ")
+ if has_space_divider and num_spaces != 1 or has_t_divider and num_spaces > 0:
+ raise ParserError(
+ "Expected an ISO 8601-like string, but was given '{}'. Try passing in a format string to resolve this.".format(
+ datetime_string
+ )
+ )
+
+ has_time = has_space_divider or has_t_divider
+ has_tz = False
+
+ # date formats (ISO 8601 and others) to test against
+ # NOTE: YYYYMM is omitted to avoid confusion with YYMMDD (no longer part of ISO 8601, but is still often used)
+ formats = [
+ "YYYY-MM-DD",
+ "YYYY-M-DD",
+ "YYYY-M-D",
+ "YYYY/MM/DD",
+ "YYYY/M/DD",
+ "YYYY/M/D",
+ "YYYY.MM.DD",
+ "YYYY.M.DD",
+ "YYYY.M.D",
+ "YYYYMMDD",
+ "YYYY-DDDD",
+ "YYYYDDDD",
+ "YYYY-MM",
+ "YYYY/MM",
+ "YYYY.MM",
+ "YYYY",
+ "W",
+ ]
+
+ if has_time:
+
+ if has_space_divider:
+ date_string, time_string = datetime_string.split(" ", 1)
+ else:
+ date_string, time_string = datetime_string.split("T", 1)
+
+ time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE)
+
+ time_components = self._TIME_RE.match(time_parts[0])
+
+ if time_components is None:
+ raise ParserError(
+ "Invalid time component provided. Please specify a format or provide a valid time component in the basic or extended ISO 8601 time format."
+ )
+
+ (
+ hours,
+ minutes,
+ seconds,
+ subseconds_sep,
+ subseconds,
+ ) = time_components.groups()
+
+ has_tz = len(time_parts) == 2
+ has_minutes = minutes is not None
+ has_seconds = seconds is not None
+ has_subseconds = subseconds is not None
+
+ is_basic_time_format = ":" not in time_parts[0]
+ tz_format = "Z"
+
+ # use 'ZZ' token instead since tz offset is present in non-basic format
+ if has_tz and ":" in time_parts[1]:
+ tz_format = "ZZ"
+
+ time_sep = "" if is_basic_time_format else ":"
+
+ if has_subseconds:
+ time_string = "HH{time_sep}mm{time_sep}ss{subseconds_sep}S".format(
+ time_sep=time_sep, subseconds_sep=subseconds_sep
+ )
+ elif has_seconds:
+ time_string = "HH{time_sep}mm{time_sep}ss".format(time_sep=time_sep)
+ elif has_minutes:
+ time_string = "HH{time_sep}mm".format(time_sep=time_sep)
+ else:
+ time_string = "HH"
+
+ if has_space_divider:
+ formats = ["{} {}".format(f, time_string) for f in formats]
+ else:
+ formats = ["{}T{}".format(f, time_string) for f in formats]
+
+ if has_time and has_tz:
+ # Add "Z" or "ZZ" to the format strings to indicate to
+ # _parse_token() that a timezone needs to be parsed
+ formats = ["{}{}".format(f, tz_format) for f in formats]
+
+ return self._parse_multiformat(datetime_string, formats)
+
+ def parse(self, datetime_string, fmt, normalize_whitespace=False):
+
+ if normalize_whitespace:
+ datetime_string = re.sub(r"\s+", " ", datetime_string)
+
+ if isinstance(fmt, list):
+ return self._parse_multiformat(datetime_string, fmt)
+
+ fmt_tokens, fmt_pattern_re = self._generate_pattern_re(fmt)
+
+ match = fmt_pattern_re.search(datetime_string)
+
+ if match is None:
+ raise ParserMatchError(
+ "Failed to match '{}' when parsing '{}'".format(fmt, datetime_string)
+ )
+
+ parts = {}
+ for token in fmt_tokens:
+ if token == "Do":
+ value = match.group("value")
+ elif token == "W":
+ value = (match.group("year"), match.group("week"), match.group("day"))
+ else:
+ value = match.group(token)
+ self._parse_token(token, value, parts)
+
+ return self._build_datetime(parts)
+
+ def _generate_pattern_re(self, fmt):
+
+ # fmt is a string of tokens like 'YYYY-MM-DD'
+ # we construct a new string by replacing each
+ # token by its pattern:
+ # 'YYYY-MM-DD' -> '(?P\d{4})-(?P\d{2})-(?P\d{2})'
+ tokens = []
+ offset = 0
+
+ # Escape all special RegEx chars
+ escaped_fmt = re.escape(fmt)
+
+ # Extract the bracketed expressions to be reinserted later.
+ escaped_fmt = re.sub(self._ESCAPE_RE, "#", escaped_fmt)
+
+ # Any number of S is the same as one.
+ # TODO: allow users to specify the number of digits to parse
+ escaped_fmt = re.sub(r"S+", "S", escaped_fmt)
+
+ escaped_data = re.findall(self._ESCAPE_RE, fmt)
+
+ fmt_pattern = escaped_fmt
+
+ for m in self._FORMAT_RE.finditer(escaped_fmt):
+ token = m.group(0)
+ try:
+ input_re = self._input_re_map[token]
+ except KeyError:
+ raise ParserError("Unrecognized token '{}'".format(token))
+ input_pattern = "(?P<{}>{})".format(token, input_re.pattern)
+ tokens.append(token)
+ # a pattern doesn't have the same length as the token
+ # it replaces! We keep the difference in the offset variable.
+ # This works because the string is scanned left-to-right and matches
+ # are returned in the order found by finditer.
+ fmt_pattern = (
+ fmt_pattern[: m.start() + offset]
+ + input_pattern
+ + fmt_pattern[m.end() + offset :]
+ )
+ offset += len(input_pattern) - (m.end() - m.start())
+
+ final_fmt_pattern = ""
+ split_fmt = fmt_pattern.split(r"\#")
+
+ # Due to the way Python splits, 'split_fmt' will always be longer
+ for i in range(len(split_fmt)):
+ final_fmt_pattern += split_fmt[i]
+ if i < len(escaped_data):
+ final_fmt_pattern += escaped_data[i][1:-1]
+
+ # Wrap final_fmt_pattern in a custom word boundary to strictly
+ # match the formatting pattern and filter out date and time formats
+ # that include junk such as: blah1998-09-12 blah, blah 1998-09-12blah,
+ # blah1998-09-12blah. The custom word boundary matches every character
+ # that is not a whitespace character to allow for searching for a date
+ # and time string in a natural language sentence. Therefore, searching
+ # for a string of the form YYYY-MM-DD in "blah 1998-09-12 blah" will
+ # work properly.
+ # Certain punctuation before or after the target pattern such as
+ # "1998-09-12," is permitted. For the full list of valid punctuation,
+ # see the documentation.
+
+ starting_word_boundary = (
+ r"(?\s])" # This is the list of punctuation that is ok before the pattern (i.e. "It can't not be these characters before the pattern")
+ r"(\b|^)" # The \b is to block cases like 1201912 but allow 201912 for pattern YYYYMM. The ^ was necessary to allow a negative number through i.e. before epoch numbers
+ )
+ ending_word_boundary = (
+ r"(?=[\,\.\;\:\?\!\"\'\`\[\]\{\}\(\)\<\>]?" # Positive lookahead stating that these punctuation marks can appear after the pattern at most 1 time
+ r"(?!\S))" # Don't allow any non-whitespace character after the punctuation
+ )
+ bounded_fmt_pattern = r"{}{}{}".format(
+ starting_word_boundary, final_fmt_pattern, ending_word_boundary
+ )
+
+ return tokens, re.compile(bounded_fmt_pattern, flags=re.IGNORECASE)
+
+ def _parse_token(self, token, value, parts):
+
+ if token == "YYYY":
+ parts["year"] = int(value)
+
+ elif token == "YY":
+ value = int(value)
+ parts["year"] = 1900 + value if value > 68 else 2000 + value
+
+ elif token in ["MMMM", "MMM"]:
+ parts["month"] = self.locale.month_number(value.lower())
+
+ elif token in ["MM", "M"]:
+ parts["month"] = int(value)
+
+ elif token in ["DDDD", "DDD"]:
+ parts["day_of_year"] = int(value)
+
+ elif token in ["DD", "D"]:
+ parts["day"] = int(value)
+
+ elif token == "Do":
+ parts["day"] = int(value)
+
+ elif token == "dddd":
+ # locale day names are 1-indexed
+ day_of_week = [x.lower() for x in self.locale.day_names].index(
+ value.lower()
+ )
+ parts["day_of_week"] = day_of_week - 1
+
+ elif token == "ddd":
+ # locale day abbreviations are 1-indexed
+ day_of_week = [x.lower() for x in self.locale.day_abbreviations].index(
+ value.lower()
+ )
+ parts["day_of_week"] = day_of_week - 1
+
+ elif token.upper() in ["HH", "H"]:
+ parts["hour"] = int(value)
+
+ elif token in ["mm", "m"]:
+ parts["minute"] = int(value)
+
+ elif token in ["ss", "s"]:
+ parts["second"] = int(value)
+
+ elif token == "S":
+ # We have the *most significant* digits of an arbitrary-precision integer.
+ # We want the six most significant digits as an integer, rounded.
+ # IDEA: add nanosecond support somehow? Need datetime support for it first.
+ value = value.ljust(7, str("0"))
+
+ # floating-point (IEEE-754) defaults to half-to-even rounding
+ seventh_digit = int(value[6])
+ if seventh_digit == 5:
+ rounding = int(value[5]) % 2
+ elif seventh_digit > 5:
+ rounding = 1
+ else:
+ rounding = 0
+
+ parts["microsecond"] = int(value[:6]) + rounding
+
+ elif token == "X":
+ parts["timestamp"] = float(value)
+
+ elif token == "x":
+ parts["expanded_timestamp"] = int(value)
+
+ elif token in ["ZZZ", "ZZ", "Z"]:
+ parts["tzinfo"] = TzinfoParser.parse(value)
+
+ elif token in ["a", "A"]:
+ if value in (self.locale.meridians["am"], self.locale.meridians["AM"]):
+ parts["am_pm"] = "am"
+ elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]):
+ parts["am_pm"] = "pm"
+
+ elif token == "W":
+ parts["weekdate"] = value
+
+ @staticmethod
+ def _build_datetime(parts):
+
+ weekdate = parts.get("weekdate")
+
+ if weekdate is not None:
+ # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that
+ year, week = int(weekdate[0]), int(weekdate[1])
+
+ if weekdate[2] is not None:
+ day = int(weekdate[2])
+ else:
+ # day not given, default to 1
+ day = 1
+
+ dt = iso_to_gregorian(year, week, day)
+ parts["year"] = dt.year
+ parts["month"] = dt.month
+ parts["day"] = dt.day
+
+ timestamp = parts.get("timestamp")
+
+ if timestamp is not None:
+ return datetime.fromtimestamp(timestamp, tz=tz.tzutc())
+
+ expanded_timestamp = parts.get("expanded_timestamp")
+
+ if expanded_timestamp is not None:
+ return datetime.fromtimestamp(
+ normalize_timestamp(expanded_timestamp),
+ tz=tz.tzutc(),
+ )
+
+ day_of_year = parts.get("day_of_year")
+
+ if day_of_year is not None:
+ year = parts.get("year")
+ month = parts.get("month")
+ if year is None:
+ raise ParserError(
+ "Year component is required with the DDD and DDDD tokens."
+ )
+
+ if month is not None:
+ raise ParserError(
+ "Month component is not allowed with the DDD and DDDD tokens."
+ )
+
+ date_string = "{}-{}".format(year, day_of_year)
+ try:
+ dt = datetime.strptime(date_string, "%Y-%j")
+ except ValueError:
+ raise ParserError(
+ "The provided day of year '{}' is invalid.".format(day_of_year)
+ )
+
+ parts["year"] = dt.year
+ parts["month"] = dt.month
+ parts["day"] = dt.day
+
+ day_of_week = parts.get("day_of_week")
+ day = parts.get("day")
+
+ # If day is passed, ignore day of week
+ if day_of_week is not None and day is None:
+ year = parts.get("year", 1970)
+ month = parts.get("month", 1)
+ day = 1
+
+ # dddd => first day of week after epoch
+ # dddd YYYY => first day of week in specified year
+ # dddd MM YYYY => first day of week in specified year and month
+ # dddd MM => first day after epoch in specified month
+ next_weekday_dt = next_weekday(datetime(year, month, day), day_of_week)
+ parts["year"] = next_weekday_dt.year
+ parts["month"] = next_weekday_dt.month
+ parts["day"] = next_weekday_dt.day
+
+ am_pm = parts.get("am_pm")
+ hour = parts.get("hour", 0)
+
+ if am_pm == "pm" and hour < 12:
+ hour += 12
+ elif am_pm == "am" and hour == 12:
+ hour = 0
+
+ # Support for midnight at the end of day
+ if hour == 24:
+ if parts.get("minute", 0) != 0:
+ raise ParserError("Midnight at the end of day must not contain minutes")
+ if parts.get("second", 0) != 0:
+ raise ParserError("Midnight at the end of day must not contain seconds")
+ if parts.get("microsecond", 0) != 0:
+ raise ParserError(
+ "Midnight at the end of day must not contain microseconds"
+ )
+ hour = 0
+ day_increment = 1
+ else:
+ day_increment = 0
+
+ # account for rounding up to 1000000
+ microsecond = parts.get("microsecond", 0)
+ if microsecond == 1000000:
+ microsecond = 0
+ second_increment = 1
+ else:
+ second_increment = 0
+
+ increment = timedelta(days=day_increment, seconds=second_increment)
+
+ return (
+ datetime(
+ year=parts.get("year", 1),
+ month=parts.get("month", 1),
+ day=parts.get("day", 1),
+ hour=hour,
+ minute=parts.get("minute", 0),
+ second=parts.get("second", 0),
+ microsecond=microsecond,
+ tzinfo=parts.get("tzinfo"),
+ )
+ + increment
+ )
+
+ def _parse_multiformat(self, string, formats):
+
+ _datetime = None
+
+ for fmt in formats:
+ try:
+ _datetime = self.parse(string, fmt)
+ break
+ except ParserMatchError:
+ pass
+
+ if _datetime is None:
+ raise ParserError(
+ "Could not match input '{}' to any of the following formats: {}".format(
+ string, ", ".join(formats)
+ )
+ )
+
+ return _datetime
+
+ # generates a capture group of choices separated by an OR operator
+ @staticmethod
+ def _generate_choice_re(choices, flags=0):
+ return re.compile(r"({})".format("|".join(choices)), flags=flags)
+
+
+class TzinfoParser(object):
+ _TZINFO_RE = re.compile(r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$")
+
+ @classmethod
+ def parse(cls, tzinfo_string):
+
+ tzinfo = None
+
+ if tzinfo_string == "local":
+ tzinfo = tz.tzlocal()
+
+ elif tzinfo_string in ["utc", "UTC", "Z"]:
+ tzinfo = tz.tzutc()
+
+ else:
+
+ iso_match = cls._TZINFO_RE.match(tzinfo_string)
+
+ if iso_match:
+ sign, hours, minutes = iso_match.groups()
+ if minutes is None:
+ minutes = 0
+ seconds = int(hours) * 3600 + int(minutes) * 60
+
+ if sign == "-":
+ seconds *= -1
+
+ tzinfo = tz.tzoffset(None, seconds)
+
+ else:
+ tzinfo = tz.gettz(tzinfo_string)
+
+ if tzinfo is None:
+ raise ParserError(
+ 'Could not parse timezone expression "{}"'.format(tzinfo_string)
+ )
+
+ return tzinfo
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/arrow/util.py b/openpype/modules/ftrack/python2_vendor/arrow/arrow/util.py
new file mode 100644
index 0000000000..acce8878df
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/arrow/util.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import
+
+import datetime
+import numbers
+
+from dateutil.rrule import WEEKLY, rrule
+
+from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US
+
+
+def next_weekday(start_date, weekday):
+ """Get next weekday from the specified start date.
+
+ :param start_date: Datetime object representing the start date.
+ :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday).
+ :return: Datetime object corresponding to the next weekday after start_date.
+
+ Usage::
+
+ # Get first Monday after epoch
+ >>> next_weekday(datetime(1970, 1, 1), 0)
+ 1970-01-05 00:00:00
+
+ # Get first Thursday after epoch
+ >>> next_weekday(datetime(1970, 1, 1), 3)
+ 1970-01-01 00:00:00
+
+ # Get first Sunday after epoch
+ >>> next_weekday(datetime(1970, 1, 1), 6)
+ 1970-01-04 00:00:00
+ """
+ if weekday < 0 or weekday > 6:
+ raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).")
+ return rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0]
+
+
+def total_seconds(td):
+ """Get total seconds for timedelta."""
+ return td.total_seconds()
+
+
+def is_timestamp(value):
+ """Check if value is a valid timestamp."""
+ if isinstance(value, bool):
+ return False
+ if not (
+ isinstance(value, numbers.Integral)
+ or isinstance(value, float)
+ or isinstance(value, str)
+ ):
+ return False
+ try:
+ float(value)
+ return True
+ except ValueError:
+ return False
+
+
+def normalize_timestamp(timestamp):
+ """Normalize millisecond and microsecond timestamps into normal timestamps."""
+ if timestamp > MAX_TIMESTAMP:
+ if timestamp < MAX_TIMESTAMP_MS:
+ timestamp /= 1e3
+ elif timestamp < MAX_TIMESTAMP_US:
+ timestamp /= 1e6
+ else:
+ raise ValueError(
+ "The specified timestamp '{}' is too large.".format(timestamp)
+ )
+ return timestamp
+
+
+# Credit to https://stackoverflow.com/a/1700069
+def iso_to_gregorian(iso_year, iso_week, iso_day):
+ """Converts an ISO week date tuple into a datetime object."""
+
+ if not 1 <= iso_week <= 53:
+ raise ValueError("ISO Calendar week value must be between 1-53.")
+
+ if not 1 <= iso_day <= 7:
+ raise ValueError("ISO Calendar day value must be between 1-7")
+
+ # The first week of the year always contains 4 Jan.
+ fourth_jan = datetime.date(iso_year, 1, 4)
+ delta = datetime.timedelta(fourth_jan.isoweekday() - 1)
+ year_start = fourth_jan - delta
+ gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1)
+
+ return gregorian
+
+
+def validate_bounds(bounds):
+ if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]":
+ raise ValueError(
+ 'Invalid bounds. Please select between "()", "(]", "[)", or "[]".'
+ )
+
+
+# Python 2.7 / 3.0+ definitions for isstr function.
+
+try: # pragma: no cover
+ basestring
+
+ def isstr(s):
+ return isinstance(s, basestring) # noqa: F821
+
+
+except NameError: # pragma: no cover
+
+ def isstr(s):
+ return isinstance(s, str)
+
+
+__all__ = ["next_weekday", "total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"]
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile b/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile
new file mode 100644
index 0000000000..d4bb2cbb9e
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py b/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py
new file mode 100644
index 0000000000..aaf3c50822
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/docs/conf.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+
+# -- Path setup --------------------------------------------------------------
+
+import io
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath(".."))
+
+about = {}
+with io.open("../arrow/_version.py", "r", encoding="utf-8") as f:
+ exec(f.read(), about)
+
+# -- Project information -----------------------------------------------------
+
+project = u"Arrow 🏹"
+copyright = "2020, Chris Smith"
+author = "Chris Smith"
+
+release = about["__version__"]
+
+# -- General configuration ---------------------------------------------------
+
+extensions = ["sphinx.ext.autodoc"]
+
+templates_path = []
+
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+master_doc = "index"
+source_suffix = ".rst"
+pygments_style = "sphinx"
+
+language = None
+
+# -- Options for HTML output -------------------------------------------------
+
+html_theme = "alabaster"
+html_theme_path = []
+html_static_path = []
+
+html_show_sourcelink = False
+html_show_sphinx = False
+html_show_copyright = True
+
+# https://alabaster.readthedocs.io/en/latest/customization.html
+html_theme_options = {
+ "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.",
+ "github_user": "arrow-py",
+ "github_repo": "arrow",
+ "github_banner": True,
+ "show_related": False,
+ "show_powered_by": False,
+ "github_button": True,
+ "github_type": "star",
+ "github_count": "true", # must be a string
+}
+
+html_sidebars = {
+ "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"]
+}
diff --git a/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst b/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst
new file mode 100644
index 0000000000..e2830b04f3
--- /dev/null
+++ b/openpype/modules/ftrack/python2_vendor/arrow/docs/index.rst
@@ -0,0 +1,566 @@
+Arrow: Better dates & times for Python
+======================================
+
+Release v\ |release| (`Installation`_) (`Changelog `_)
+
+.. include:: ../README.rst
+ :start-after: start-inclusion-marker-do-not-remove
+ :end-before: end-inclusion-marker-do-not-remove
+
+User's Guide
+------------
+
+Creation
+~~~~~~~~
+
+Get 'now' easily:
+
+.. code-block:: python
+
+ >>> arrow.utcnow()
+
+
+ >>> arrow.now()
+
+
+ >>> arrow.now('US/Pacific')
+
+
+Create from timestamps (:code:`int` or :code:`float`):
+
+.. code-block:: python
+
+ >>> arrow.get(1367900664)
+
+
+ >>> arrow.get(1367900664.152325)
+
+
+Use a naive or timezone-aware datetime, or flexibly specify a timezone:
+
+.. code-block:: python
+
+ >>> arrow.get(datetime.utcnow())
+
+
+ >>> arrow.get(datetime(2013, 5, 5), 'US/Pacific')
+
+
+ >>> from dateutil import tz
+ >>> arrow.get(datetime(2013, 5, 5), tz.gettz('US/Pacific'))
+
+
+ >>> arrow.get(datetime.now(tz.gettz('US/Pacific')))
+
+
+Parse from a string:
+
+.. code-block:: python
+
+ >>> arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss')
+
+
+Search a date in a string:
+
+.. code-block:: python
+
+ >>> arrow.get('June was born in May 1980', 'MMMM YYYY')
+
+
+Some ISO 8601 compliant strings are recognized and parsed without a format string:
+
+ >>> arrow.get('2013-09-30T15:34:00.000-07:00')
+
+
+Arrow objects can be instantiated directly too, with the same arguments as a datetime:
+
+.. code-block:: python
+
+ >>> arrow.get(2013, 5, 5)
+
+
+ >>> arrow.Arrow(2013, 5, 5)
+
+
+Properties
+~~~~~~~~~~
+
+Get a datetime or timestamp representation:
+
+.. code-block:: python
+
+ >>> a = arrow.utcnow()
+ >>> a.datetime
+ datetime.datetime(2013, 5, 7, 4, 38, 15, 447644, tzinfo=tzutc())
+
+ >>> a.timestamp
+ 1367901495
+
+Get a naive datetime, and tzinfo:
+
+.. code-block:: python
+
+ >>> a.naive
+ datetime.datetime(2013, 5, 7, 4, 38, 15, 447644)
+
+ >>> a.tzinfo
+ tzutc()
+
+Get any datetime value:
+
+.. code-block:: python
+
+ >>> a.year
+ 2013
+
+Call datetime functions that return properties:
+
+.. code-block:: python
+
+ >>> a.date()
+ datetime.date(2013, 5, 7)
+
+ >>> a.time()
+ datetime.time(4, 38, 15, 447644)
+
+Replace & Shift
+~~~~~~~~~~~~~~~
+
+Get a new :class:`Arrow ` object, with altered attributes, just as you would with a datetime:
+
+.. code-block:: python
+
+ >>> arw = arrow.utcnow()
+ >>> arw
+
+
+ >>> arw.replace(hour=4, minute=40)
+
+
+Or, get one with attributes shifted forward or backward:
+
+.. code-block:: python
+
+ >>> arw.shift(weeks=+3)
+
+
+Even replace the timezone without altering other attributes:
+
+.. code-block:: python
+
+ >>> arw.replace(tzinfo='US/Pacific')
+
+
+Move between the earlier and later moments of an ambiguous time:
+
+.. code-block:: python
+
+ >>> paris_transition = arrow.Arrow(2019, 10, 27, 2, tzinfo="Europe/Paris", fold=0)
+ >>> paris_transition
+
+ >>> paris_transition.ambiguous
+ True
+ >>> paris_transition.replace(fold=1)
+
+
+Format
+~~~~~~
+
+.. code-block:: python
+
+ >>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ')
+ '2013-05-07 05:23:16 -00:00'
+
+Convert
+~~~~~~~
+
+Convert from UTC to other timezones by name or tzinfo:
+
+.. code-block:: python
+
+ >>> utc = arrow.utcnow()
+ >>> utc
+
+
+ >>> utc.to('US/Pacific')
+
+
+ >>> utc.to(tz.gettz('US/Pacific'))
+
+
+Or using shorthand:
+
+.. code-block:: python
+
+ >>> utc.to('local')
+
+
+ >>> utc.to('local').to('utc')
+
+
+
+Humanize
+~~~~~~~~
+
+Humanize relative to now:
+
+.. code-block:: python
+
+ >>> past = arrow.utcnow().shift(hours=-1)
+ >>> past.humanize()
+ 'an hour ago'
+
+Or another Arrow, or datetime:
+
+.. code-block:: python
+
+ >>> present = arrow.utcnow()
+ >>> future = present.shift(hours=2)
+ >>> future.humanize(present)
+ 'in 2 hours'
+
+Indicate time as relative or include only the distance
+
+.. code-block:: python
+
+ >>> present = arrow.utcnow()
+ >>> future = present.shift(hours=2)
+ >>> future.humanize(present)
+ 'in 2 hours'
+ >>> future.humanize(present, only_distance=True)
+ '2 hours'
+
+
+Indicate a specific time granularity (or multiple):
+
+.. code-block:: python
+
+ >>> present = arrow.utcnow()
+ >>> future = present.shift(minutes=66)
+ >>> future.humanize(present, granularity="minute")
+ 'in 66 minutes'
+ >>> future.humanize(present, granularity=["hour", "minute"])
+ 'in an hour and 6 minutes'
+ >>> present.humanize(future, granularity=["hour", "minute"])
+ 'an hour and 6 minutes ago'
+ >>> future.humanize(present, only_distance=True, granularity=["hour", "minute"])
+ 'an hour and 6 minutes'
+
+Support for a growing number of locales (see ``locales.py`` for supported languages):
+
+.. code-block:: python
+
+
+ >>> future = arrow.utcnow().shift(hours=1)
+ >>> future.humanize(a, locale='ru')
+ 'через 2 час(а,ов)'
+
+
+Ranges & Spans
+~~~~~~~~~~~~~~
+
+Get the time span of any unit:
+
+.. code-block:: python
+
+ >>> arrow.utcnow().span('hour')
+ (, )
+
+Or just get the floor and ceiling:
+
+.. code-block:: python
+
+ >>> arrow.utcnow().floor('hour')
+
+
+ >>> arrow.utcnow().ceil('hour')
+
+
+You can also get a range of time spans:
+
+.. code-block:: python
+
+ >>> start = datetime(2013, 5, 5, 12, 30)
+ >>> end = datetime(2013, 5, 5, 17, 15)
+ >>> for r in arrow.Arrow.span_range('hour', start, end):
+ ... print r
+ ...
+ (,