Skip to content

ftrack_base_handler

BaseHandler

Base class for handling ftrack events.

Attributes:

Name Type Description
enabled bool

Is handler enabled.

priority int

Priority of handler processing. The lower value is the earlier is handler processed.

handler_type str

Has only debugging purposes.

Parameters:

Name Type Description Default
session Session

Connected ftrack session.

required
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
class BaseHandler(metaclass=ABCMeta):
    """Base class for handling ftrack events.

    Attributes:
        enabled (bool): Is handler enabled.
        priority (int): Priority of handler processing. The lower value is the
            earlier is handler processed.
        handler_type (str): Has only debugging purposes.

    Args:
        session (ftrack_api.Session): Connected ftrack session.

    """
    _log: Optional[logging.Logger] = None
    _process_id: Optional[str] = None
    # Default priority is 100
    enabled: bool = True
    priority: int = 100
    handler_type: str = "Base"
    _handler_label: Optional[str] = None
    # Mark base classes to be ignored for discovery
    __ignore_handler_class: bool = True

    def __init__(self, session):
        if not isinstance(session, ftrack_api.session.Session):
            raise TypeError(
                "Expected 'ftrack_api.Session' object got '{}'".format(
                    str(type(session))))

        self._session = session

        self.register = self.register_wrapper(self.register)

    @classmethod
    def ignore_handler_class(cls) -> bool:
        """Check if handler class should be ignored.

        Do not touch implementation of this method, set
            '__ignore_handler_class' to 'True' if you want to ignore class.

        """
        cls_name = cls.__name__
        if not cls_name.startswith("_"):
            cls_name = f"_{cls_name}"
        return getattr(cls, f"{cls_name}__ignore_handler_class", False)

    @staticmethod
    def join_filter_values(values: Iterable[str]) -> str:
        return ",".join({'"{}"'.format(value) for value in values})

    @classmethod
    def join_query_keys(cls, keys: Iterable[str]) -> str:
        return cls.join_filter_values(keys)

    @property
    def log(self) -> logging.Logger:
        """Quick access to logger.

        Returns:
            logging.Logger: Logger that can be used for logging of handler.

        """
        if self._log is None:
            # TODO better logging mechanism
            self._log = logging.getLogger(self.__class__.__name__)
            self._log.setLevel(logging.DEBUG)
        return self._log

    @property
    def handler_label(self) -> str:
        if self._handler_label is None:
            self._handler_label = self.__class__.__name__
        return self._handler_label

    @property
    def session(self) -> ftrack_api.Session:
        """Fast access to session.

        Returns:
            session (ftrack_api.Session): Session which is source of events.

        """
        return self._session

    def reset_session(self):
        """Reset session cache."""
        self.session.reset()

    @staticmethod
    def process_identifier() -> str:
        """Helper property to have unified access to process id.

        Todos:
            Use some global approach rather then implementation on
                'BaseEntity'.

        """
        if not BaseHandler._process_id:
            BaseHandler._process_id = str(uuid.uuid4())
        return BaseHandler._process_id

    @abstractmethod
    def register(self):
        """Subscribe to event topics."""
        pass

    def cleanup(self):
        """Cleanup handler.

        This method should end threads, timers, close connections, etc.
        """
        pass

    def register_wrapper(self, func):
        @functools.wraps(func)
        def wrapper_register(*args, **kwargs):
            if not self.enabled:
                return

            try:
                start_time = time.perf_counter()
                func(*args, **kwargs)
                end_time = time.perf_counter()
                run_time = end_time - start_time
                self.log.info((
                    "{} \"{}\" - Registered successfully ({:.4f}sec)"
                ).format(self.handler_type, self.handler_label, run_time))

            except NotImplementedError:
                self.log.error((
                    "{} \"{}\" - Register method is not implemented"
                ).format(self.handler_type, self.handler_label))

            except Exception as exc:
                self.log.error("{} \"{}\" - Registration failed ({})".format(
                    self.handler_type, self.handler_label, str(exc)
                ))
        return wrapper_register

    def _get_entities(self, event, session=None, ignore=None):
        entities = []
        selection = event["data"].get("selection")
        if not selection:
            return entities

        if ignore is None:
            ignore = []
        elif isinstance(ignore, str):
            ignore = [ignore]

        filtered_selection = []
        for entity in selection:
            if entity["entityType"] not in ignore:
                filtered_selection.append(entity)

        if not filtered_selection:
            return entities

        if session is None:
            session = self.session
            session._local_cache.clear()

        for entity in filtered_selection:
            entities.append(session.get(
                self._get_entity_type(entity, session),
                entity.get("entityId")
            ))

        return entities

    def _get_entity_type(self, entity, session=None):
        """Translate entity type so it can be used with API.

        Todos:
            Use object id rather.

        """
        # Get entity type and make sure it is lower cased. Most places except
        # the component tab in the Sidebar will use lower case notation.
        entity_type = entity.get("entityType").replace("_", "").lower()

        if session is None:
            session = self.session

        for schema in session.schemas:
            alias_for = schema.get("alias_for")

            if (
                alias_for
                and isinstance(alias_for, str)
                and alias_for.lower() == entity_type
            ):
                return schema["id"]

        for schema in self.session.schemas:
            if schema["id"].lower() == entity_type:
                return schema["id"]

        raise ValueError(
            "Unable to translate entity type: {0}.".format(entity_type)
        )

    def show_message(
        self,
        event: ftrack_api.event.base.Event,
        message: str,
        success: Optional[bool]=False,
    ):
        """Shows message to user who triggered event.

        Args:
            event (ftrack_api.event.base.Event): Event used for source
                of user id.
            message (str): Message that will be shown to user.
            success (bool): Define type (color) of message. False -> red color.

        """
        if not isinstance(success, bool):
            success = False

        try:
            message = str(message)
        except Exception:
            return

        user_id = event["source"]["user"]["id"]
        target = (
            "applicationId=ftrack.client.web and user.id=\"{}\""
        ).format(user_id)
        self.session.event_hub.publish(
            ftrack_api.event.base.Event(
                topic="ftrack.action.trigger-user-interface",
                data={
                    "type": "message",
                    "success": success,
                    "message": message
                },
                target=target
            ),
            on_error="ignore"
        )

    def show_interface(
        self,
        items: List[Dict[str, Any]],
        title: Optional[str] = "",
        user_id: Optional[str] = None,
        user: Optional[Any] = None,
        event: Optional[ftrack_api.event.base.Event] = None,
        username: Optional[str] = None,
        submit_btn_label: Optional[str] = None,
    ):
        """Shows ftrack widgets interface to user.

        Interface is shown to a user. To identify user one of arguments must be
        passed: 'user_id', 'user', 'event', 'username'.

        Args:
            items (List[Dict[str, Any]]) Interface items (their structure is
                defined by ftrack documentation).
            title (str): Title of shown widget.
            user_id (str): User id.
            user (Any): Object of ftrack user (queried using ftrack api
                session).
            event (ftrack_api.Event): Event which can be used as source for
                user id.
            username (str): Username of user to get it's id. This is slowest
                way how user id is received.
            submit_btn_label (str): Label of submit button in ftrack widget.

        """
        if user_id:
            pass

        elif user:
            user_id = user["id"]

        elif username:
            user = self.session.query(
                "User where username is \"{}\"".format(username)
            ).first()
            if not user:
                raise ValueError((
                    "ftrack user with username \"{}\" was not found!"
                ).format(username))

            user_id = user["id"]

        elif event:
            user_id = event["source"]["user"]["id"]

        if not user_id:
            return

        target = (
            "applicationId=ftrack.client.web and user.id=\"{}\""
        ).format(user_id)

        event_data = {
            "type": "widget",
            "items": items,
            "title": title
        }
        if submit_btn_label:
            event_data["submit_button_label"] = submit_btn_label

        self.session.event_hub.publish(
            ftrack_api.event.base.Event(
                topic="ftrack.action.trigger-user-interface",
                data=event_data,
                target=target
            ),
            on_error="ignore"
        )

    def show_interface_from_dict(
        self,
        messages: Dict[str, Union[str, List[str]]],
        title: Optional[str] = "",
        user_id: Optional[str] = None,
        user: Optional[Any] = None,
        event: Optional[ftrack_api.event.base.Event] = None,
        username: Optional[str] = None,
        submit_btn_label: Optional[str] = None,
    ):
        # TODO Find out how and where is this used
        if not messages:
            self.log.debug("No messages to show! (messages dict is empty)")
            return
        items = []
        splitter = {"type": "label", "value": "---"}
        first = True
        for key, value in messages.items():
            if not first:
                items.append(splitter)
            first = False

            items.append({"type": "label", "value": "<h3>{}</h3>".format(key)})
            if isinstance(value, str):
                value = [value]

            for item in value:
                items.append({"type": "label", "value": f"<p>{item}</p>"})

        self.show_interface(
            items,
            title=title,
            user_id=user_id,
            user=user,
            event=event,
            username=username,
            submit_btn_label=submit_btn_label
        )

    def trigger_action(
        self,
        action_identifier: str,
        event: Optional[ftrack_api.event.base.Event] = None,
        session: Optional[ftrack_api.Session] = None,
        selection: Optional[List[Dict[str, str]]] = None,
        user_data: Optional[Dict[str, Any]] = None,
        topic: Optional[str] = "ftrack.action.launch",
        additional_event_data: Optional[Dict[str, Any]] = None,
        on_error: Optional[str] = "ignore"
    ):
        self.log.debug(
            "Triggering action \"{}\" Begins".format(action_identifier))

        if not session:
            session = self.session

        # Getting selection and user data
        if event:
            if selection is None:
                selection = event.get("data", {}).get("selection")
            if user_data is None:
                user_data = event.get("source", {}).get("user")

        # Without selection and user data skip triggering
        msg = "Can't trigger \"{}\" action without {}."
        if selection is None:
            self.log.error(msg.format(action_identifier, "selection"))
            return

        if user_data is None:
            self.log.error(msg.format(action_identifier, "user data"))
            return

        event_data = {
            "actionIdentifier": action_identifier,
            "selection": selection
        }

        # Add additional data
        if additional_event_data:
            event_data.update(additional_event_data)

        # Create and trigger event
        session.event_hub.publish(
            ftrack_api.event.base.Event(
                topic=topic,
                data=event_data,
                source={"user": user_data}
            ),
            on_error=on_error
        )
        self.log.debug(
            "Action \"{}\" triggered".format(action_identifier))

    def trigger_event(
        self,
        topic: str,
        event_data: Optional[Dict[str, Any]] = None,
        session: Optional[ftrack_api.Session] = None,
        source: Optional[Dict[str, Any]] = None,
        event: Optional[ftrack_api.event.base.Event] = None,
        on_error: Optional[str] = "ignore"
    ):
        if session is None:
            session = self.session

        if not source and event:
            source = event.get("source")

        if event_data is None:
            event_data = {}
        # Create and trigger event
        event = 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__)))

    def get_project_from_entity(
        self,
        entity: ftrack_api.entity.base.Entity,
        session: Optional[ftrack_api.Session] = None
    ):
        low_entity_type = entity.entity_type.lower()
        if low_entity_type == "project":
            return entity

        if "project" in entity:
            # reviewsession, task(Task, Shot, Sequence,...)
            return entity["project"]

        if low_entity_type == "filecomponent":
            entity = entity["version"]
            low_entity_type = entity.entity_type.lower()

        if low_entity_type == "assetversion":
            asset = entity["asset"]
            parent = None
            if asset:
                parent = asset["parent"]

            if parent:
                if parent.entity_type.lower() == "project":
                    return parent

                if "project" in parent:
                    return parent["project"]

        project_data = entity["link"][0]

        if session is None:
            session = self.session
        return session.query(
            "Project where id is {}".format(project_data["id"])
        ).one()

    def get_project_entity_from_event(
        self,
        session: ftrack_api.Session,
        event: ftrack_api.event.base.Event,
        project_id: str,
    ):
        """Load or query and fill project entity from/to event data.

        Project data are stored by ftrack id because in most cases it is
        easier to access project id than project name.

        Args:
            session (ftrack_api.Session): Current session.
            event (ftrack_api.Event): Processed event by session.
            project_id (str): ftrack project id.

        Returns:
            Union[str, None]: Project name based on entities or None if project
                cannot be defined.

        """
        if not project_id:
            raise ValueError(
                "Entered `project_id` is not valid. {} ({})".format(
                    str(project_id), str(type(project_id))
                )
            )

        project_id_mapping = event["data"].setdefault(
            "project_entity_by_id", {}
        )
        if project_id in project_id_mapping:
            return project_id_mapping[project_id]

        # Get project entity from task and store to event
        project_entity = session.query((
            "select full_name from Project where id is \"{}\""
        ).format(project_id)).first()
        project_id_mapping[project_id] = project_entity

        return project_entity

    def get_project_name_from_event(
        self,
        session: ftrack_api.Session,
        event: ftrack_api.event.base.Event,
        project_id: str,
    ):
        """Load or query and fill project entity from/to event data.

        Project data are stored by ftrack id because in most cases it is
        easier to access project id than project name.

        Args:
            session (ftrack_api.Session): Current session.
            event (ftrack_api.Event): Processed event by session.
            project_id (str): ftrack project id.

        Returns:
            Union[str, None]: Project name based on entities or None if project
                cannot be defined.

        """
        if not project_id:
            raise ValueError(
                "Entered `project_id` is not valid. {} ({})".format(
                    str(project_id), str(type(project_id))
                )
            )

        project_id_mapping = event["data"].setdefault("project_id_name", {})
        if project_id in project_id_mapping:
            return project_id_mapping[project_id]

        # Get project entity from task and store to event
        project_entity = self.get_project_entity_from_event(
            session, event, project_id
        )
        if project_entity:
            project_name = project_entity["full_name"]
        project_id_mapping[project_id] = project_name
        return project_name

    def get_ayon_project_from_event(
        self,
        event: ftrack_api.event.base.Event,
        project_name: str
    ):
        """Get AYON project from event.

        Args:
            event (ftrack_api.Event): Event which is source of project id.
            project_name (Union[str, None]): Project name.

        Returns:
            Union[dict[str, Any], None]: AYON project.

        """
        ayon_projects = event["data"].setdefault("ayon_projects", {})
        if project_name in ayon_projects:
            return ayon_projects[project_name]

        project = None
        if project_name:
            project = get_project(project_name)
        ayon_projects[project_name] = project
        return project

    def get_project_settings_from_event(
        self,
        event: ftrack_api.event.base.Event,
        project_name: str
    ):
        """Load or fill AYON's project settings from event data.

        Project data are stored by ftrack id because in most cases it is
        easier to access project id than project name.

        Args:
            event (ftrack_api.Event): Processed event by session.
            project_name (str): Project name.

        """
        project_settings_by_name = event["data"].setdefault(
            "project_settings", {}
        )
        if project_name in project_settings_by_name:
            return copy.deepcopy(project_settings_by_name[project_name])

        # NOTE there is no safe way how to get project settings if project
        #   does not exist on AYON server.
        # TODO Should we somehow find out if ftrack is enabled for the
        #   project?
        # TODO how to find out which bundle should be used?
        project = self.get_ayon_project_from_event(event, project_name)
        if not project:
            project_name = None
        project_settings = get_addons_settings(project_name=project_name)
        project_settings_by_name[project_name] = project_settings
        return copy.deepcopy(project_settings)

    @staticmethod
    def get_entity_path(entity: ftrack_api.entity.base.Entity) -> str:
        """Return full hierarchical path to entity."""
        return "/".join(
            [ent["name"] for ent in entity["link"]]
        )

    @classmethod
    def add_traceback_to_job(
        cls,
        job: ftrack_api.entity.job.Job,
        session: ftrack_api.Session,
        exc_info: Tuple,
        description: Optional[str] = None,
        component_name: Optional[str] = None,
        job_status: Optional[str] = None
    ):
        """Add traceback file to a job.

        Args:
            job (JobEntity): Entity of job where file should be able to
                download (Created or queried with passed session).
            session (Session): ftrack session which was used to query/create
                entered job.
            exc_info (tuple): Exception info (e.g. from `sys.exc_info()`).
            description (str): Change job description to describe what
                happened. Job description won't change if not passed.
            component_name (str): Name of component and default name of
                downloaded file. Class name and current date time are used if
                not specified.
            job_status (str): Status of job which will be set. By default is
                set to 'failed'.

        """
        if description:
            job_data = {
                "description": description
            }
            job["data"] = json.dumps(job_data)

        if not job_status:
            job_status = "failed"

        job["status"] = job_status

        # Create temp file where traceback will be stored
        with tempfile.NamedTemporaryFile(
            mode="w", prefix="ayon_ftrack_", suffix=".txt", delete=False
        ) as temp_obj:
            temp_filepath = temp_obj.name

        # Store traceback to file
        result = traceback.format_exception(*exc_info)
        with open(temp_filepath, "w") as temp_file:
            temp_file.write("".join(result))

        # Upload file with traceback to ftrack server and add it to job
        if not component_name:
            component_name = "{}_{}".format(
                cls.__name__,
                datetime.datetime.now().strftime("%y-%m-%d-%H%M")
            )
        cls.add_file_component_to_job(
            job, session, temp_filepath, component_name
        )
        # Delete temp file
        os.remove(temp_filepath)

    @staticmethod
    def add_file_component_to_job(
        job: ftrack_api.entity.job.Job,
        session: ftrack_api.Session,
        filepath: str,
        basename: Optional[str] = None
    ):
        """Add filepath as downloadable component to job.

        Args:
            job (JobEntity): Entity of job where file should be able to
                download (Created or queried with passed session).
            session (Session): ftrack session which was used to query/create
                entered job.
            filepath (str): Path to file which should be added to job.
            basename (str): Defines name of file which will be downloaded on
                user's side. Must be without extension otherwise extension will
                be duplicated in downloaded name. Basename from entered path
                used when not entered.

        """
        # Make sure session's locations are configured
        # - they can be deconfigured e.g. using `rollback` method
        session._configure_locations()

        # Query `ftrack.server` location where component will be stored
        location = session.query(
            "Location where name is \"ftrack.server\""
        ).one()

        # Use filename as basename if not entered (must be without extension)
        if basename is None:
            basename = os.path.splitext(
                os.path.basename(filepath)
            )[0]

        component = session.create_component(
            filepath,
            data={"name": basename},
            location=location
        )
        session.create(
            "JobComponent",
            {
                "component_id": component["id"],
                "job_id": job["id"]
            }
        )
        session.commit()

