Merge branch 'refs/heads/develop' into feature/PYPE-463_sync_to_avalon_event

# Conflicts:
#	pype/ftrack/actions/action_create_cust_attrs.py
#	pype/ftrack/actions/action_sync_to_avalon.py
#	pype/ftrack/events/action_sync_to_avalon.py
#	pype/ftrack/events/event_del_avalon_id_from_new.py
#	pype/ftrack/events/event_sync_hier_attr.py
#	pype/ftrack/events/event_sync_to_avalon.py
#	pype/ftrack/events/event_user_assigment.py
This commit is contained in:
Milan Kolar 2019-11-27 18:12:07 +01:00
commit 42c815a57e
722 changed files with 2659 additions and 167002 deletions

View file

@ -1,2 +1,2 @@
from .lib import *
from .ftrack_server import FtrackServer
from .ftrack_server import FtrackServer, check_ftrack_url

View file

@ -1,6 +1,6 @@
import os
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pype.ftrack.lib.io_nonsingleton import DbConnector

View file

@ -2,7 +2,7 @@ import sys
import argparse
import logging
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -3,7 +3,7 @@ import sys
import argparse
import logging
import subprocess
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -4,7 +4,7 @@ import argparse
import json
import arrow
import logging
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pype.ftrack.lib.avalon_sync import cust_attr_id_key
from pypeapp import config

View file

@ -4,7 +4,7 @@ import logging
import argparse
import re
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from avalon import lib as avalonlib
from pype.ftrack.lib.io_nonsingleton import DbConnector

View file

@ -4,7 +4,7 @@ import re
import argparse
import logging
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pypeapp import config

View file

@ -4,7 +4,7 @@ import json
import argparse
import logging
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -3,7 +3,7 @@ import sys
import logging
from bson.objectid import ObjectId
import argparse
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pype.ftrack.lib.io_nonsingleton import DbConnector
@ -277,10 +277,7 @@ class DeleteAsset(BaseAction):
'message': 'No entities to delete in avalon'
}
or_subquery = []
for id in all_ids:
or_subquery.append({'_id': id})
delete_query = {'$or': or_subquery}
delete_query = {'_id': {'$in': all_ids}}
self.db.delete_many(delete_query)
return {

View file

@ -2,7 +2,7 @@ import os
import sys
import logging
import argparse
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pype.ftrack.lib.io_nonsingleton import DbConnector
@ -97,10 +97,7 @@ class AssetsRemover(BaseAction):
'message': 'None of assets'
}
or_subquery = []
for id in all_ids:
or_subquery.append({'_id': id})
delete_query = {'$or': or_subquery}
delete_query = {'_id': {'$in': all_ids}}
self.db.delete_many(delete_query)
self.db.uninstall()

View file

@ -4,7 +4,7 @@ import json
import logging
import subprocess
from operator import itemgetter
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pypeapp import Logger, config
@ -36,12 +36,13 @@ class DJVViewAction(BaseAction):
'file_ext', ["img", "mov", "exr"]
)
def register(self):
assert (self.djv_path is not None), (
'DJV View is not installed'
' or paths in presets are not set correctly'
)
super().register()
def preregister(self):
if self.djv_path is None:
return (
'DJV View is not installed'
' or paths in presets are not set correctly'
)
return True
def discover(self, session, entities, event):
"""Return available actions based on *event*. """

View file

@ -4,7 +4,7 @@ import argparse
import logging
import json
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
@ -108,6 +108,7 @@ class JobKiller(BaseAction):
'Changing Job ({}) status: {} -> failed'
).format(job['id'], origin_status))
except Exception:
session.rollback()
self.log.warning((
'Changing Job ({}) has failed'
).format(job['id']))

View file

@ -2,7 +2,7 @@ import os
import sys
import argparse
import logging
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -2,12 +2,12 @@ import os
import json
from ruamel import yaml
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pypeapp import config
from pype.ftrack.lib.avalon_sync import get_avalon_attr
from pype.vendor.ftrack_api import session as fa_session
from ftrack_api import session as fa_session
class PrepareProject(BaseAction):

View file

