Merge pull request #2418 from pypeclub/feature/OP-1172_Start-timer-post-launch-hook

TimersManager: Start timer post launch hook
This commit is contained in:
Jakub Trllo 2021-12-20 14:12:11 +01:00 committed by GitHub
commit 9c39f8fff0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 214 additions and 69 deletions

View file

@ -640,6 +640,10 @@ class LaunchHook:
def app_name(self):
return getattr(self.application, "full_name", None)
@property
def modules_manager(self):
return getattr(self.launch_context, "modules_manager", None)
def validate(self):
"""Optional validation of launch hook on initialization.
@ -702,9 +706,13 @@ class ApplicationLaunchContext:
"""
def __init__(self, application, executable, **data):
from openpype.modules import ModulesManager
# Application object
self.application = application
self.modules_manager = ModulesManager()
# Logger
logger_name = "{}-{}".format(self.__class__.__name__, self.app_name)
self.log = PypeLogger.get_logger(logger_name)
@ -812,10 +820,7 @@ class ApplicationLaunchContext:
paths.append(path)
# Load modules paths
from openpype.modules import ModulesManager
manager = ModulesManager()
paths.extend(manager.collect_launch_hook_paths())
paths.extend(self.modules_manager.collect_launch_hook_paths())
return paths

View file

@ -1433,7 +1433,11 @@ def get_creator_by_name(creator_name, case_sensitive=False):
@with_avalon
def change_timer_to_current_context():
"""Called after context change to change timers"""
"""Called after context change to change timers.
TODO:
- use TimersManager's static method instead of reimplementing it here
"""
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
if not webserver_url:
log.warning("Couldn't find webserver url")
@ -1448,8 +1452,7 @@ def change_timer_to_current_context():
data = {
"project_name": avalon.io.Session["AVALON_PROJECT"],
"asset_name": avalon.io.Session["AVALON_ASSET"],
"task_name": avalon.io.Session["AVALON_TASK"],
"hierarchy": get_hierarchy()
"task_name": avalon.io.Session["AVALON_TASK"]
}
requests.post(rest_api_url, json=data)

View file

@ -52,7 +52,7 @@ class PostFtrackHook(PostLaunchHook):
)
if entity:
self.ftrack_status_change(session, entity, project_name)
self.start_timer(session, entity, ftrack_api)
except Exception:
self.log.warning(
"Couldn't finish Ftrack procedure.", exc_info=True
@ -160,26 +160,3 @@ class PostFtrackHook(PostLaunchHook):
" on Ftrack entity type \"{}\""
).format(next_status_name, entity.entity_type)
self.log.warning(msg)
def start_timer(self, session, entity, _ftrack_api):
"""Start Ftrack timer on task from context."""
self.log.debug("Triggering timer start.")
user_entity = session.query("User where username is \"{}\"".format(
os.environ["FTRACK_API_USER"]
)).first()
if not user_entity:
self.log.warning(
"Couldn't find user with username \"{}\" in Ftrack".format(
os.environ["FTRACK_API_USER"]
)
)
return
try:
user_entity.start_timer(entity, force=True)
session.commit()
self.log.debug("Timer start triggered successfully.")
except Exception:
self.log.warning("Couldn't trigger Ftrack timer.", exc_info=True)

View file

@ -0,0 +1,3 @@
class InvalidContextError(ValueError):
"""Context for which the timer should be started is invalid."""
pass

View file

@ -0,0 +1,45 @@
from openpype.lib import PostLaunchHook
class PostStartTimerHook(PostLaunchHook):
"""Start timer with TimersManager module.
This module requires enabled TimerManager module.
"""
order = None
def execute(self):
project_name = self.data.get("project_name")
asset_name = self.data.get("asset_name")
task_name = self.data.get("task_name")
missing_context_keys = set()
if not project_name:
missing_context_keys.add("project_name")
if not asset_name:
missing_context_keys.add("asset_name")
if not task_name:
missing_context_keys.add("task_name")
if missing_context_keys:
missing_keys_str = ", ".join([
"\"{}\"".format(key) for key in missing_context_keys
])
self.log.debug("Hook {} skipped. Missing data keys: {}".format(
self.__class__.__name__, missing_keys_str
))
return
timers_manager = self.modules_manager.modules_by_name.get(
"timers_manager"
)
if not timers_manager or not timers_manager.enabled:
self.log.info((
"Skipping starting timer because"
" TimersManager is not available."
))
return
timers_manager.start_timer_with_webserver(
project_name, asset_name, task_name, logger=self.log
)