log property

Quick access to logger.

Returns:

Type Description
Logger

logging.Logger: Logger that can be used for logging of handler.

session property

Fast access to session.

Returns:

Name Type Description
session Session

Session which is source of events.

add_file_component_to_job(job, session, filepath, basename=None) staticmethod

Add filepath as downloadable component to job.

Parameters:

Name Type Description Default
job JobEntity

Entity of job where file should be able to download (Created or queried with passed session).

required
session Session

ftrack session which was used to query/create entered job.

required
filepath str

Path to file which should be added to job.

required
basename str

Defines name of file which will be downloaded on user's side. Must be without extension otherwise extension will be duplicated in downloaded name. Basename from entered path used when not entered.

None
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
@staticmethod
def add_file_component_to_job(
    job: ftrack_api.entity.job.Job,
    session: ftrack_api.Session,
    filepath: str,
    basename: Optional[str] = None
):
    """Add filepath as downloadable component to job.

    Args:
        job (JobEntity): Entity of job where file should be able to
            download (Created or queried with passed session).
        session (Session): ftrack session which was used to query/create
            entered job.
        filepath (str): Path to file which should be added to job.
        basename (str): Defines name of file which will be downloaded on
            user's side. Must be without extension otherwise extension will
            be duplicated in downloaded name. Basename from entered path
            used when not entered.

    """
    # Make sure session's locations are configured
    # - they can be deconfigured e.g. using `rollback` method
    session._configure_locations()

    # Query `ftrack.server` location where component will be stored
    location = session.query(
        "Location where name is \"ftrack.server\""
    ).one()

    # Use filename as basename if not entered (must be without extension)
    if basename is None:
        basename = os.path.splitext(
            os.path.basename(filepath)
        )[0]

    component = session.create_component(
        filepath,
        data={"name": basename},
        location=location
    )
    session.create(
        "JobComponent",
        {
            "component_id": component["id"],
            "job_id": job["id"]
        }
    )
    session.commit()