@ -7,7 +7,7 @@ import json
from pypeapp import Logger, config
from pype.ftrack import BaseAction
from pype.vendor import ftrack_api
import ftrack_api
from avalon import io, api
log = Logger().get_logger(__name__)
@ -61,12 +61,12 @@ class RVAction(BaseAction):
def set_rv_path(self):
self.rv_path = self.config_data.get("rv_path")
def register(self):
assert (self.rv_path is not None), (
'RV is not installed'
' or paths in presets are not set correctly'
)
super().register()
def preregister(self):
if self.rv_path is None:
return (
'RV is not installed or paths in presets are not set correctly'
)
return True
def get_components_from_entity(self, session, entity, components):
"""Get components from various entity types.

View file

@ -0,0 +1,347 @@
import os
from operator import itemgetter
from pype.ftrack import BaseAction
class SeedDebugProject(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
identifier = "seed.debug.project"
#: Action label.
label = "SeedDebugProject"
#: Action description.
description = "Description"
#: priority
priority = 100
#: roles that are allowed to register this action
role_list = ["Pypeclub"]
icon = "{}/ftrack/action_icons/SeedProject.svg".format(
os.environ.get("PYPE_STATICS_SERVER", "")
)
# Asset names which will be created in `Assets` entity
assets = [
"Addax", "Alpaca", "Ant", "Antelope", "Aye", "Badger", "Bear", "Bee",
"Beetle", "Bluebird", "Bongo", "Bontebok", "Butterflie", "Caiman",
"Capuchin", "Capybara", "Cat", "Caterpillar", "Coyote", "Crocodile",
"Cuckoo", "Deer", "Dragonfly", "Duck", "Eagle", "Egret", "Elephant",
"Falcon", "Fossa", "Fox", "Gazelle", "Gecko", "Gerbil",
"GiantArmadillo", "Gibbon", "Giraffe", "Goose", "Gorilla",
"Grasshoper", "Hare", "Hawk", "Hedgehog", "Heron", "Hog",
"Hummingbird", "Hyena", "Chameleon", "Cheetah", "Iguana", "Jackal",
"Jaguar", "Kingfisher", "Kinglet", "Kite", "Komodo", "Lemur",
"Leopard", "Lion", "Lizard", "Macaw", "Malachite", "Mandrill",
"Mantis", "Marmoset", "Meadowlark", "Meerkat", "Mockingbird",
"Mongoose", "Monkey", "Nyal", "Ocelot", "Okapi", "Oribi", "Oriole",
"Otter", "Owl", "Panda", "Parrot", "Pelican", "Pig", "Porcupine",
"Reedbuck", "Rhinocero", "Sandpiper", "Servil", "Skink", "Sloth",
"Snake", "Spider", "Squirrel", "Sunbird", "Swallow", "Swift", "Tiger",
"Sylph", "Tanager", "Vulture", "Warthog", "Waterbuck", "Woodpecker",
"Zebra"
]
# Tasks which will be created for Assets
asset_tasks = [
"Modeling", "Lookdev", "Rigging"
]
# Tasks which will be created for Shots
shot_tasks = [
"Animation", "Lighting", "Compositing", "FX"
]
# Define how much sequences will be created
default_seq_count = 5
# Define how much shots will be created for each sequence
default_shots_count = 10
existing_projects = None
new_project_item = "< New Project >"
current_project_item = "< Current Project >"
def discover(self, session, entities, event):
''' Validation '''
return True
def interface(self, session, entities, event):
if event["data"].get("values", {}):
return
title = "Select Project where you want to create seed data"
items = []
item_splitter = {"type": "label", "value": "---"}
description_label = {
"type": "label",
"value": (
"WARNING: Action does NOT check if entities already exist !!!"
)
}
items.append(description_label)
all_projects = session.query("select full_name from Project").all()
self.existing_projects = [proj["full_name"] for proj in all_projects]
projects_items = [
{"label": proj, "value": proj} for proj in self.existing_projects
]
data_items = []
data_items.append({
"label": self.new_project_item,
"value": self.new_project_item
})
data_items.append({
"label": self.current_project_item,
"value": self.current_project_item
})
data_items.extend(sorted(
projects_items,
key=itemgetter("label"),
reverse=False
))
projects_item = {
"label": "Choose Project",
"type": "enumerator",
"name": "project_name",
"data": data_items,
"value": self.current_project_item
}
items.append(projects_item)
items.append(item_splitter)
items.append({
"label": "Number of assets",
"type": "number",
"name": "asset_count",
"value": len(self.assets)
})
items.append({
"label": "Number of sequences",
"type": "number",
"name": "seq_count",
"value": self.default_seq_count
})
items.append({
"label": "Number of shots",
"type": "number",
"name": "shots_count",
"value": self.default_shots_count
})
items.append(item_splitter)
note_label = {
"type": "label",
"value": (
"<p><i>NOTE: Enter project name and choose schema if you "
"chose `\"< New Project >\"`(code is optional)</i><p>"
)
}
items.append(note_label)
items.append({
"label": "Project name",
"name": "new_project_name",
"type": "text",
"value": ""
})
project_schemas = [
sch["name"] for sch in self.session.query("ProjectSchema").all()
]
schemas_item = {
"label": "Choose Schema",
"type": "enumerator",
"name": "new_schema_name",
"data": [
{"label": sch, "value": sch} for sch in project_schemas
],
"value": project_schemas[0]
}
items.append(schemas_item)
items.append({
"label": "*Project code",
"name": "new_project_code",
"type": "text",
"value": "",
"empty_text": "Optional..."
})
return {
"items": items,
"title": title
}
def launch(self, session, in_entities, event):
if "values" not in event["data"]:
return
# THIS IS THE PROJECT PART
values = event["data"]["values"]
selected_project = values["project_name"]
if selected_project == self.new_project_item:
project_name = values["new_project_name"]
if project_name in self.existing_projects:
msg = "Project \"{}\" already exist".format(project_name)
self.log.error(msg)
return {"success": False, "message": msg}
project_code = values["new_project_code"]
project_schema_name = values["new_schema_name"]
if not project_code:
project_code = project_name
project_code = project_code.lower().replace(" ", "_").strip()
_project = session.query(
"Project where name is \"{}\"".format(project_code)
).first()
if _project:
msg = "Project with code \"{}\" already exist".format(
project_code
)
self.log.error(msg)
return {"success": False, "message": msg}
project_schema = session.query(
"ProjectSchema where name is \"{}\"".format(
project_schema_name
)
).one()
# Create the project with the chosen schema.
self.log.debug((
"*** Creating Project: name <{}>, code <{}>, schema <{}>"
).format(project_name, project_code, project_schema_name))
project = session.create("Project", {
"name": project_code,
"full_name": project_name,
"project_schema": project_schema
})
session.commit()
elif selected_project == self.current_project_item:
entity = in_entities[0]
if entity.entity_type.lower() == "project":
project = entity
else:
if "project" in entity:
project = entity["project"]
else:
project = entity["parent"]["project"]
project_schema = project["project_schema"]
self.log.debug((
"*** Using Project: name <{}>, code <{}>, schema <{}>"
).format(
project["full_name"], project["name"], project_schema["name"]
))
else:
project = session.query("Project where full_name is \"{}\"".format(
selected_project
)).one()
project_schema = project["project_schema"]
self.log.debug((
"*** Using Project: name <{}>, code <{}>, schema <{}>"
).format(
project["full_name"], project["name"], project_schema["name"]
))
# THIS IS THE MAGIC PART
task_types = {}
for _type in project_schema["_task_type_schema"]["types"]:
if _type["name"] not in task_types:
task_types[_type["name"]] = _type
self.task_types = task_types
asset_count = values.get("asset_count") or len(self.assets)
seq_count = values.get("seq_count") or self.default_seq_count
shots_count = values.get("shots_count") or self.default_shots_count
self.create_assets(project, asset_count)
self.create_shots(project, seq_count, shots_count)
return True
def create_assets(self, project, asset_count):
self.log.debug("*** Creating assets:")
main_entity = self.session.create("Folder", {
"name": "Assets",
"parent": project
})
self.log.debug("- Assets")
available_assets = len(self.assets)
repetitive_times = (
int(asset_count / available_assets) +
(asset_count % available_assets > 0)
)
created_assets = 0
for _asset_name in self.assets:
if created_assets >= asset_count:
break
for asset_num in range(1, repetitive_times + 1):
if created_assets >= asset_count:
break
asset_name = "%s_%02d" % (_asset_name, asset_num)
asset = self.session.create("AssetBuild", {
"name": asset_name,
"parent": main_entity
})
created_assets += 1
self.log.debug("- Assets/{}".format(asset_name))
for task_name in self.asset_tasks:
self.session.create("Task", {
"name": task_name,
"parent": asset,
"type": self.task_types[task_name]
})
self.log.debug("- Assets/{}/{}".format(
asset_name, task_name
))
self.log.debug("*** Commiting Assets")
self.session.commit()
def create_shots(self, project, seq_count, shots_count):
self.log.debug("*** Creating shots:")
main_entity = self.session.create("Folder", {
"name": "Shots",
"parent": project
})
self.log.debug("- Shots")
for seq_num in range(1, seq_count+1):
seq_name = "sq%03d" % seq_num
seq = self.session.create("Sequence", {
"name": seq_name,
"parent": main_entity
})
self.log.debug("- Shots/{}".format(seq_name))
for shot_num in range(1, shots_count+1):
shot_name = "%ssh%04d" % (seq_name, (shot_num*10))
shot = self.session.create("Shot", {
"name": shot_name,
"parent": seq
})
self.log.debug("- Shots/{}/{}".format(seq_name, shot_name))
for task_name in self.shot_tasks:
self.session.create("Task", {
"name": task_name,
"parent": shot,
"type": self.task_types[task_name]
})
self.log.debug("- Shots/{}/{}/{}".format(
seq_name, shot_name, task_name
))
self.log.debug("*** Commiting Shots")
self.session.commit()
def register(session, plugins_presets={}):
'''Register plugin. Called when used as an plugin.'''
SeedDebugProject(session, plugins_presets).register()

View file

@ -1,4 +1,4 @@
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -12,8 +12,8 @@ from pymongo import UpdateOne
from pype.ftrack import BaseAction
from pype.ftrack.lib import avalon_sync
from pype.ftrack.lib.io_nonsingleton import DbConnector
from pype.vendor import ftrack_api
from pype.vendor.ftrack_api import session as fa_session
import ftrack_api
from ftrack_api import session as fa_session
class SyncEntitiesFactory:
@ -318,8 +318,8 @@ class SyncEntitiesFactory:
def filter_by_duplicate_regex(self):
filter_queue = queue.Queue()
failed_regex_msg = "{} - Entity has invalid symbol/s in name"
duplicate_msg = "Multiple entities have name \"{}\":"
failed_regex_msg = "{} - Entity has invalid symbols in the name"
duplicate_msg = "There are multiple entities with the name: \"{}\":"
for ids in self.failed_regex.values():
for id in ids:
@ -547,15 +547,19 @@ class SyncEntitiesFactory:
])
cust_attr_query = (
"select value, entity_id from CustomAttributeValue "
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration.key in ({})"
)
[values] = self.session._call([{
call_expr = [{
"action": "query",
"expression": cust_attr_query.format(
entity_ids_joined, attributes_joined
)
}])
}]
if hasattr(self.session, "_call"):
[values] = self.session._call(call_expr)
else:
[values] = self.session.call(call_expr)
for value in values["data"]:
entity_id = value["entity_id"]
@ -608,13 +612,17 @@ class SyncEntitiesFactory:
attributes_joined = ", ".join([
"\"{}\"".format(name) for name in attribute_names
])
[values] = self.session._call([{
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id from CustomAttributeValue "
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration.key in ({})"
).format(entity_ids_joined, attributes_joined)
}])
}]
if hasattr(self.session, "_call"):
[values] = self.session._call(call_expr)
else:
[values] = self.session.call(call_expr)
avalon_hier = []
for value in values["data"]:
@ -749,8 +757,8 @@ class SyncEntitiesFactory:
if not_set_ids:
self.log.debug((
"- Debug information: Filtering bug, in entities dict are "
"empty dicts (function should not affect) <{}>"
"- Debug information: Filtering bug, there are empty dicts"
"in entities dict (functionality should not be affected) <{}>"
).format("| ".join(not_set_ids)))
for id in not_set_ids:
self.entities_dict.pop(id)
@ -913,7 +921,7 @@ class SyncEntitiesFactory:
self.deleted_entities = deleted_entities
self.log.debug((
"Ftrack -> Avalon comparation: New <{}> "
"Ftrack -> Avalon comparison: New <{}> "
"| Existing <{}> | Deleted <{}>"
).format(
len(create_ftrack_ids),
@ -1046,9 +1054,9 @@ class SyncEntitiesFactory:
))
self._ent_paths_by_ftrack_id.pop(ftrack_id, None)
msg = (
"<Entity renamed back> It is not allowed to change"
" name of entity or it's parents"
" that already has published context"
"<Entity renamed back> It is not possible to change"
" the name of an entity or it's parents, "
" if it already contained published data."
)
self.report_items["warning"][msg].append(ent_path)
@ -1068,14 +1076,14 @@ class SyncEntitiesFactory:
# TODO logging
ent_path = self.get_ent_path(ftrack_id)
msg = (
"<Entity moved back in hierachy> It is not allowed"
" to change hierarchy of entity or it's parents"
" that already has published context"
"<Entity moved back in hierachy> It is not possible"
" to change the hierarchy of an entity or it's parents,"
" if it already contained published data."
)
self.report_items["warning"][msg].append(ent_path)
self.log.warning((
"Entity has published context so was moved"
" back in hierarchy <{}>"
" Entity contains published data so it was moved"
" back to it's original hierarchy <{}>"
).format(ent_path))
self.entities_dict[ftrack_id]["entity"]["parent_id"] = (
old_ftrack_parent_id
@ -1116,14 +1124,14 @@ class SyncEntitiesFactory:
# TODO logging
# TODO report (turn off auto-sync?)
self.log.error((
"Entity has published context but was moved in"
" hierarchy and previous parent was not found so it is"
" not possible to solve this programmatically <{}>"
"The entity contains published data but it was moved to"
" a different place in the hierarchy and it's previous"
" parent cannot be found."
" It's impossible to solve this programmatically <{}>"
).format(ent_path))
msg = (
"<Entity not synchronizable> Parent of entity can't be"
" changed due to published context and previous parent"
" was not found"
"<Entity can't be synchronised> Hierarchy of an entity" " can't be changed due to published data and missing"
" previous parent"
)
self.report_items["error"][msg].append(ent_path)
self.filter_with_children(ftrack_id)
@ -1143,14 +1151,15 @@ class SyncEntitiesFactory:
):
ent_path = self.get_ent_path(ftrack_id)
self.log.error((
"Entity has published context but was moved in"
" hierarchy and previous parents were moved too it is"
" not possible to solve this programmatically <{}>"
"The entity contains published data but it was moved to"
" a different place in the hierarchy and it's previous"
" parents were moved too."
" It's impossible to solve this programmatically <{}>"
).format(ent_path))
msg = (
"<Entity not synchronizable> Parent of entity can't be"
" changed due to published context but whole hierarchy"
" was scrambled"
"<Entity not synchronizable> Hierarchy of an entity"
" can't be changed due to published data and scrambled"
"hierarchy"
)
continue
@ -1160,8 +1169,8 @@ class SyncEntitiesFactory:
entities_to_create = []
# TODO logging
self.log.warning(
"Ftrack entities must be recreated because have"
" published context but were removed"
"Ftrack entities must be recreated because they were deleted,"
" but they contain published data."
)
_avalon_ent = old_parent_ent
@ -1178,13 +1187,13 @@ class SyncEntitiesFactory:
# TODO report
# TODO logging
self.log.error((
"Can't recreate entity in Ftrack because entity with"
" same name already exists in different hierarchy <{}>"
"Can't recreate the entity in Ftrack because an entity" " with the same name already exists in a different"
" place in the hierarchy <{}>"
).format(av_ent_path))
msg = (
"<Entity not synchronizable> Parent of entity can't be"
" changed due to published context but previous parent"
" had name that exist in different hierarchy level"
"<Entity not synchronizable> Hierarchy of an entity"
" can't be changed. I contains published data and it's" " previous parent had a name, that is duplicated at a "
" different hierarchy level"
)
self.report_items["error"][msg].append(av_ent_path)
self.filter_with_children(ftrack_id)
@ -1526,7 +1535,7 @@ class SyncEntitiesFactory:
else:
# TODO logging - What is happening here?
self.log.warning((
"In avalon are entities without valid parents that"
"Avalon contains entities without valid parents that"
" lead to Project (should not cause errors)"
" - MongoId <{}>"
).format(str(entity_id)))
@ -1670,7 +1679,7 @@ class SyncEntitiesFactory:
"Project code was changed back to \"{}\"".format(avalon_code)
)
msg = (
"It is not allowed to change"
"It is not possible to change"
" project code after synchronization"
)
self.report_items["warning"][msg] = sub_msg
@ -1895,8 +1904,8 @@ class SyncEntitiesFactory:
ent_path = self.get_ent_path(new_entity_id)
msg = (
"Deleted entity was recreated because had (or his children)"
" published context"
"Deleted entity was recreated because it or its children"
" contain published data"
)
self.report_items["info"][msg].append(ent_path)
@ -1906,7 +1915,7 @@ class SyncEntitiesFactory:
def regex_duplicate_interface(self):
items = []
if self.failed_regex or self.tasks_failed_regex:
subtitle = "Not allowed symbols in entity names:"
subtitle = "Entity names contain prohibited symbols:"
items.append({
"type": "label",
"value": "# {}".format(subtitle)
@ -1914,7 +1923,7 @@ class SyncEntitiesFactory:
items.append({
"type": "label",
"value": (
"<p><i>NOTE: Allowed symbols are Letters( a-Z ),"
"<p><i>NOTE: You can use Letters( a-Z ),"
" Numbers( 0-9 ) and Underscore( _ )</i></p>"
)
})
@ -1967,8 +1976,8 @@ class SyncEntitiesFactory:
items.append({
"type": "label",
"value": (
"<p><i>NOTE: It is not allowed to have same name"
" for multiple entities in one project</i></p>"
"<p><i>NOTE: It is not allowed to use the same name"
" for multiple entities in the same project</i></p>"
)
})
log_msgs = []
@ -2165,7 +2174,7 @@ class SyncToAvalonLocal(BaseAction):
self.log.error(
"Synchronization failed due to code error", exc_info=True
)
msg = "An error has happened during synchronization"
msg = "An error occurred during synchronization"
title = "Synchronization report ({}):".format(ft_project_name)
items = []
items.append({

View file

@ -6,7 +6,7 @@ import collections
import json
import re
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from avalon import io, inventory, schema

View file

@ -4,7 +4,7 @@ import argparse
import logging
import json
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
@ -43,7 +43,7 @@ class ThumbToChildren(BaseAction):
'description': 'Push thumbnails to Childrens'
})
})
session.commit()
try:
for entity in entities:
thumbid = entity['thumbnail_id']
@ -53,10 +53,11 @@ class ThumbToChildren(BaseAction):
# inform the user that the job is done
job['status'] = 'done'
except Exception:
except Exception as exc:
session.rollback()
# fail the job if something goes wrong
job['status'] = 'failed'
raise
raise exc
finally:
session.commit()

View file

@ -3,7 +3,7 @@ import sys
import argparse
import logging
import json
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
@ -40,9 +40,9 @@ class ThumbToParent(BaseAction):
'status': 'running',
'data': json.dumps({
'description': 'Push thumbnails to parents'
})
})
})
session.commit()
try:
for entity in entities:
parent = None
@ -74,10 +74,11 @@ class ThumbToParent(BaseAction):
# inform the user that the job is done
job['status'] = status or 'done'
except Exception as e:
except Exception as exc:
session.rollback()
# fail the job if something goes wrong
job['status'] = 'failed'
raise e
raise exc
finally:
session.commit()

View file

@ -6,7 +6,7 @@ import collections
import json
import re
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from avalon import io, inventory, schema
from pype.ftrack.lib.io_nonsingleton import DbConnector

View file

@ -1,7 +1,7 @@
import os
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction
from pype.vendor.ftrack_api import session as fa_session
from ftrack_api import session as fa_session
class ActionAskWhereIRun(BaseAction):

View file

@ -1,7 +1,7 @@
import platform
import socket
import getpass
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseAction

View file

@ -12,8 +12,8 @@ from pymongo import UpdateOne
from pype.ftrack import BaseAction
from pype.ftrack.lib import avalon_sync
from pype.ftrack.lib.io_nonsingleton import DbConnector
from pype.vendor import ftrack_api
from pype.vendor.ftrack_api import session as fa_session
import ftrack_api
from ftrack_api import session as fa_session
from pypeapp import config
@ -548,7 +548,7 @@ class SyncEntitiesFactory:
])
cust_attr_query = (
"select value, entity_id from CustomAttributeValue "
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration.key in ({})"
)
[values] = self.session._call([{
@ -612,7 +612,7 @@ class SyncEntitiesFactory:
[values] = self.session._call([{
"action": "query",
"expression": (
"select value, entity_id from CustomAttributeValue "
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration.key in ({})"
).format(entity_ids_joined, attributes_joined)
}])

View file

@ -1,4 +1,4 @@
from pype.ftrack import BaseEvent
import BaseEvent
from pype.ftrack.lib import avalon_sync
from pype.ftrack.events.event_sync_to_avalon import SyncToAvalonEvent

View file

@ -1,4 +1,4 @@
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseEvent
import operator
@ -80,10 +80,10 @@ class NextTaskUpdate(BaseEvent):
'>>> [ {} ] updated to [ Ready ]'
).format(path))
except Exception as e:
session.rollback()
self.log.warning((
'!!! [ {} ] status couldnt be set: [ {} ]'
).format(path, e))
session.rollback()
).format(path, str(e)), exc_info=True)
def register(session, plugins_presets):

View file

@ -1,8 +1,8 @@
from pype.vendor import ftrack_api
from pype.ftrack import BaseEvent
import ftrack_api
from pype.ftrack.lib import BaseEvent
class Radio_buttons(BaseEvent):
class RadioButtons(BaseEvent):
ignore_me = True
@ -37,4 +37,4 @@ class Radio_buttons(BaseEvent):
def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
Radio_buttons(session, plugins_presets).register()
RadioButtons(session, plugins_presets).register()

View file

@ -14,8 +14,8 @@ from pype.ftrack.lib import avalon_sync
from pype.ftrack.lib.avalon_sync import (
cust_attr_id_key, cust_attr_auto_sync, entity_schemas
)
from pype.vendor import ftrack_api
from pype.vendor.ftrack_api import session as fa_session
import ftrack_api
from ftrack_api import session as fa_session
from pype.ftrack import BaseEvent
from pype.ftrack.lib.io_nonsingleton import DbConnector

View file

@ -1,11 +1,11 @@
import os
import sys
import re
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseEvent
class Test_Event(BaseEvent):
class TestEvent(BaseEvent):
ignore_me = True
@ -23,4 +23,4 @@ class Test_Event(BaseEvent):
def register(session, plugins_presets):
'''Register plugin. Called when used as an plugin.'''
Test_Event(session, plugins_presets).register()
TestEvent(session, plugins_presets).register()

View file

@ -1,4 +1,4 @@
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseEvent
@ -49,7 +49,10 @@ class ThumbnailEvents(BaseEvent):
self.log.info(msg)
session.commit()
try:
session.commit()
except Exception:
session.rollback()
def register(session, plugins_presets):

View file

@ -1,4 +1,4 @@
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseEvent
from pype.ftrack.lib import avalon_sync
from pype.ftrack.lib.io_nonsingleton import DbConnector

View file

@ -1,4 +1,4 @@
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack import BaseEvent
@ -6,7 +6,6 @@ class VersionToTaskStatus(BaseEvent):
def launch(self, session, event):
'''Propagates status from version to task when changed'''
session.commit()
# start of event procedure ----------------------------------
for entity in event['data'].get('entities', []):
@ -62,8 +61,10 @@ class VersionToTaskStatus(BaseEvent):
task['status'] = task_status
session.commit()
except Exception as e:
session.rollback()
self.log.warning('!!! [ {} ] status couldnt be set:\
[ {} ]'.format(path, e))
session.rollback()
else:
self.log.info('>>> [ {} ] updated to [ {} ]'.format(
path, task_status['name']))

View file

@ -1 +1,2 @@
from .ftrack_server import FtrackServer
from .lib import check_ftrack_url

View file

@ -9,11 +9,12 @@ import atexit
import time
from urllib.parse import urlparse
import requests
from pype.vendor import ftrack_api
import ftrack_api
from pype.ftrack.lib import credentials
from pype.ftrack.ftrack_server import FtrackServer
from pype.ftrack.ftrack_server.lib import ftrack_events_mongo_settings
from pype.ftrack.ftrack_server.lib import (
ftrack_events_mongo_settings, check_ftrack_url
)
import socket_thread
@ -25,36 +26,6 @@ class MongoPermissionsError(Exception):
super().__init__(message)
def check_ftrack_url(url, log_errors=True):
"""Checks if Ftrack server is responding"""
if not url:
print('ERROR: Ftrack URL is not set!')
return None
url = url.strip('/ ')
if 'http' not in url:
if url.endswith('ftrackapp.com'):
url = 'https://' + url
else:
url = 'https://{0}.ftrackapp.com'.format(url)
try:
result = requests.get(url, allow_redirects=False)
except requests.exceptions.RequestException:
if log_errors:
print('ERROR: Entered Ftrack URL is not accesible!')
return False
if (result.status_code != 200 or 'FTRACK_VERSION' not in result.headers):
if log_errors:
print('ERROR: Entered Ftrack URL is not accesible!')
return False
print('DEBUG: Ftrack server {} is accessible.'.format(url))
return url
def check_mongo_url(host, port, log_error=False):
"""Checks if mongo server is responding"""
sock = None

View file

@ -2,7 +2,7 @@ import os
import sys
import types
import importlib
from pype.vendor import ftrack_api
import ftrack_api
import time
import logging
import inspect
@ -100,7 +100,10 @@ class FtrackServer:
log.warning(msg, exc_info=e)
if len(register_functions_dict) < 1:
raise Exception
raise Exception((
"There are no events with register function."
" Registered paths: \"{}\""
).format("| ".join(paths)))
# Load presets for setting plugins
key = "user"

View file

@ -1,4 +1,5 @@
import os
import requests
try:
from urllib.parse import urlparse, parse_qs
except ImportError:
@ -66,3 +67,33 @@ def get_ftrack_event_mongo_info():
url = "mongodb://{}{}{}{}".format(user_pass, socket_path, dab, auth)
return url, database, collection
def check_ftrack_url(url, log_errors=True):
"""Checks if Ftrack server is responding"""
if not url:
print('ERROR: Ftrack URL is not set!')
return None
url = url.strip('/ ')
if 'http' not in url:
if url.endswith('ftrackapp.com'):
url = 'https://' + url
else:
url = 'https://{0}.ftrackapp.com'.format(url)
try:
result = requests.get(url, allow_redirects=False)
except requests.exceptions.RequestException:
if log_errors:
print('ERROR: Entered Ftrack URL is not accesible!')
return False
if (result.status_code != 200 or 'FTRACK_VERSION' not in result.headers):
if log_errors:
print('ERROR: Entered Ftrack URL is not accesible!')
return False
print('DEBUG: Ftrack server {} is accessible.'.format(url))
return url

View file

@ -30,8 +30,7 @@ def main(args):
server.run_server(session)
except Exception as exc:
import traceback
traceback.print_tb(exc.__traceback__)
log.error("Event server crashed. See traceback below", exc_info=True)
finally:
log.debug("First closing socket")

View file

@ -6,8 +6,8 @@ import signal
import threading
from ftrack_server import FtrackServer
from pype.vendor import ftrack_api
from pype.vendor.ftrack_api.event.hub import EventHub
import ftrack_api
from ftrack_api.event.hub import EventHub
from pypeapp import Logger
log = Logger().get_logger("Event Server Legacy")

View file

@ -1,6 +1,6 @@
import os
import json
from pype.vendor import ftrack_api
import ftrack_api
import appdirs

View file

@ -22,7 +22,12 @@ import pymongo
from pymongo.client_session import ClientSession
class NotActiveTable(Exception):
pass
def __init__(self, *args, **kwargs):
msg = "Active table is not set. (This is bug)"
if not (args or kwargs):
args = (default_message,)
super().__init__(*args, **kwargs)
def auto_reconnect(func):
"""Handling auto reconnect in 3 retry times"""
@ -37,7 +42,16 @@ def auto_reconnect(func):
time.sleep(0.1)
else:
raise
return decorated
def check_active_table(func):
"""Check if DbConnector has active table before db method is called"""
@functools.wraps(func)
def decorated(obj, *args, **kwargs):
if not obj.active_table:
raise NotActiveTable()
return func(obj, *args, **kwargs)
return decorated
@ -53,7 +67,6 @@ def check_active_table(func):
class DbConnector:
log = logging.getLogger(__name__)
timeout = 1000
@ -68,10 +81,18 @@ class DbConnector:
self.active_table = table_name
def __getitem__(self, key):
# gives direct access to collection withou setting `active_table`
return self._database[key]
def __getattribute__(self, attr):
# not all methods of PyMongo database are implemented with this it is
# possible to use them too
try:
return super().__getattribute__(attr)
return super(DbConnector, self).__getattribute__(attr)
except AttributeError:
if self.active_table is None:
raise NotActiveTable()
return self._database[self.active_table].__getattribute__(attr)
def install(self):
@ -131,6 +152,15 @@ class DbConnector:
def exist_table(self, table_name):
return table_name in self.tables()
def create_table(self, name, **options):
if self.exist_table(name):
return
return self._database.create_collection(name, **options)
def exist_table(self, table_name):
return table_name in self.tables()
def tables(self):
"""List available tables
Returns:
@ -166,18 +196,21 @@ class DbConnector:
@check_active_table
@auto_reconnect
def find(self, filter, projection=None, sort=None, **options):
options["projection"] = projection
options["sort"] = sort
return self._database[self.active_table].find(filter, **options)
return self._database[self.active_table].find(
filter, projection, **options
)
@check_active_table
@auto_reconnect
def find_one(self, filter, projection=None, sort=None, **options):
assert isinstance(filter, dict), "filter must be <dict>"
options["projection"] = projection
options["sort"] = sort
return self._database[self.active_table].find_one(filter, **options)
return self._database[self.active_table].find_one(
filter,
projection,
**options
)
@check_active_table
@auto_reconnect
@ -202,8 +235,8 @@ class DbConnector:
@check_active_table
@auto_reconnect
def distinct(self, *args, **kwargs):
return self._database[self.active_table].distinct(*args, **kwargs)
def distinct(self, **options):
return self._database[self.active_table].distinct(**options)
@check_active_table
@auto_reconnect
@ -216,10 +249,14 @@ class DbConnector:
@auto_reconnect
def delete_one(self, filter, collation=None, **options):
options["collation"] = collation
return self._database[self.active_table].delete_one(filter, **options)
return self._database[self.active_table].delete_one(
filter, **options
)
@check_active_table
@auto_reconnect
def delete_many(self, filter, collation=None, **options):
options["collation"] = collation
return self._database[self.active_table].delete_many(filter, **options)
return self._database[self.active_table].delete_many(
filter, **options
)