View file

@ -39,17 +39,23 @@ class TimersManagerModuleRestApi:
async def start_timer(self, request):
data = await request.json()
try:
project_name = data['project_name']
asset_name = data['asset_name']
task_name = data['task_name']
hierarchy = data['hierarchy']
project_name = data["project_name"]
asset_name = data["asset_name"]
task_name = data["task_name"]
except KeyError:
log.error("Payload must contain fields 'project_name, " +
"'asset_name', 'task_name', 'hierarchy'")
return Response(status=400)
msg = (
"Payload must contain fields 'project_name,"
" 'asset_name' and 'task_name'"
)
log.error(msg)
return Response(status=400, message=msg)
self.module.stop_timers()
self.module.start_timer(project_name, asset_name, task_name, hierarchy)
try:
self.module.start_timer(project_name, asset_name, task_name)
except Exception as exc:
return Response(status=404, message=str(exc))
return Response(status=200)
async def stop_timer(self, request):

View file

@ -1,9 +1,15 @@
import os
import platform
from openpype.modules import OpenPypeModule
from openpype_interfaces import ITrayService
from avalon.api import AvalonMongoDB
from openpype.modules import OpenPypeModule
from openpype_interfaces import (
ITrayService,
ILaunchHookPaths
)
from .exceptions import InvalidContextError
class ExampleTimersManagerConnector:
"""Timers manager can handle timers of multiple modules/addons.
@ -64,7 +70,7 @@ class ExampleTimersManagerConnector:
self._timers_manager_module.timer_stopped(self._module.id)
class TimersManager(OpenPypeModule, ITrayService):
class TimersManager(OpenPypeModule, ITrayService, ILaunchHookPaths):
""" Handles about Timers.
Should be able to start/stop all timers at once.
@ -151,47 +157,112 @@ class TimersManager(OpenPypeModule, ITrayService):
self._idle_manager.stop()
self._idle_manager.wait()
def start_timer(self, project_name, asset_name, task_name, hierarchy):
"""
Start timer for 'project_name', 'asset_name' and 'task_name'
def get_timer_data_for_path(self, task_path):
"""Convert string path to a timer data.
Called from REST api by hosts.
Args:
project_name (string)
asset_name (string)
task_name (string)
hierarchy (string)
It is expected that first item is project name, last item is task name
and parent asset name is before task name.
"""
path_items = task_path.split("/")
if len(path_items) < 3:
raise InvalidContextError("Invalid path \"{}\"".format(task_path))
task_name = path_items.pop(-1)
asset_name = path_items.pop(-1)
project_name = path_items.pop(0)
return self.get_timer_data_for_context(
project_name, asset_name, task_name, self.log
)
def get_launch_hook_paths(self):
"""Implementation of `ILaunchHookPaths`."""
return os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"launch_hooks"
)
@staticmethod
def get_timer_data_for_context(
project_name, asset_name, task_name, logger=None
):
"""Prepare data for timer related callbacks.
TODO:
- return predefined object that has access to asset document etc.
"""
if not project_name or not asset_name or not task_name:
raise InvalidContextError((
"Missing context information got"
" Project: \"{}\" Asset: \"{}\" Task: \"{}\""
).format(str(project_name), str(asset_name), str(task_name)))
dbconn = AvalonMongoDB()
dbconn.install()
dbconn.Session["AVALON_PROJECT"] = project_name
asset_doc = dbconn.find_one({
"type": "asset", "name": asset_name
})
asset_doc = dbconn.find_one(
{
"type": "asset",
"name": asset_name
},
{
"data.tasks": True,
"data.parents": True
}
)
if not asset_doc:
raise ValueError("Uknown asset {}".format(asset_name))
dbconn.uninstall()
raise InvalidContextError((
"Asset \"{}\" not found in project \"{}\""
).format(asset_name, project_name))
task_type = ''
asset_data = asset_doc.get("data") or {}
asset_tasks = asset_data.get("tasks") or {}
if task_name not in asset_tasks:
dbconn.uninstall()
raise InvalidContextError((
"Task \"{}\" not found on asset \"{}\" in project \"{}\""
).format(task_name, asset_name, project_name))
task_type = ""
try:
task_type = asset_doc["data"]["tasks"][task_name]["type"]
task_type = asset_tasks[task_name]["type"]
except KeyError:
self.log.warning("Couldn't find task_type for {}".
format(task_name))
msg = "Couldn't find task_type for {}".format(task_name)
if logger is not None:
logger.warning(msg)
else:
print(msg)
hierarchy = hierarchy.split("\\")
hierarchy.append(asset_name)
hierarchy_items = asset_data.get("parents") or []
hierarchy_items.append(asset_name)
data = {
dbconn.uninstall()
return {
"project_name": project_name,
"task_name": task_name,
"task_type": task_type,
"hierarchy": hierarchy
"hierarchy": hierarchy_items
}
def start_timer(self, project_name, asset_name, task_name):
"""Start timer for passed context.
Args:
project_name (str): Project name
asset_name (str): Asset name
task_name (str): Task name
"""
data = self.get_timer_data_for_context(
project_name, asset_name, task_name, self.log
)
self.timer_started(None, data)
def get_task_time(self, project_name, asset_name, task_name):
"""Get total time for passed context.
TODO:
- convert context to timer data
"""
times = {}
for module_id, connector in self._connectors_by_module_id.items():
if hasattr(connector, "get_task_time"):
@ -202,6 +273,10 @@ class TimersManager(OpenPypeModule, ITrayService):
return times
def timer_started(self, source_id, data):
"""Connector triggered that timer has started.
New timer has started for context in data.
"""
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
@ -219,6 +294,14 @@ class TimersManager(OpenPypeModule, ITrayService):
self.is_running = True
def timer_stopped(self, source_id):
"""Connector triggered that hist timer has stopped.
Should stop all other timers.
TODO:
- pass context for which timer has stopped to validate if timers are
same and valid
"""
for module_id, connector in self._connectors_by_module_id.items():
if module_id == source_id:
continue
@ -237,6 +320,7 @@ class TimersManager(OpenPypeModule, ITrayService):
self.timer_started(None, self.last_task)
def stop_timers(self):
"""Stop all timers."""
if self.is_running is False:
return
@ -295,18 +379,40 @@ class TimersManager(OpenPypeModule, ITrayService):
self, server_manager
)
def change_timer_from_host(self, project_name, asset_name, task_name):
"""Prepared method for calling change timers on REST api"""
@staticmethod
def start_timer_with_webserver(
project_name, asset_name, task_name, logger=None
):
"""Prepared method for calling change timers on REST api.
Webserver must be active. At the moment is Webserver running only when
OpenPype Tray is used.
Args:
project_name (str): Project name.
asset_name (str): Asset name.
task_name (str): Task name.
logger (logging.Logger): Logger object. Using 'print' if not
passed.
"""
webserver_url = os.environ.get("OPENPYPE_WEBSERVER_URL")
if not webserver_url:
self.log.warning("Couldn't find webserver url")
msg = "Couldn't find webserver url"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
rest_api_url = "{}/timers_manager/start_timer".format(webserver_url)
try:
import requests
except Exception:
self.log.warning("Couldn't start timer")
msg = "Couldn't start timer ('requests' is not available)"
if logger is not None:
logger.warning(msg)
else:
print(msg)
return
data = {
"project_name": project_name,
@ -314,4 +420,4 @@ class TimersManager(OpenPypeModule, ITrayService):
"task_name": task_name
}
requests.post(rest_api_url, json=data)
return requests.post(rest_api_url, json=data)