add_traceback_to_job(job, session, exc_info, description=None, component_name=None, job_status=None) classmethod

Add traceback file to a job.

Parameters:

Name Type Description Default
job JobEntity

Entity of job where file should be able to download (Created or queried with passed session).

required
session Session

ftrack session which was used to query/create entered job.

required
exc_info tuple

Exception info (e.g. from sys.exc_info()).

required
description str

Change job description to describe what happened. Job description won't change if not passed.

None
component_name str

Name of component and default name of downloaded file. Class name and current date time are used if not specified.

None
job_status str

Status of job which will be set. By default is set to 'failed'.

None
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
@classmethod
def add_traceback_to_job(
    cls,
    job: ftrack_api.entity.job.Job,
    session: ftrack_api.Session,
    exc_info: Tuple,
    description: Optional[str] = None,
    component_name: Optional[str] = None,
    job_status: Optional[str] = None
):
    """Add traceback file to a job.

    Args:
        job (JobEntity): Entity of job where file should be able to
            download (Created or queried with passed session).
        session (Session): ftrack session which was used to query/create
            entered job.
        exc_info (tuple): Exception info (e.g. from `sys.exc_info()`).
        description (str): Change job description to describe what
            happened. Job description won't change if not passed.
        component_name (str): Name of component and default name of
            downloaded file. Class name and current date time are used if
            not specified.
        job_status (str): Status of job which will be set. By default is
            set to 'failed'.

    """
    if description:
        job_data = {
            "description": description
        }
        job["data"] = json.dumps(job_data)

    if not job_status:
        job_status = "failed"

    job["status"] = job_status

    # Create temp file where traceback will be stored
    with tempfile.NamedTemporaryFile(
        mode="w", prefix="ayon_ftrack_", suffix=".txt", delete=False
    ) as temp_obj:
        temp_filepath = temp_obj.name

    # Store traceback to file
    result = traceback.format_exception(*exc_info)
    with open(temp_filepath, "w") as temp_file:
        temp_file.write("".join(result))

    # Upload file with traceback to ftrack server and add it to job
    if not component_name:
        component_name = "{}_{}".format(
            cls.__name__,
            datetime.datetime.now().strftime("%y-%m-%d-%H%M")
        )
    cls.add_file_component_to_job(
        job, session, temp_filepath, component_name
    )
    # Delete temp file
    os.remove(temp_filepath)