View file

@ -345,25 +345,44 @@ class AppAction(BaseHandler):
statuses = presets['status_update']
actual_status = entity['status']['name'].lower()
next_status_name = None
for key, value in statuses.items():
if actual_status in value or '_any_' in value:
if key != '_ignore_':
next_status_name = key
already_tested = []
ent_path = "/".join(
[ent["name"] for ent in entity['link']]
)
while True:
next_status_name = None
for key, value in statuses.items():
if key in already_tested:
continue
if actual_status in value or '_any_' in value:
if key != '_ignore_':
next_status_name = key
already_tested.append(key)
break
already_tested.append(key)
if next_status_name is None:
break
if next_status_name is not None:
try:
query = 'Status where name is "{}"'.format(
next_status_name
)
status = session.query(query).one()
entity['status'] = status
session.commit()
self.log.debug("Changing status to \"{}\" <{}>".format(
next_status_name, ent_path
))
break
except Exception:
session.rollback()
msg = (
'Status "{}" in presets wasn\'t found on Ftrack'
).format(next_status_name)
'Status "{}" in presets wasn\'t found'
' on Ftrack entity type "{}"'
).format(next_status_name, entity.entity_type)
self.log.warning(msg)
# Set origin avalon environments

View file

@ -1,8 +1,8 @@
import functools
import time
from pypeapp import Logger
from pype.vendor import ftrack_api
from pype.vendor.ftrack_api import session as fa_session
import ftrack_api
from ftrack_api import session as fa_session
from pype.ftrack.ftrack_server import session_processor
@ -13,6 +13,13 @@ class MissingPermision(Exception):
super().__init__(message)
class PreregisterException(Exception):
def __init__(self, message=None):
if not message:
message = "Pre-registration conditions were not met"
super().__init__(message)
class BaseHandler(object):
'''Custom Action base class
@ -89,15 +96,17 @@ class BaseHandler(object):
'!{} "{}" - You\'re missing required {} permissions'
).format(self.type, label, str(MPE)))
except AssertionError as ae:
self.log.info((
self.log.warning((
'!{} "{}" - {}'
).format(self.type, label, str(ae)))
except NotImplementedError:
self.log.error((
'{} "{}" - Register method is not implemented'
).format(
self.type, label)
)
).format(self.type, label))
except PreregisterException as exc:
self.log.warning((
'{} "{}" - {}'
).format(self.type, label, str(exc)))
except Exception as e:
self.log.error('{} "{}" - Registration failed ({})'.format(
self.type, label, str(e))
@ -119,6 +128,7 @@ class BaseHandler(object):
try:
return func(*args, **kwargs)
except Exception as exc:
self.session.rollback()
msg = '{} "{}": Failed ({})'.format(self.type, label, str(exc))
self.log.error(msg, exc_info=True)
return {
@ -163,10 +173,10 @@ class BaseHandler(object):
if result is True:
return
msg = "Pre-register conditions were not met"
msg = None
if isinstance(result, str):
msg = result
raise Exception(msg)
raise PreregisterException(msg)
def preregister(self):
'''
@ -593,3 +603,24 @@ class BaseHandler(object):
self.log.debug(
"Action \"{}\" Triggered successfully".format(action_name)
)
def trigger_event(
self, topic, event_data={}, session=None, source=None,
event=None, on_error="ignore"
):
if session is None:
session = self.session
if not source and event:
source = event.get("source")
# Create and trigger event
event = fa_session.ftrack_api.event.base.Event(
topic=topic,
data=event_data,
source=source
)
session.event_hub.publish(event, on_error=on_error)
self.log.debug((
"Publishing event: {}"
).format(str(event.__dict__)))