cleanup()

Cleanup handler.

This method should end threads, timers, close connections, etc.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
125
126
127
128
129
130
def cleanup(self):
    """Cleanup handler.

    This method should end threads, timers, close connections, etc.
    """
    pass

get_ayon_project_from_event(event, project_name)

Get AYON project from event.

Parameters:

Name Type Description Default
event Event

Event which is source of project id.

required
project_name Union[str, None]

Project name.

required

Returns:

Type Description

Union[dict[str, Any], None]: AYON project.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
def get_ayon_project_from_event(
    self,
    event: ftrack_api.event.base.Event,
    project_name: str
):
    """Get AYON project from event.

    Args:
        event (ftrack_api.Event): Event which is source of project id.
        project_name (Union[str, None]): Project name.

    Returns:
        Union[dict[str, Any], None]: AYON project.

    """
    ayon_projects = event["data"].setdefault("ayon_projects", {})
    if project_name in ayon_projects:
        return ayon_projects[project_name]

    project = None
    if project_name:
        project = get_project(project_name)
    ayon_projects[project_name] = project
    return project

get_entity_path(entity) staticmethod

Return full hierarchical path to entity.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
636
637
638
639
640
641
@staticmethod
def get_entity_path(entity: ftrack_api.entity.base.Entity) -> str:
    """Return full hierarchical path to entity."""
    return "/".join(
        [ent["name"] for ent in entity["link"]]
    )

get_project_entity_from_event(session, event, project_id)

Load or query and fill project entity from/to event data.

Project data are stored by ftrack id because in most cases it is easier to access project id than project name.

Parameters:

Name Type Description Default
session Session

Current session.

required
event Event

Processed event by session.

required
project_id str

ftrack project id.

required

Returns:

Type Description

Union[str, None]: Project name based on entities or None if project cannot be defined.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
def get_project_entity_from_event(
    self,
    session: ftrack_api.Session,
    event: ftrack_api.event.base.Event,
    project_id: str,
):
    """Load or query and fill project entity from/to event data.

    Project data are stored by ftrack id because in most cases it is
    easier to access project id than project name.

    Args:
        session (ftrack_api.Session): Current session.
        event (ftrack_api.Event): Processed event by session.
        project_id (str): ftrack project id.

    Returns:
        Union[str, None]: Project name based on entities or None if project
            cannot be defined.

    """
    if not project_id:
        raise ValueError(
            "Entered `project_id` is not valid. {} ({})".format(
                str(project_id), str(type(project_id))
            )
        )

    project_id_mapping = event["data"].setdefault(
        "project_entity_by_id", {}
    )
    if project_id in project_id_mapping:
        return project_id_mapping[project_id]

    # Get project entity from task and store to event
    project_entity = session.query((
        "select full_name from Project where id is \"{}\""
    ).format(project_id)).first()
    project_id_mapping[project_id] = project_entity

    return project_entity