View file

@ -26,6 +26,7 @@ class BaseEvent(BaseHandler):
try:
func(*args, **kwargs)
except Exception as exc:
self.session.rollback()
self.log.error(
'Event "{}" Failed: {}'.format(
self.__class__.__name__, str(exc)

View file

@ -4,9 +4,9 @@ import threading
import time
from Qt import QtCore, QtGui, QtWidgets
from pype.vendor import ftrack_api
import ftrack_api
from pypeapp import style
from pype.ftrack import FtrackServer, credentials
from pype.ftrack import FtrackServer, check_ftrack_url, credentials
from . import login_dialog
from pype import api as pype
@ -24,7 +24,8 @@ class FtrackModule:
self.thread_timer = None
self.bool_logged = False
self.bool_action_server = False
self.bool_action_server_running = False
self.bool_action_thread_running = False
self.bool_timer_event = False
def show_login_widget(self):
@ -74,28 +75,50 @@ class FtrackModule:
# Actions part
def start_action_server(self):
self.bool_action_thread_running = True
self.set_menu_visibility()
if (
self.thread_action_server is not None and
self.bool_action_thread_running is False
):
self.stop_action_server()
if self.thread_action_server is None:
self.thread_action_server = threading.Thread(
target=self.set_action_server
)
self.thread_action_server.daemon = True
self.thread_action_server.start()
log.info("Ftrack action server launched")
self.bool_action_server = True
self.set_menu_visibility()
def set_action_server(self):
try:
self.action_server.run_server()
except Exception as exc:
log.error(
"Ftrack Action server crashed! Please try to start again.",
exc_info=True
first_check = True
while self.bool_action_thread_running is True:
if not check_ftrack_url(os.environ['FTRACK_SERVER']):
if first_check:
log.warning(
"Could not connect to Ftrack server"
)
first_check = False
time.sleep(1)
continue
log.info(
"Connected to Ftrack server. Running actions session"
)
# TODO show message to user
self.bool_action_server = False
try:
self.bool_action_server_running = True
self.set_menu_visibility()
self.action_server.run_server()
if self.bool_action_thread_running:
log.debug("Ftrack action server has stopped")
except Exception:
log.warning(
"Ftrack Action server crashed. Trying to connect again",
exc_info=True
)
self.bool_action_server_running = False
self.set_menu_visibility()
first_check = True
self.bool_action_thread_running = False
def reset_action_server(self):
self.stop_action_server()
@ -103,16 +126,21 @@ class FtrackModule:
def stop_action_server(self):
try:
self.bool_action_thread_running = False
self.action_server.stop_session()
if self.thread_action_server is not None:
self.thread_action_server.join()
self.thread_action_server = None
log.info("Ftrack action server stopped")
self.bool_action_server = False
log.info("Ftrack action server was forced to stop")
self.bool_action_server_running = False
self.set_menu_visibility()
except Exception as e:
log.error("During Killing action server: {0}".format(e))
except Exception:
log.warning(
"Error has happened during Killing action server",
exc_info=True
)
# Definition of Tray menu
def tray_menu(self, parent_menu):
@ -158,6 +186,9 @@ class FtrackModule:
def tray_start(self):
self.validate()
def tray_exit(self):
self.stop_action_server()
# Definition of visibility of each menu actions
def set_menu_visibility(self):
@ -170,9 +201,9 @@ class FtrackModule:
self.stop_timer_thread()
return
self.aRunActionS.setVisible(not self.bool_action_server)
self.aResetActionS.setVisible(self.bool_action_server)
self.aStopActionS.setVisible(self.bool_action_server)
self.aRunActionS.setVisible(not self.bool_action_thread_running)
self.aResetActionS.setVisible(self.bool_action_thread_running)
self.aStopActionS.setVisible(self.bool_action_thread_running)
if self.bool_timer_event is False:
self.start_timer_thread()