get_project_name_from_event(session, event, project_id)

Load or query and fill project entity from/to event data.

Project data are stored by ftrack id because in most cases it is easier to access project id than project name.

Parameters:

Name Type Description Default
session Session

Current session.

required
event Event

Processed event by session.

required
project_id str

ftrack project id.

required

Returns:

Type Description

Union[str, None]: Project name based on entities or None if project cannot be defined.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def get_project_name_from_event(
    self,
    session: ftrack_api.Session,
    event: ftrack_api.event.base.Event,
    project_id: str,
):
    """Load or query and fill project entity from/to event data.

    Project data are stored by ftrack id because in most cases it is
    easier to access project id than project name.

    Args:
        session (ftrack_api.Session): Current session.
        event (ftrack_api.Event): Processed event by session.
        project_id (str): ftrack project id.

    Returns:
        Union[str, None]: Project name based on entities or None if project
            cannot be defined.

    """
    if not project_id:
        raise ValueError(
            "Entered `project_id` is not valid. {} ({})".format(
                str(project_id), str(type(project_id))
            )
        )

    project_id_mapping = event["data"].setdefault("project_id_name", {})
    if project_id in project_id_mapping:
        return project_id_mapping[project_id]

    # Get project entity from task and store to event
    project_entity = self.get_project_entity_from_event(
        session, event, project_id
    )
    if project_entity:
        project_name = project_entity["full_name"]
    project_id_mapping[project_id] = project_name
    return project_name

get_project_settings_from_event(event, project_name)

Load or fill AYON's project settings from event data.

Project data are stored by ftrack id because in most cases it is easier to access project id than project name.

Parameters:

Name Type Description Default
event Event

Processed event by session.

required
project_name str

Project name.

required
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
def get_project_settings_from_event(
    self,
    event: ftrack_api.event.base.Event,
    project_name: str
):
    """Load or fill AYON's project settings from event data.

    Project data are stored by ftrack id because in most cases it is
    easier to access project id than project name.

    Args:
        event (ftrack_api.Event): Processed event by session.
        project_name (str): Project name.

    """
    project_settings_by_name = event["data"].setdefault(
        "project_settings", {}
    )
    if project_name in project_settings_by_name:
        return copy.deepcopy(project_settings_by_name[project_name])

    # NOTE there is no safe way how to get project settings if project
    #   does not exist on AYON server.
    # TODO Should we somehow find out if ftrack is enabled for the
    #   project?
    # TODO how to find out which bundle should be used?
    project = self.get_ayon_project_from_event(event, project_name)
    if not project:
        project_name = None
    project_settings = get_addons_settings(project_name=project_name)
    project_settings_by_name[project_name] = project_settings
    return copy.deepcopy(project_settings)

ignore_handler_class() classmethod

Check if handler class should be ignored.

Do not touch implementation of this method, set '__ignore_handler_class' to 'True' if you want to ignore class.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
52
53
54
55
56
57
58
59
60
61
62
63
@classmethod
def ignore_handler_class(cls) -> bool:
    """Check if handler class should be ignored.

    Do not touch implementation of this method, set
        '__ignore_handler_class' to 'True' if you want to ignore class.

    """
    cls_name = cls.__name__
    if not cls_name.startswith("_"):
        cls_name = f"_{cls_name}"
    return getattr(cls, f"{cls_name}__ignore_handler_class", False)

process_identifier() staticmethod

Helper property to have unified access to process id.

Todos

Use some global approach rather then implementation on 'BaseEntity'.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
107
108
109
110
111
112
113
114
115
116
117
118
@staticmethod
def process_identifier() -> str:
    """Helper property to have unified access to process id.

    Todos:
        Use some global approach rather then implementation on
            'BaseEntity'.

    """
    if not BaseHandler._process_id:
        BaseHandler._process_id = str(uuid.uuid4())
    return BaseHandler._process_id

register() abstractmethod

Subscribe to event topics.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
120
121
122
123
@abstractmethod
def register(self):
    """Subscribe to event topics."""
    pass

reset_session()

Reset session cache.

Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
103
104
105
def reset_session(self):
    """Reset session cache."""
    self.session.reset()

show_interface(items, title='', user_id=None, user=None, event=None, username=None, submit_btn_label=None)

Shows ftrack widgets interface to user.

Interface is shown to a user. To identify user one of arguments must be passed: 'user_id', 'user', 'event', 'username'.

Parameters:

Name Type Description Default
title str

Title of shown widget.

''
user_id str

User id.

None
user Any

Object of ftrack user (queried using ftrack api session).

None
event Event

Event which can be used as source for user id.

None
username str

Username of user to get it's id. This is slowest way how user id is received.

None
submit_btn_label str

Label of submit button in ftrack widget.

None
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def show_interface(
    self,
    items: List[Dict[str, Any]],
    title: Optional[str] = "",
    user_id: Optional[str] = None,
    user: Optional[Any] = None,
    event: Optional[ftrack_api.event.base.Event] = None,
    username: Optional[str] = None,
    submit_btn_label: Optional[str] = None,
):
    """Shows ftrack widgets interface to user.

    Interface is shown to a user. To identify user one of arguments must be
    passed: 'user_id', 'user', 'event', 'username'.

    Args:
        items (List[Dict[str, Any]]) Interface items (their structure is
            defined by ftrack documentation).
        title (str): Title of shown widget.
        user_id (str): User id.
        user (Any): Object of ftrack user (queried using ftrack api
            session).
        event (ftrack_api.Event): Event which can be used as source for
            user id.
        username (str): Username of user to get it's id. This is slowest
            way how user id is received.
        submit_btn_label (str): Label of submit button in ftrack widget.

    """
    if user_id:
        pass

    elif user:
        user_id = user["id"]

    elif username:
        user = self.session.query(
            "User where username is \"{}\"".format(username)
        ).first()
        if not user:
            raise ValueError((
                "ftrack user with username \"{}\" was not found!"
            ).format(username))

        user_id = user["id"]

    elif event:
        user_id = event["source"]["user"]["id"]

    if not user_id:
        return

    target = (
        "applicationId=ftrack.client.web and user.id=\"{}\""
    ).format(user_id)

    event_data = {
        "type": "widget",
        "items": items,
        "title": title
    }
    if submit_btn_label:
        event_data["submit_button_label"] = submit_btn_label

    self.session.event_hub.publish(
        ftrack_api.event.base.Event(
            topic="ftrack.action.trigger-user-interface",
            data=event_data,
            target=target
        ),
        on_error="ignore"
    )

show_message(event, message, success=False)

Shows message to user who triggered event.

Parameters:

Name Type Description Default
event Event

Event used for source of user id.

required
message str

Message that will be shown to user.

required
success bool

Define type (color) of message. False -> red color.

False
Source code in client/ayon_ftrack/common/event_handlers/ftrack_base_handler.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def show_message(
    self,
    event: ftrack_api.event.base.Event,
    message: str,
    success: Optional[bool]=False,
):
    """Shows message to user who triggered event.

    Args:
        event (ftrack_api.event.base.Event): Event used for source
            of user id.
        message (str): Message that will be shown to user.
        success (bool): Define type (color) of message. False -> red color.

    """
    if not isinstance(success, bool):
        success = False

    try:
        message = str(message)
    except Exception:
        return

    user_id = event["source"]["user"]["id"]
    target = (
        "applicationId=ftrack.client.web and user.id=\"{}\""
    ).format(user_id)
    self.session.event_hub.publish(
        ftrack_api.event.base.Event(
            topic="ftrack.action.trigger-user-interface",
            data={
                "type": "message",
                "success": success,
                "message": message
            },
            target=target
        ),
        on_error="ignore"
    )