Skip to content

ayon_applications

Application

Hold information about application.

Object by itself does nothing special.

Parameters:

Name Type Description Default
data dict

Data for the version containing information about executables, variant label or if is enabled. Only required key is executables.

required
group ApplicationGroup

App group object that created the application and under which application belongs.

required
Source code in client/ayon_applications/defs.py
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
class Application:
    """Hold information about application.

    Object by itself does nothing special.

    Args:
        data (dict): Data for the version containing information about
            executables, variant label or if is enabled.
            Only required key is `executables`.
        group (ApplicationGroup): App group object that created the application
            and under which application belongs.

    """
    def __init__(self, data: dict[str, Any], group: ApplicationGroup):
        self._data = data
        name = data["name"]
        label = data["label"] or name
        enabled = False
        if group.enabled:
            enabled = data.get("enabled", True)

        if group.label:
            full_label = " ".join((group.label, label))
        else:
            full_label = label
        env = {}
        try:
            env = json.loads(data["environment"])
        except Exception:
            pass

        arguments = data["arguments"]
        if isinstance(arguments, dict):
            arguments = arguments.get(platform.system().lower())

        if not arguments:
            arguments = []

        _executables = data["executables"].get(platform.system().lower(), [])
        executables = [
            ApplicationExecutable(executable)
            for executable in _executables
        ]

        self.group = group

        self.name = name
        self.label = label
        self.enabled = enabled
        self.use_python_2 = data.get("use_python_2", False)

        self.full_name = f"{group.name}/{name}"
        self.full_label = full_label
        self.arguments = arguments
        self.executables = executables
        self._environment = env
        self.redirect_output = data.get("redirect_output", True)

    def __repr__(self):
        return f"<{self.__class__.__name__}> - {self.full_name}"

    @property
    def environment(self) -> dict[str, str]:
        return copy.deepcopy(self._environment)

    @property
    def manager(self) -> "ApplicationManager":
        return self.group.manager

    @property
    def host_name(self) -> Optional[str]:
        return self.group.host_name

    @property
    def icon(self) -> dict[str, str] | None:
        return self.group.icon

    @property
    def is_host(self) -> bool:
        return self.group.is_host

    def find_executable(self) -> Optional[ApplicationExecutable]:
        """Try to find existing executable for application.

        Returns (str): Path to executable from `executables` or None if any
            exists.
        """
        for executable in self.executables:
            if executable.exists():
                return executable
        return None

    def launch(self, *args, **kwargs) -> Optional[subprocess.Popen]:
        """Launch the application.

        For this purpose is used manager's launch method to keep logic at one
        place.

        Arguments must match with manager's launch method. That's why *args
        **kwargs are used.

        Returns:
            subprocess.Popen: Return executed process as Popen object.

        """
        return self.manager.launch(self.full_name, *args, **kwargs)

find_executable()

Try to find existing executable for application.

Returns (str): Path to executable from executables or None if any exists.

Source code in client/ayon_applications/defs.py
295
296
297
298
299
300
301
302
303
304
def find_executable(self) -> Optional[ApplicationExecutable]:
    """Try to find existing executable for application.

    Returns (str): Path to executable from `executables` or None if any
        exists.
    """
    for executable in self.executables:
        if executable.exists():
            return executable
    return None

launch(*args, **kwargs)

Launch the application.

For this purpose is used manager's launch method to keep logic at one place.

Arguments must match with manager's launch method. That's why args *kwargs are used.

Returns:

Type Description
Optional[Popen]

subprocess.Popen: Return executed process as Popen object.

Source code in client/ayon_applications/defs.py
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def launch(self, *args, **kwargs) -> Optional[subprocess.Popen]:
    """Launch the application.

    For this purpose is used manager's launch method to keep logic at one
    place.

    Arguments must match with manager's launch method. That's why *args
    **kwargs are used.

    Returns:
        subprocess.Popen: Return executed process as Popen object.

    """
    return self.manager.launch(self.full_name, *args, **kwargs)

ApplicationExecutable

Representation of executable loaded from settings.

Source code in client/ayon_applications/defs.py
 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
class ApplicationExecutable:
    """Representation of executable loaded from settings."""

    def __init__(self, executable: str):
        # Try to format executable with environments
        try:
            executable = executable.format(**os.environ)
        except Exception:
            pass

        # On MacOS check if exists path to executable when ends with `.app`
        # - it is common that path will lead to "/Applications/Blender" but
        #   real path is "/Applications/Blender.app"
        if platform.system().lower() == "darwin":
            executable = self.macos_executable_prep(executable)

        self.executable_path = executable

    def __str__(self) -> str:
        return self.executable_path

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}> {self.executable_path}"

    @staticmethod
    def macos_executable_prep(executable: str) -> str:
        """Try to find full path to executable file.

        Real executable is stored in '*.app/Contents/MacOS/<executable>'.

        Having path to '*.app' gives ability to read it's plist info and
        use "CFBundleExecutable" key from plist to know what is "executable."

        Plist is stored in '*.app/Contents/Info.plist'.

        This is because some '*.app' directories don't have same permissions
        as real executable.
        """
        # Try to find if there is `.app` file
        if not os.path.exists(executable):
            _executable = executable + ".app"
            if os.path.exists(_executable):
                executable = _executable

        # Try to find real executable if executable has `Contents` subfolder
        contents_dir = os.path.join(executable, "Contents")
        if os.path.exists(contents_dir):
            executable_filename = None
            # Load plist file and check for bundle executable
            plist_filepath = os.path.join(contents_dir, "Info.plist")
            if os.path.exists(plist_filepath):
                import plistlib

                if hasattr(plistlib, "load"):
                    with open(plist_filepath, "rb") as stream:
                        parsed_plist = plistlib.load(stream)
                else:
                    parsed_plist = plistlib.readPlist(plist_filepath)
                executable_filename = parsed_plist.get("CFBundleExecutable")

            if executable_filename:
                executable = os.path.join(
                    contents_dir, "MacOS", executable_filename
                )

        return executable

    def as_args(self) -> list[str]:
        return [self.executable_path]

    def _realpath(self) -> Optional[str]:
        """Check if path is valid executable path."""
        # Check for executable in PATH
        result = find_executable(self.executable_path)
        if result is not None:
            return result

        # This is not 100% validation but it is better than remove ability to
        #   launch .bat, .sh or extentionless files
        if os.path.isfile(self.executable_path):
            return self.executable_path
        return None

    def exists(self) -> bool:
        if not self.executable_path:
            return False
        return bool(self._realpath())

macos_executable_prep(executable) staticmethod

Try to find full path to executable file.

Real executable is stored in '*.app/Contents/MacOS/'.

Having path to '*.app' gives ability to read it's plist info and use "CFBundleExecutable" key from plist to know what is "executable."

Plist is stored in '*.app/Contents/Info.plist'.

This is because some '*.app' directories don't have same permissions as real executable.

Source code in client/ayon_applications/defs.py
 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
@staticmethod
def macos_executable_prep(executable: str) -> str:
    """Try to find full path to executable file.

    Real executable is stored in '*.app/Contents/MacOS/<executable>'.

    Having path to '*.app' gives ability to read it's plist info and
    use "CFBundleExecutable" key from plist to know what is "executable."

    Plist is stored in '*.app/Contents/Info.plist'.

    This is because some '*.app' directories don't have same permissions
    as real executable.
    """
    # Try to find if there is `.app` file
    if not os.path.exists(executable):
        _executable = executable + ".app"
        if os.path.exists(_executable):
            executable = _executable

    # Try to find real executable if executable has `Contents` subfolder
    contents_dir = os.path.join(executable, "Contents")
    if os.path.exists(contents_dir):
        executable_filename = None
        # Load plist file and check for bundle executable
        plist_filepath = os.path.join(contents_dir, "Info.plist")
        if os.path.exists(plist_filepath):
            import plistlib

            if hasattr(plistlib, "load"):
                with open(plist_filepath, "rb") as stream:
                    parsed_plist = plistlib.load(stream)
            else:
                parsed_plist = plistlib.readPlist(plist_filepath)
            executable_filename = parsed_plist.get("CFBundleExecutable")

        if executable_filename:
            executable = os.path.join(
                contents_dir, "MacOS", executable_filename
            )

    return executable

ApplicationExecutableNotFound

Bases: Exception

Defined executable paths are not available on the machine.

Source code in client/ayon_applications/exceptions.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ApplicationExecutableNotFound(Exception):
    """Defined executable paths are not available on the machine."""

    def __init__(self, application):
        self.application = application
        details = None
        if not application.executables:
            msg = (
                "Executable paths for application \"{}\"({}) are not set."
            )
        else:
            msg = (
                "Defined executable paths for application \"{}\"({})"
                " are not valid or not available on this machine."
            )
            details = "Defined paths:"
            for executable in application.executables:
                details += "\n- " + executable.executable_path

        self.msg = msg.format(application.full_label, application.full_name)
        self.details = details

        exc_mgs = str(self.msg)
        if details:
            # Is good idea to pass new line symbol to exception message?
            exc_mgs += "\n\n" + details
        self.exc_msg = exc_mgs
        super().__init__(exc_mgs)

ApplicationGroup

Hold information about application group.

Application group wraps different versions(variants) of application. e.g. "maya" is group and "maya_2020" is variant.

Group hold host_name which is implementation name used in AYON. Also holds enabled if whole app group is enabled or icon for application icon path in resources.

Group has also environment which hold same environments for all variants.

Parameters:

Name Type Description Default
name str

Groups' name.

required
data dict

Group defying data loaded from settings.

required
manager ApplicationManager

Manager that created the group.

required
Source code in client/ayon_applications/defs.py
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
class ApplicationGroup:
    """Hold information about application group.

    Application group wraps different versions(variants) of application.
    e.g. "maya" is group and "maya_2020" is variant.

    Group hold `host_name` which is implementation name used in AYON. Also
    holds `enabled` if whole app group is enabled or `icon` for application
    icon path in resources.

    Group has also `environment` which hold same environments for all variants.

    Args:
        name (str): Groups' name.
        data (dict): Group defying data loaded from settings.
        manager (ApplicationManager): Manager that created the group.

    """
    def __init__(
        self,
        name: str,
        data: dict[str, Any],
        manager: "ApplicationManager",
    ):
        icon = manager.get_app_icon(name)

        label = data.get("label")
        if not label:
            label = manager.get_app_label(name)

        self.name = name
        self.manager = manager
        self._data = data

        self.enabled = data["enabled"]
        self.label = label
        self.icon: dict[str, str] | None = icon
        env = {}
        try:
            env = json.loads(data["environment"])
        except Exception:
            pass
        self._environment = env

        host_name = data["host_name"] or None
        self.is_host = host_name is not None
        self.host_name = host_name

        settings_variants = data["variants"]
        variants = {}
        for variant_data in settings_variants:
            app_variant = Application(variant_data, self)
            variants[app_variant.name] = app_variant

        self.variants = variants

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}> - {self.name}"

    def __iter__(self) -> Generator["Application", None, None]:
        for variant in self.variants.values():
            yield variant

    @property
    def environment(self) -> dict[str, str]:
        return copy.deepcopy(self._environment)

ApplicationLaunchContext

Context of launching application.

Main purpose of context is to prepare launch arguments and keyword arguments for new process. Most important part of keyword arguments preparations are environment variables.

During the whole process is possible to use data attribute to store object usable in multiple places.

Launch arguments are strings in list. It is possible to "chain" argument when order of them matters. That is possible to do with adding list where order is right and should not change. NOTE: This is recommendation, not requirement. e.g.: ["nuke.exe", "--NukeX"] -> In this case any part of process may insert argument between nuke.exe and --NukeX. To keep them together it is better to wrap them in another list: [["nuke.exe", "--NukeX"]].

Notes

It is possible to use launch context only to prepare environment variables. In that case executable may be None and can be used 'run_prelaunch_hooks' method to run prelaunch hooks which prepare them.

Parameters:

Name Type Description Default
application Application

Application definition.

required
executable ApplicationExecutable

Object with path to executable.

required
env_group Optional[str]

Environment variable group. If not set 'DEFAULT_ENV_SUBGROUP' is used.

None
launch_type Optional[str]

Launch type. If not set 'local' is used.

None
**data dict

Any additional data. Data may be used during preparation to store objects usable in multiple places.

{}
Source code in client/ayon_applications/manager.py
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
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
class ApplicationLaunchContext:
    """Context of launching application.

    Main purpose of context is to prepare launch arguments and keyword
    arguments for new process. Most important part of keyword arguments
    preparations are environment variables.

    During the whole process is possible to use `data` attribute to store
    object usable in multiple places.

    Launch arguments are strings in list. It is possible to "chain" argument
    when order of them matters. That is possible to do with adding list where
    order is right and should not change.
    NOTE: This is recommendation, not requirement.
    e.g.: `["nuke.exe", "--NukeX"]` -> In this case any part of process may
    insert argument between `nuke.exe` and `--NukeX`. To keep them together
    it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`.

    Notes:
        It is possible to use launch context only to prepare environment
            variables. In that case `executable` may be None and can be used
            'run_prelaunch_hooks' method to run prelaunch hooks which prepare
            them.

    Args:
        application (Application): Application definition.
        executable (ApplicationExecutable): Object with path to executable.
        env_group (Optional[str]): Environment variable group. If not set
            'DEFAULT_ENV_SUBGROUP' is used.
        launch_type (Optional[str]): Launch type. If not set 'local' is used.
        **data (dict): Any additional data. Data may be used during
            preparation to store objects usable in multiple places.
    """

    def __init__(
        self,
        application: Application,
        executable: ApplicationExecutable,
        env_group: Optional[str] = None,
        launch_type: Optional[str] = None,
        **data,
    ):
        from .process import ProcessManager

        # Application object
        self.application: Application = application

        self.addons_manager: AddonsManager = AddonsManager()
        self.process_manager: ProcessManager = ProcessManager()
        self.redirect_output: bool = application.redirect_output

        # Logger
        self.log: logging.Logger = Logger.get_logger(
            f"{self.__class__.__name__}-{application.full_name}"
        )

        self.executable: ApplicationExecutable = executable

        if launch_type is None:
            launch_type = LaunchTypes.local
        self.launch_type: str = launch_type

        if env_group is None:
            env_group = DEFAULT_ENV_SUBGROUP

        self.env_group: str = env_group

        self.data: dict[str, Any] = dict(data)
        project_name = self.data.get("project_name")
        if project_name and "project_settings" not in self.data:
            self.data["project_settings"] = get_project_settings(
                project_name
            )

        launch_args = []
        if executable is not None:
            launch_args = executable.as_args()
        # subprocess.Popen launch arguments (first argument in constructor)
        self.launch_args: list[str] = launch_args
        self.launch_args.extend(application.arguments)
        if self.data.get("app_args"):
            self.launch_args.extend(self.data.pop("app_args"))

        # Handle launch environemtns
        src_env = self.data.pop("env", None)
        if src_env is not None and not isinstance(src_env, dict):
            self.log.warning(
                f"Passed `env` kwarg has invalid type: {type(src_env)}."
                " Expected: `dict`. Using `os.environ` instead."
            )
            src_env = None

        if src_env is None:
            src_env = os.environ

        ignored_env = {"QT_API", "PYTHONHOME"}
        env = {
            key: str(value)
            for key, value in src_env.items()
            if key not in ignored_env
        }
        # subprocess.Popen keyword arguments
        self.kwargs: dict[str, Any] = {"env": env}

        if platform.system().lower() == "windows":
            # Detach new process from currently running process on Windows
            flags = (
                subprocess.CREATE_NEW_PROCESS_GROUP
                | subprocess.DETACHED_PROCESS
            )
            self.kwargs["creationflags"] = flags

        if not sys.stdout:
            self.kwargs["stdout"] = subprocess.DEVNULL
            self.kwargs["stderr"] = subprocess.DEVNULL

        # TODO: add type hints
        # note that these need to be None in order to trigger discovery
        # when 'discover_launch_hooks' is called
        self.prelaunch_hooks = None
        self.postlaunch_hooks = None

        self.process: Optional[Popen] = None
        self._prelaunch_hooks_executed = False

    @property
    def env(self) -> dict[str, str]:
        if (
            "env" not in self.kwargs
            or self.kwargs["env"] is None
        ):
            self.kwargs["env"] = {}
        return self.kwargs["env"]

    @env.setter
    def env(self, value: dict[str, str]) -> None:
        if not isinstance(value, dict):
            raise TypeError(
                f"'env' attribute expect 'dict' object. Got: {type(value)}"
            )
        self.kwargs["env"] = value

    @property
    def modules_manager(self) -> AddonsManager:
        """
        Deprecated:
            Use 'addons_manager' instead.

        """
        return self.addons_manager

    def _collect_addons_launch_hook_paths(self) -> list[str]:
        """Helper to collect application launch hooks from addons.

        Module have to have implemented 'get_launch_hook_paths' method which
        can expect application as argument or nothing.

        Returns:
            list[str]: Paths to launch hook directories.

        """
        expected_types = (list, tuple, set)

        output = []
        for module in self.addons_manager.get_enabled_addons():
            # Skip module if does not have implemented 'get_launch_hook_paths'
            func = getattr(module, "get_launch_hook_paths", None)
            if func is None:
                continue

            func = module.get_launch_hook_paths
            if hasattr(inspect, "signature"):
                sig = inspect.signature(func)
                expect_args = len(sig.parameters) > 0
            else:
                expect_args = len(inspect.getargspec(func)[0]) > 0

            # Pass application argument if method expect it.
            try:
                if expect_args:
                    hook_paths = func(self.application)
                else:
                    hook_paths = func()
            except Exception:
                self.log.warning(
                    "Failed to call 'get_launch_hook_paths'",
                    exc_info=True
                )
                continue

            if not hook_paths:
                continue

            # Convert string to list
            if isinstance(hook_paths, str):
                hook_paths = [hook_paths]

            # Skip invalid types
            if not isinstance(hook_paths, expected_types):
                self.log.warning(
                    "Result of `get_launch_hook_paths` has invalid"
                    f" type {type(hook_paths)}. Expected {expected_types}"
                )
                continue

            output.extend(hook_paths)
        return output

    def paths_to_launch_hooks(self) -> list[str]:
        """Directory paths where to look for launch hooks."""
        # This method has potential to be part of application manager (maybe).
        paths = []

        # TODO load additional studio paths from settings
        global_hooks_dir = os.path.join(AYON_CORE_ROOT, "hooks")

        hooks_dirs = [
            global_hooks_dir
        ]
        if self.host_name:
            # If host requires launch hooks and is module then launch hooks
            #   should be collected using 'collect_launch_hook_paths'
            #   - module have to implement 'get_launch_hook_paths'
            host_module = self.addons_manager.get_host_addon(self.host_name)
            if not host_module:
                hooks_dirs.append(os.path.join(
                    AYON_CORE_ROOT, "hosts", self.host_name, "hooks"
                ))

        for path in hooks_dirs:
            if (
                os.path.exists(path)
                and os.path.isdir(path)
                and path not in paths
            ):
                paths.append(path)

        # Load modules paths
        paths.extend(self._collect_addons_launch_hook_paths())

        return paths

    def discover_launch_hooks(self, force: bool = False) -> None:
        """Load and prepare launch hooks."""
        if (
            self.prelaunch_hooks is not None
            or self.postlaunch_hooks is not None
        ):
            if not force:
                self.log.info("Launch hooks were already discovered.")
                return

            self.prelaunch_hooks.clear()
            self.postlaunch_hooks.clear()

        self.log.debug("Discovery of launch hooks started.")

        paths = self.paths_to_launch_hooks()
        self.log.debug("Paths searched for launch hooks:\n{}".format(
            "\n".join(f"- {path}" for path in paths)
        ))

        all_classes: dict[str, list[Type[Union[PreLaunchHook, PostLaunchHook]]]] = {  # noqa: E501
            "pre": [],
            "post": []
        }
        for path in paths:
            if not os.path.exists(path):
                self.log.info(
                    f"Path to launch hooks does not exist: \"{path}\""
                )
                continue

            result = modules_from_path(path)
            # Future compatibility using 'ModulesResult'
            # TODO Remove when ayon-core > 1.9.10 is required
            if hasattr(result, "modules"):
                modules = [item.module for item in result.modules]
            else:
                modules_info, _crashed = result
                modules = [module for _, module in modules_info]

            for module in modules:
                all_classes["pre"].extend(
                    classes_from_module(PreLaunchHook, module)
                )
                all_classes["post"].extend(
                    classes_from_module(PostLaunchHook, module)
                )

        for launch_type, classes in all_classes.items():
            hooks_with_order = []
            hooks_without_order = []
            for klass in classes:
                try:
                    hook = klass(self)
                    if not hook.is_valid:
                        self.log.debug(
                            "Skipped hook invalid for current launch context:"
                            f" {klass.__name__}"
                        )
                        continue

                    if inspect.isabstract(hook):
                        self.log.debug(
                            f"Skipped abstract hook: {klass.__name__}"
                        )
                        continue

                    # Separate hooks by pre/post class
                    if hook.order is None:
                        hooks_without_order.append(hook)
                    else:
                        hooks_with_order.append(hook)

                except Exception:
                    self.log.warning(
                        f"Initialization of hook failed: {klass.__name__}",
                        exc_info=True
                    )

            # Sort hooks with order by order
            ordered_hooks = list(sorted(
                hooks_with_order, key=lambda obj: obj.order
            ))
            # Extend ordered hooks with hooks without defined order
            ordered_hooks.extend(hooks_without_order)

            if launch_type == "pre":
                self.prelaunch_hooks = ordered_hooks
            else:
                self.postlaunch_hooks = ordered_hooks

        self.log.debug(
            f"Found {len(self.prelaunch_hooks)} prelaunch"
            f" and {len(self.postlaunch_hooks)} postlaunch hooks."
        )

    @property
    def app_name(self) -> str:
        return self.application.name

    @property
    def host_name(self) -> str:
        return self.application.host_name

    @property
    def app_group(self) -> ApplicationGroup:
        return self.application.group

    @property
    def manager(self) -> ApplicationManager:
        return self.application.manager

    def _run_process(self) -> subprocess.Popen:
        """Run the process with the given launch arguments and keyword args.

        This method will handle the process differently based on the platform
        it is running on. It will create a temporary file for output on
        Windows and macos, while on Linux it will use a mid-process to launch
        the application with the provided arguments and environment variables.

        It will pass file paths to temporary files to the mid-process where
        the process output and pid will be stored.

        Returns:
            subprocess.Popen: The process object created by Popen.

        """
        # Windows and macOS have easier process start
        low_platform = platform.system().lower()
        if low_platform in ("windows", "darwin"):
            return self._execute_with_stdout()
        # Linux uses mid-process
        # - it is possible that the mid-process executable is not
        #   available for this version of AYON in that case use standard
        #   launch
        launch_args = get_linux_launcher_args()
        if launch_args is None:
            return subprocess.Popen(self.launch_args, **self.kwargs)

        # Prepare data that will be passed to mid-process
        # - store arguments to a json and pass path to json as last argument
        # - pass environments to set
        app_env = self.kwargs.pop("env", {})
        # create temporary file path passed to mid-process

        output_file = None
        if self.redirect_output:
            with tempfile.NamedTemporaryFile(
                mode="w",
                prefix=f"ayon_{self.application.host_name}_output_",
                suffix=".txt",
                delete=False,
                encoding="utf-8",
            ) as temp_file:
                output_file = temp_file.name
        # create temporary file to read back pid
        with tempfile.NamedTemporaryFile(
            mode="w",
            prefix="ayon_pid_",
            suffix=".txt",
            delete=False,
            encoding="utf-8",
        ) as pid_temp_file:
            pid_file = pid_temp_file.name

        json_data = {
            "args": self.launch_args,
            "env": app_env,
            "pid_file": pid_file,
        }
        if output_file:
            json_data["stdout"] = output_file
            json_data["stderr"] = output_file

        if app_env:
            # Filter environments of subprocess
            self.kwargs["env"] = {
                key: value
                for key, value in os.environ.items()
                if key in app_env
            }

        # Create the temp file
        with tempfile.NamedTemporaryFile(
            mode="w", prefix="ay_app_args", suffix=".json", delete=False
        ) as json_temp:
            json_temp_filepath = json_temp.name
            json.dump(json_data, json_temp)

        launch_args.append(json_temp_filepath)

        # Create mid-process which will launch application
        process = subprocess.Popen(launch_args, **self.kwargs)
        # Wait until the process finishes
        #   - This is important! The process would stay in "open" state.
        process.wait()

        # read back pid from the json file
        try:
            with open(json_temp_filepath, encoding="utf-8") as stream:
                json_data = json.load(stream)

                try:
                    import psutil
                except ImportError:
                    psutil = None

                pid_from_mid = json_data.get("pid")
                executable = Path(str(self.executable))
                start_time = None
                if pid_from_mid and psutil:
                    start_time = (
                        self.process_manager.get_process_start_time_by_pid(
                            pid_from_mid)
                    )
                    executable = (
                        self.process_manager.get_executable_path_by_pid(
                            pid_from_mid)
                    ) or executable

                from .process import ProcessInfo

                process_info = ProcessInfo(
                    name=self.application.full_name,
                    executable=executable,
                    args=self.launch_args,
                    env=app_env,
                    cwd=self.kwargs.get("cwd") or os.getcwd(),
                    pid=pid_from_mid,
                    output=Path(output_file) if self.redirect_output else None,
                    start_time=start_time,
                )
                # Store process info to the database
                self.process_manager.store_process_info(process_info)
        except OSError:
            self.log.exception(
                "Failed to read process info from JSON file: %s"
            )

        # Remove the temp file
        os.remove(json_temp_filepath)
        # Return process which is already terminated
        return process

    def run_prelaunch_hooks(self) -> None:
        """Run prelaunch hooks.

        This method will be executed only once, any future calls will skip
        the processing.

        Raises:
            RuntimeError: When prelaunch hooks were already executed.

        """
        if self._prelaunch_hooks_executed:
            self.log.warning("Prelaunch hooks were already executed.")
            return
        # Discover launch hooks
        self.discover_launch_hooks()

        # Execute prelaunch hooks
        for hook in self.prelaunch_hooks:
            self.log.debug(
                f"Executing prelaunch hook: {hook.__class__.__name__}"
            )
            hook.execute()
        self._prelaunch_hooks_executed = True

    def launch(self) -> Optional[subprocess.Popen]:
        """Collect data for new process and then create it.

        This method must not be executed more than once.

        Returns:
            subprocess.Popen: Created process as Popen object.

        """
        if self.process is not None:
            self.log.warning("Application was already launched.")
            return None

        if not self._prelaunch_hooks_executed:
            self.run_prelaunch_hooks()

        self.log.debug("All prelaunch hook executed. Starting new process.")

        # Prepare subprocess args
        args_len_str = ""
        if isinstance(self.launch_args, str):
            args = self.launch_args
        else:
            args = self.clear_launch_args(self.launch_args)
            args_len_str = f" ({len(args)})"
        self.log.info(
            f'Launching "{self.application.full_name}"'
            f" with args{args_len_str}: {args}"
        )
        self.launch_args = args

        # Run process
        self.process = self._run_process()

        # Process post launch hooks
        for hook in self.postlaunch_hooks:
            self.log.debug(
                f"Executing postlaunch hook: {hook.__class__.__name__}"
            )

            # TODO how to handle errors?
            # - store to variable to let them accessible?
            try:
                hook.execute()

            except Exception:
                self.log.warning(
                    "After launch procedures were not successful.",
                    exc_info=True,
                )

        self.log.debug(f"Launch of {self.application.full_name} finished.")

        return self.process

    @staticmethod
    def clear_launch_args(args: list) -> list[str]:
        """Collect launch arguments to final order.

        Launch argument should be a list that may contain another lists this
        function will upack inner lists and keep ordering.

        ```
        # source
        [ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]
        # result
        [ arg1, arg2, arg3, arg4, arg5, arg6]

        Args:
            args (list): Source arguments in list may contain inner lists.

        Returns:
            list: Unpacked arguments.

        """
        all_cleared = False
        while not all_cleared:
            all_cleared = True
            new_args = []
            for arg in args:
                if isinstance(arg, (list, tuple, set)):
                    all_cleared = False
                    for _arg in arg:
                        new_args.append(_arg)
                else:
                    new_args.append(arg)
            args = new_args

        return args

    def _execute_with_stdout(self) -> subprocess.Popen:
        """Run the process with stdout and stderr redirected to a file.

        Stores process information to the database.

        Returns:
            subprocess.Popen: The process object created by Popen.
        """
        from .process import ProcessInfo

        process_info = ProcessInfo(
            name=self.application.full_name,
            executable=Path(str(self.executable)),
            args=self.launch_args,
            env=self.kwargs.get("env", {}),
            cwd=self.kwargs.get("cwd") or os.getcwd(),
            output=None,
            pid=None,
            start_time=None,
        )

        if self.redirect_output:
            with tempfile.NamedTemporaryFile(
                mode="w",
                prefix=f"ayon_{self.application.host_name}_output_",
                suffix=".txt",
                delete=False,
                encoding="utf-8",
            ) as temp_file:
                temp_file_path = temp_file.name

            with open(temp_file_path, "wb") as tmp_file:
                self.kwargs["stdout"] = tmp_file
                self.kwargs["stderr"] = tmp_file
                process = subprocess.Popen(self.launch_args, **self.kwargs)
                process_info.output = Path(temp_file_path)
        else:
            process = subprocess.Popen(self.launch_args, **self.kwargs)

        start_time = self.process_manager.get_process_start_time(process)
        process_info.pid = process.pid
        process_info.start_time = start_time
        # Store process info to the database
        self.process_manager.store_process_info(process_info)

        return process

modules_manager property

Deprecated

Use 'addons_manager' instead.

clear_launch_args(args) staticmethod

Collect launch arguments to final order.

Launch argument should be a list that may contain another lists this function will upack inner lists and keep ordering.

```

source

[ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]

result

[ arg1, arg2, arg3, arg4, arg5, arg6]

Args: args (list): Source arguments in list may contain inner lists.

Returns: list: Unpacked arguments.

Source code in client/ayon_applications/manager.py
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
@staticmethod
def clear_launch_args(args: list) -> list[str]:
    """Collect launch arguments to final order.

    Launch argument should be a list that may contain another lists this
    function will upack inner lists and keep ordering.

    ```
    # source
    [ [ arg1, [ arg2, arg3 ] ], arg4, [arg5, arg6]]
    # result
    [ arg1, arg2, arg3, arg4, arg5, arg6]

    Args:
        args (list): Source arguments in list may contain inner lists.

    Returns:
        list: Unpacked arguments.

    """
    all_cleared = False
    while not all_cleared:
        all_cleared = True
        new_args = []
        for arg in args:
            if isinstance(arg, (list, tuple, set)):
                all_cleared = False
                for _arg in arg:
                    new_args.append(_arg)
            else:
                new_args.append(arg)
        args = new_args

    return args

discover_launch_hooks(force=False)

Load and prepare launch hooks.

Source code in client/ayon_applications/manager.py
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
def discover_launch_hooks(self, force: bool = False) -> None:
    """Load and prepare launch hooks."""
    if (
        self.prelaunch_hooks is not None
        or self.postlaunch_hooks is not None
    ):
        if not force:
            self.log.info("Launch hooks were already discovered.")
            return

        self.prelaunch_hooks.clear()
        self.postlaunch_hooks.clear()

    self.log.debug("Discovery of launch hooks started.")

    paths = self.paths_to_launch_hooks()
    self.log.debug("Paths searched for launch hooks:\n{}".format(
        "\n".join(f"- {path}" for path in paths)
    ))

    all_classes: dict[str, list[Type[Union[PreLaunchHook, PostLaunchHook]]]] = {  # noqa: E501
        "pre": [],
        "post": []
    }
    for path in paths:
        if not os.path.exists(path):
            self.log.info(
                f"Path to launch hooks does not exist: \"{path}\""
            )
            continue

        result = modules_from_path(path)
        # Future compatibility using 'ModulesResult'
        # TODO Remove when ayon-core > 1.9.10 is required
        if hasattr(result, "modules"):
            modules = [item.module for item in result.modules]
        else:
            modules_info, _crashed = result
            modules = [module for _, module in modules_info]

        for module in modules:
            all_classes["pre"].extend(
                classes_from_module(PreLaunchHook, module)
            )
            all_classes["post"].extend(
                classes_from_module(PostLaunchHook, module)
            )

    for launch_type, classes in all_classes.items():
        hooks_with_order = []
        hooks_without_order = []
        for klass in classes:
            try:
                hook = klass(self)
                if not hook.is_valid:
                    self.log.debug(
                        "Skipped hook invalid for current launch context:"
                        f" {klass.__name__}"
                    )
                    continue

                if inspect.isabstract(hook):
                    self.log.debug(
                        f"Skipped abstract hook: {klass.__name__}"
                    )
                    continue

                # Separate hooks by pre/post class
                if hook.order is None:
                    hooks_without_order.append(hook)
                else:
                    hooks_with_order.append(hook)

            except Exception:
                self.log.warning(
                    f"Initialization of hook failed: {klass.__name__}",
                    exc_info=True
                )

        # Sort hooks with order by order
        ordered_hooks = list(sorted(
            hooks_with_order, key=lambda obj: obj.order
        ))
        # Extend ordered hooks with hooks without defined order
        ordered_hooks.extend(hooks_without_order)

        if launch_type == "pre":
            self.prelaunch_hooks = ordered_hooks
        else:
            self.postlaunch_hooks = ordered_hooks

    self.log.debug(
        f"Found {len(self.prelaunch_hooks)} prelaunch"
        f" and {len(self.postlaunch_hooks)} postlaunch hooks."
    )

launch()

Collect data for new process and then create it.

This method must not be executed more than once.

Returns:

Type Description
Optional[Popen]

subprocess.Popen: Created process as Popen object.

Source code in client/ayon_applications/manager.py
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
def launch(self) -> Optional[subprocess.Popen]:
    """Collect data for new process and then create it.

    This method must not be executed more than once.

    Returns:
        subprocess.Popen: Created process as Popen object.

    """
    if self.process is not None:
        self.log.warning("Application was already launched.")
        return None

    if not self._prelaunch_hooks_executed:
        self.run_prelaunch_hooks()

    self.log.debug("All prelaunch hook executed. Starting new process.")

    # Prepare subprocess args
    args_len_str = ""
    if isinstance(self.launch_args, str):
        args = self.launch_args
    else:
        args = self.clear_launch_args(self.launch_args)
        args_len_str = f" ({len(args)})"
    self.log.info(
        f'Launching "{self.application.full_name}"'
        f" with args{args_len_str}: {args}"
    )
    self.launch_args = args

    # Run process
    self.process = self._run_process()

    # Process post launch hooks
    for hook in self.postlaunch_hooks:
        self.log.debug(
            f"Executing postlaunch hook: {hook.__class__.__name__}"
        )

        # TODO how to handle errors?
        # - store to variable to let them accessible?
        try:
            hook.execute()

        except Exception:
            self.log.warning(
                "After launch procedures were not successful.",
                exc_info=True,
            )

    self.log.debug(f"Launch of {self.application.full_name} finished.")

    return self.process

paths_to_launch_hooks()

Directory paths where to look for launch hooks.

Source code in client/ayon_applications/manager.py
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
def paths_to_launch_hooks(self) -> list[str]:
    """Directory paths where to look for launch hooks."""
    # This method has potential to be part of application manager (maybe).
    paths = []

    # TODO load additional studio paths from settings
    global_hooks_dir = os.path.join(AYON_CORE_ROOT, "hooks")

    hooks_dirs = [
        global_hooks_dir
    ]
    if self.host_name:
        # If host requires launch hooks and is module then launch hooks
        #   should be collected using 'collect_launch_hook_paths'
        #   - module have to implement 'get_launch_hook_paths'
        host_module = self.addons_manager.get_host_addon(self.host_name)
        if not host_module:
            hooks_dirs.append(os.path.join(
                AYON_CORE_ROOT, "hosts", self.host_name, "hooks"
            ))

    for path in hooks_dirs:
        if (
            os.path.exists(path)
            and os.path.isdir(path)
            and path not in paths
        ):
            paths.append(path)

    # Load modules paths
    paths.extend(self._collect_addons_launch_hook_paths())

    return paths

run_prelaunch_hooks()

Run prelaunch hooks.

This method will be executed only once, any future calls will skip the processing.

Raises:

Type Description
RuntimeError

When prelaunch hooks were already executed.

Source code in client/ayon_applications/manager.py
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def run_prelaunch_hooks(self) -> None:
    """Run prelaunch hooks.

    This method will be executed only once, any future calls will skip
    the processing.

    Raises:
        RuntimeError: When prelaunch hooks were already executed.

    """
    if self._prelaunch_hooks_executed:
        self.log.warning("Prelaunch hooks were already executed.")
        return
    # Discover launch hooks
    self.discover_launch_hooks()

    # Execute prelaunch hooks
    for hook in self.prelaunch_hooks:
        self.log.debug(
            f"Executing prelaunch hook: {hook.__class__.__name__}"
        )
        hook.execute()
    self._prelaunch_hooks_executed = True

ApplicationLaunchFailed

Bases: Exception

Application launch failed due to known reason.

Message should be self explanatory as traceback won't be shown.

Source code in client/ayon_applications/exceptions.py
41
42
43
44
45
46
class ApplicationLaunchFailed(Exception):
    """Application launch failed due to known reason.

    Message should be self explanatory as traceback won't be shown.
    """
    pass

ApplicationManager

Load applications and tools and store them by their full name.

Parameters:

Name Type Description Default
studio_settings dict

Preloaded studio settings. When passed manager will always use these values. Gives ability to create manager using different settings.

None
Source code in client/ayon_applications/manager.py
 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
class ApplicationManager:
    """Load applications and tools and store them by their full name.

    Args:
        studio_settings (dict): Preloaded studio settings. When passed manager
            will always use these values. Gives ability to create manager
            using different settings.
    """

    def __init__(self, studio_settings: Optional[dict[str, Any]] = None):
        self.log = Logger.get_logger(self.__class__.__name__)

        self.app_groups: dict[str, ApplicationGroup] = {}
        self.applications: dict[str, Application] = {}
        self.tool_groups: dict[str, EnvironmentToolGroup] = {}
        self.tools: dict[str, EnvironmentTool] = {}

        self._app_group_info: dict[str, GroupAppInfo] = {}

        self._studio_settings = studio_settings

        self.refresh()

    def get_app_label(self, group_name: str) -> str:
        """Get label for application group by name.

        Args:
            group_name (str): Application group name.

        Returns:
            str: Application label.

        """
        group_info = self._app_group_info.get(group_name)
        if group_info is not None and group_info.label:
            return group_info.label
        return group_name

    def get_app_icon(self, group_name: str) -> dict[str, str] | None:
        """Get icon for application group by name.

        Args:
            group_name (str): Application name.

        Returns:
            dict[str, str] | None: Application icon definition.

        """
        group_info = self._app_group_info.get(group_name)
        if group_info is None:
            return None
        return group_info.icon

    def set_studio_settings(self, studio_settings: dict[str, Any]) -> None:
        """Ability to change init system settings.

        This will trigger refresh of manager.
        """
        self._studio_settings = studio_settings

        self.refresh()

    def refresh(self) -> None:
        """Refresh applications from settings."""
        from ayon_applications import ApplicationsAddon

        self.app_groups.clear()
        self.applications.clear()
        self.tool_groups.clear()
        self.tools.clear()
        self._app_group_info.clear()

        app_group_info = {}
        app_items = ApplicationsAddon.get_application_items()
        for app_item in app_items:
            group_name = app_item["full_name"].split("/")[0]
            if group_name in app_group_info:
                continue

            app_group_info[group_name] = GroupAppInfo(
                group_name,
                label=app_item["group_label"],
                icon=app_item["icon"],
            )

        self._app_group_info = app_group_info

        if self._studio_settings is not None:
            settings = copy.deepcopy(self._studio_settings)
        else:
            settings = get_studio_settings(
                clear_metadata=False, exclude_locals=False
            )

        applications_addon_settings = settings["applications"]

        # Prepare known applications
        app_defs = applications_addon_settings["applications"]
        additional_apps = app_defs.pop("additional_apps")
        for additional_app in additional_apps:
            app_name = additional_app.pop("name")
            if app_name in app_defs:
                self.log.warning(
                    f"Additional application '{app_name}' is already"
                    " in built-in applications."
                )
            app_defs[app_name] = additional_app

        for group_name, variant_defs in app_defs.items():
            group = ApplicationGroup(group_name, variant_defs, self)
            self.app_groups[group_name] = group
            for app in group:
                self.applications[app.full_name] = app

        tools_definitions = applications_addon_settings["tool_groups"]
        for tool_group_data in tools_definitions:
            group = EnvironmentToolGroup(tool_group_data, self)
            self.tool_groups[group.name] = group
            for tool in group:
                self.tools[tool.full_name] = tool

    def find_latest_available_variant_for_group(
        self, group_name: str
    ) -> Optional[ApplicationGroup]:
        group = self.app_groups.get(group_name)
        if group is None or not group.enabled:
            return None

        output = None
        for _, variant in reversed(sorted(group.variants.items())):
            executable = variant.find_executable()
            if executable:
                output = variant
                break
        return output

    def create_launch_context(
        self, app_name: str, **data
    ) -> "ApplicationLaunchContext":
        """Prepare launch context for application.

        Args:
            app_name (str): Name of application that should be launched.
            **data (Any): Any additional data. Data may be used during

        Returns:
            ApplicationLaunchContext: Launch context for application.

        Raises:
            ApplicationNotFound: Application was not found by entered name.
        """

        app = self.applications.get(app_name)
        if not app:
            raise ApplicationNotFound(app_name)

        executable = app.find_executable()

        return ApplicationLaunchContext(
            app, executable, **data
        )

    def launch_with_context(
        self, launch_context: "ApplicationLaunchContext"
    ) -> Optional[subprocess.Popen]:
        """Launch application using existing launch context.

        Args:
            launch_context (ApplicationLaunchContext): Prepared launch
                context.
        """

        if not launch_context.executable:
            raise ApplicationExecutableNotFound(launch_context.application)
        return launch_context.launch()

    def launch(self, app_name, **data) -> Optional[subprocess.Popen]:
        """Launch procedure.

        For host application it's expected to contain "project_name",
        "folder_path" and "task_name".

        Args:
            app_name (str): Name of application that should be launched.
            **data (Any): Any additional data. Data may be used during
                preparation to store objects usable in multiple places.

        Raises:
            ApplicationNotFound: Application was not found by entered
                argument `app_name`.
            ApplicationExecutableNotFound: Executables in application
                definition were not found on this machine.
            ApplicationLaunchFailed: Something important for application launch
                failed. Exception should contain an explanation message,
                traceback should not be needed.

        """
        context = self.create_launch_context(app_name, **data)
        return self.launch_with_context(context)

create_launch_context(app_name, **data)

Prepare launch context for application.

Parameters:

Name Type Description Default
app_name str

Name of application that should be launched.

required
**data Any

Any additional data. Data may be used during

{}

Returns:

Name Type Description
ApplicationLaunchContext 'ApplicationLaunchContext'

Launch context for application.

Raises:

Type Description
ApplicationNotFound

Application was not found by entered name.

Source code in client/ayon_applications/manager.py
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
def create_launch_context(
    self, app_name: str, **data
) -> "ApplicationLaunchContext":
    """Prepare launch context for application.

    Args:
        app_name (str): Name of application that should be launched.
        **data (Any): Any additional data. Data may be used during

    Returns:
        ApplicationLaunchContext: Launch context for application.

    Raises:
        ApplicationNotFound: Application was not found by entered name.
    """

    app = self.applications.get(app_name)
    if not app:
        raise ApplicationNotFound(app_name)

    executable = app.find_executable()

    return ApplicationLaunchContext(
        app, executable, **data
    )

get_app_icon(group_name)

Get icon for application group by name.

Parameters:

Name Type Description Default
group_name str

Application name.

required

Returns:

Type Description
dict[str, str] | None

dict[str, str] | None: Application icon definition.

Source code in client/ayon_applications/manager.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def get_app_icon(self, group_name: str) -> dict[str, str] | None:
    """Get icon for application group by name.

    Args:
        group_name (str): Application name.

    Returns:
        dict[str, str] | None: Application icon definition.

    """
    group_info = self._app_group_info.get(group_name)
    if group_info is None:
        return None
    return group_info.icon

get_app_label(group_name)

Get label for application group by name.

Parameters:

Name Type Description Default
group_name str

Application group name.

required

Returns:

Name Type Description
str str

Application label.

Source code in client/ayon_applications/manager.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def get_app_label(self, group_name: str) -> str:
    """Get label for application group by name.

    Args:
        group_name (str): Application group name.

    Returns:
        str: Application label.

    """
    group_info = self._app_group_info.get(group_name)
    if group_info is not None and group_info.label:
        return group_info.label
    return group_name

launch(app_name, **data)

Launch procedure.

For host application it's expected to contain "project_name", "folder_path" and "task_name".

Parameters:

Name Type Description Default
app_name str

Name of application that should be launched.

required
**data Any

Any additional data. Data may be used during preparation to store objects usable in multiple places.

{}

Raises:

Type Description
ApplicationNotFound

Application was not found by entered argument app_name.

ApplicationExecutableNotFound

Executables in application definition were not found on this machine.

ApplicationLaunchFailed

Something important for application launch failed. Exception should contain an explanation message, traceback should not be needed.

Source code in client/ayon_applications/manager.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def launch(self, app_name, **data) -> Optional[subprocess.Popen]:
    """Launch procedure.

    For host application it's expected to contain "project_name",
    "folder_path" and "task_name".

    Args:
        app_name (str): Name of application that should be launched.
        **data (Any): Any additional data. Data may be used during
            preparation to store objects usable in multiple places.

    Raises:
        ApplicationNotFound: Application was not found by entered
            argument `app_name`.
        ApplicationExecutableNotFound: Executables in application
            definition were not found on this machine.
        ApplicationLaunchFailed: Something important for application launch
            failed. Exception should contain an explanation message,
            traceback should not be needed.

    """
    context = self.create_launch_context(app_name, **data)
    return self.launch_with_context(context)

launch_with_context(launch_context)

Launch application using existing launch context.

Parameters:

Name Type Description Default
launch_context ApplicationLaunchContext

Prepared launch context.

required
Source code in client/ayon_applications/manager.py
215
216
217
218
219
220
221
222
223
224
225
226
227
def launch_with_context(
    self, launch_context: "ApplicationLaunchContext"
) -> Optional[subprocess.Popen]:
    """Launch application using existing launch context.

    Args:
        launch_context (ApplicationLaunchContext): Prepared launch
            context.
    """

    if not launch_context.executable:
        raise ApplicationExecutableNotFound(launch_context.application)
    return launch_context.launch()

refresh()

Refresh applications from settings.

Source code in client/ayon_applications/manager.py
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
def refresh(self) -> None:
    """Refresh applications from settings."""
    from ayon_applications import ApplicationsAddon

    self.app_groups.clear()
    self.applications.clear()
    self.tool_groups.clear()
    self.tools.clear()
    self._app_group_info.clear()

    app_group_info = {}
    app_items = ApplicationsAddon.get_application_items()
    for app_item in app_items:
        group_name = app_item["full_name"].split("/")[0]
        if group_name in app_group_info:
            continue

        app_group_info[group_name] = GroupAppInfo(
            group_name,
            label=app_item["group_label"],
            icon=app_item["icon"],
        )

    self._app_group_info = app_group_info

    if self._studio_settings is not None:
        settings = copy.deepcopy(self._studio_settings)
    else:
        settings = get_studio_settings(
            clear_metadata=False, exclude_locals=False
        )

    applications_addon_settings = settings["applications"]

    # Prepare known applications
    app_defs = applications_addon_settings["applications"]
    additional_apps = app_defs.pop("additional_apps")
    for additional_app in additional_apps:
        app_name = additional_app.pop("name")
        if app_name in app_defs:
            self.log.warning(
                f"Additional application '{app_name}' is already"
                " in built-in applications."
            )
        app_defs[app_name] = additional_app

    for group_name, variant_defs in app_defs.items():
        group = ApplicationGroup(group_name, variant_defs, self)
        self.app_groups[group_name] = group
        for app in group:
            self.applications[app.full_name] = app

    tools_definitions = applications_addon_settings["tool_groups"]
    for tool_group_data in tools_definitions:
        group = EnvironmentToolGroup(tool_group_data, self)
        self.tool_groups[group.name] = group
        for tool in group:
            self.tools[tool.full_name] = tool

set_studio_settings(studio_settings)

Ability to change init system settings.

This will trigger refresh of manager.

Source code in client/ayon_applications/manager.py
106
107
108
109
110
111
112
113
def set_studio_settings(self, studio_settings: dict[str, Any]) -> None:
    """Ability to change init system settings.

    This will trigger refresh of manager.
    """
    self._studio_settings = studio_settings

    self.refresh()

ApplicationNotFound

Bases: Exception

Application was not found in ApplicationManager by name.

Source code in client/ayon_applications/exceptions.py
1
2
3
4
5
6
7
8
class ApplicationNotFound(Exception):
    """Application was not found in ApplicationManager by name."""

    def __init__(self, app_name):
        self.app_name = app_name
        super().__init__(
            f"Application \"{app_name}\" was not found."
        )

ApplicationsAddon

Bases: AYONAddon, IPluginPaths, ITrayAction

Source code in client/ayon_applications/addon.py
 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
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
class ApplicationsAddon(AYONAddon, IPluginPaths, ITrayAction):
    name = "applications"
    version = __version__

    # Tray action attributes
    label = "Process Monitor"
    admin_action = True

    _icons_cache: dict[str, bytes | None] = {}

    def tray_init(self) -> None:
        """Initialize the tray action."""
        self._process_monitor_window: Optional[ProcessMonitorWindow] = None

    def on_action_trigger(self) -> None:
        """Action triggered when the tray icon is clicked."""
        from ayon_applications.ui.process_monitor import (
            ProcessMonitorWindow,
        )
        if self._process_monitor_window is None:
            self._process_monitor_window = ProcessMonitorWindow()

        self._process_monitor_window.show()
        self._process_monitor_window.raise_()
        self._process_monitor_window.activateWindow()

    def get_app_environments_for_context(
        self,
        project_name: str,
        folder_path: str,
        task_name: str,
        full_app_name: str,
        env_group: Optional[str] = None,
        launch_type: Optional[str] = None,
        env: Optional[dict[str, str]] = None,
    ) -> dict[str, str]:
        """Calculate environment variables for launch context.

        Args:
            project_name (str): Project name.
            folder_path (str): Folder path.
            task_name (str): Task name.
            full_app_name (str): Full application name.
            env_group (Optional[str]): Environment group.
            launch_type (Optional[str]): Launch type.
            env (Optional[dict[str, str]]): Environment variables to update.

        Returns:
            dict[str, str]: Environment variables for context.

        """
        from ayon_applications.utils import get_app_environments_for_context

        if not full_app_name:
            return {}

        return get_app_environments_for_context(
            project_name,
            folder_path,
            task_name,
            full_app_name,
            env_group=env_group,
            launch_type=launch_type,
            env=env,
            addons_manager=self.manager
        )

    def get_farm_publish_environment_variables(
        self,
        project_name: str,
        folder_path: str,
        task_name: str,
        full_app_name: Optional[str] = None,
        env_group: Optional[str] = None,
    ) -> dict[str, str]:
        """Calculate environment variables for farm publish.

        Args:
            project_name (str): Project name.
            folder_path (str): Folder path.
            task_name (str): Task name.
            env_group (Optional[str]): Environment group.
            full_app_name (Optional[str]): Full application name. Value from
                environment variable 'AYON_APP_NAME' is used if 'None' is
                passed.

        Returns:
            dict[str, str]: Environment variables for farm publish.

        """
        if full_app_name is None:
            full_app_name = os.getenv("AYON_APP_NAME")

        return self.get_app_environments_for_context(
            project_name,
            folder_path,
            task_name,
            full_app_name,
            env_group=env_group,
            launch_type=LaunchTypes.farm_publish
        )

    def get_applications_manager(
        self, settings: Optional[dict[str, Any]] = None
    ) -> "ApplicationManager":
        """Get applications manager.

        Args:
            settings (Optional[dict]): Studio/project settings.

        Returns:
            ApplicationManager: Applications manager.

        """
        return ApplicationManager(settings)

    def get_plugin_paths(self) -> dict[str, list[str]]:
        return {}

    def get_publish_plugin_paths(self, host_name: str) -> list[str]:
        return [
            os.path.join(APPLICATIONS_ADDON_ROOT, "plugins", "publish")
        ]

    def get_launch_hook_paths(self, app: "Application") -> list[str]:
        return [
            os.path.join(APPLICATIONS_ADDON_ROOT, "hooks")
        ]

    def get_loader_action_plugin_paths(self, host_name):
        return [
            os.path.join(APPLICATIONS_ADDON_ROOT, "plugins", "load_actions"),
        ]

    def get_app_icon_path(self, icon_filename: str) -> str:
        """DEPRECATED Get icon path.

        Args:
            icon_filename (str): Icon filename.

        Returns:
            Optional[str]: Icon path or None if not found.

        """
        return get_app_icon_path(icon_filename)

    @classmethod
    def get_custom_icons_info(cls) -> list[dict[str, str]]:
        """List custom icons available on the server.

        Returns:
            list[dict[str, str]]: List of custom icons.

        """
        endpoint = f"addons/{cls.name}/{cls.version}/customIcons"
        response = ayon_api.get(endpoint)
        response.raise_for_status()
        return response.data["icons"]

    @classmethod
    def upload_custom_icon(
        cls, path: str, filename: str | None = None
    ) -> None:
        """Upload custom icon to AYON server.

        Args:
            path (str): Path to icon file.
            filename (str | None): Icon filename which will be used
                to store the icon on the server. This value is then used in
                settings.

        """
        if filename is None:
            filename = os.path.basename(path)
        endpoint = f"addons/{cls.name}/{cls.version}/customIcons/{filename}"
        response = ayon_api.upload_file(
            endpoint, path
        )
        response.raise_for_status()

    @classmethod
    def delete_custom_icon(cls, filename: str) -> None:
        """Delete custom icon from AYON server.

        Args:
            filename (str): Icon filename which will be deleted
                from the server.

        """
        endpoint = f"addons/{cls.name}/{cls.version}/customIcons/{filename}"
        response = ayon_api.delete(endpoint)
        response.raise_for_status()

    @classmethod
    def get_app_icon_url(
        cls, icon: dict[str, Any] | str, server: bool = False
    ) -> str | None:
        """Get icon path.

        icon filename can be either a full URL (http/https/file/...)
        or a bare filename. Full URLs are used as is while bare filenames
        resolve to the addons icons folder.

        Method does not validate if icon filename exist on server.

        Args:
            icon (dict[str, Any] | str): Icon name.
            server (bool): Return url to AYON server.

        Returns:
            str | None: Icon path or None is server url is not
                available.

        """
        if not icon:
            return None

        if isinstance(icon, str):
            icon_filename = icon
        elif isinstance(icon, dict):
            # NOTE At this moment the url always leads to addon's icons
            #   endpoint and last part of path is filename
            url = icon.get("url")
            if not isinstance(url, str):
                return None
            icon_filename = os.path.basename(url)

        else:
            return None

        # check if its a full URL
        try:
            url = urllib.parse.urlparse(icon_filename)
            if url.scheme:
                return icon_filename
        except Exception:
            pass

        icon_name = os.path.basename(icon_filename)
        if server:
            base_url = ayon_api.get_base_url()
            return (
                f"{base_url}/api/addons/{cls.name}/{cls.version}"
                f"/icons/{icon_name}"
            )
        server_url = os.getenv("AYON_WEBSERVER_URL")
        if not server_url:
            return None
        return "/".join([
            server_url, "addons", cls.name, "icons", icon_name
        ])

    @classmethod
    def get_application_items(
        cls,
        project_name: str | None = None,
        task_id: str | None = None,
        *,
        variant: str | None = None,
        version: str | None = None,
    ) -> list[dict[str, Any]]:
        """Get application items.

        This is meant as api for other addons to get application items for
            a given context. Can also filter applications for a specific task.

        It does handle project bundles and settings variant automatically.

        Args:
            project_name (str | None): Project name.
            task_id (str | None): Task id for which applications are fitlered.
            variant (str | None): Settings variant. Current settings variant
                is used if not passed in.
            version (str | None): Specific version of applications addon
                to get items for. If None, it will use the version
                resolved for current context (variant and project).

        Example application dict (may vary based on applications
            addon version):
            {
                "host_name": str
                "full_name": str
                "full_label": str
                "group_label": str
                "variant_label": str
                "icon": dict[str, str] | None
                "show_grouped": bool
            }

        Returns:
            list[dict]: Application items.

        """
        if variant is None:
            variant = get_settings_variant()

        query_params = {"variant": variant}
        if version is not None:
            query_params["version"] = version

        query = urllib.parse.urlencode(query_params)
        context_path = ""
        if project_name:
            context_path = f"/{project_name}"
            if task_id:
                context_path = f"{context_path}/task/{task_id}"

        response = ayon_api.get(
            f"addons/{cls.name}/{cls.version}/"
            f"apps{context_path}?{query}"
        )
        app_items = response.data["applications"]

        # Fill icon urls with 'addon_url' and prepare icon definitions
        if not version:
            version = cls.version
        addon_url = f"/addons/{cls.name}/{version}"

        for app_item in app_items:
            icon = app_item["icon"]
            if not icon:
                continue
            try:
                url = icon["url"].format(addon_url=addon_url)
            except Exception:
                continue
            app_item["icon"] = {
                "type": "ayon_url",
                "url": url.lstrip("/"),
            }
        return app_items

    @classmethod
    def get_tool_items(
        cls,
        project_name: str | None = None,
        *,
        variant: str | None = None,
        version: str | None = None,
    ) -> list[dict[str, Any]]:
        """Get tool items.

        This is meant as api for other addons to get tools items for a given
            context.

        It does handle project bundles and settings variant automatically.

        Args:
            project_name (str | None): Project name.
            variant (str | None): Settings variant. Current settings variant
                is used if not passed in.
            version (str | None): Specific version of applications addon
                to get items for. If None, it will use the version
                resolved for current context (variant and project).

        Example tool dict (may vary based on applications addon version):
            {
                "full_name": str,
                "full_label": str,
                "group_label": str,
                "variant_label": str,
                "host_names": list[str],
                "app_variants": list[str],
            }

        Returns:
            list[dict]: Tool items.

        """
        if variant is None:
            variant = get_settings_variant()

        query_params = {"variant": variant}
        if version is not None:
            query_params["version"] = version

        query = urllib.parse.urlencode(query_params)

        context_path = ""
        if project_name:
            context_path = f"/{project_name}"

        response = ayon_api.get(
            f"addons/{cls.name}/{cls.version}/"
            f"tools{context_path}?{query}"
        )
        return response.data["applications"]

    def launch_application(
        self,
        app_name: str,
        project_name: str,
        folder_path: str,
        task_name: str,
        workfile_path: Optional[str] = None,
        use_last_workfile: Optional[bool] = None,
    ):
        """Launch application.

        Args:
            app_name (str): Full application name e.g. 'maya/2024'.
            project_name (str): Project name.
            folder_path (str): Folder path.
            task_name (str): Task name.
            workfile_path (Optional[str]): Workfile path to use.
            use_last_workfile (Optional[bool]): Explicitly tell to use or
                not use last workfile. Ignored if 'workfile_path' is passed.

        """
        ensure_addons_are_process_ready(
            addon_name=self.name,
            addon_version=self.version,
            project_name=project_name,
        )
        headless = is_headless_mode_enabled()

        data = {
            "project_name": project_name,
            "folder_path": folder_path,
            "task_name": task_name,
        }
        # Backwards compatibility 'workfile_path' was added
        #   before 'use_last_workfile'
        if isinstance(workfile_path, bool):
            use_last_workfile = workfile_path
            workfile_path = None
            warnings.warn(
                "Passed 'use_last_workfile' as positional argument."
                " Use explicit 'use_last_workfile' keyword argument instead.",
                DeprecationWarning,
                stacklevel=2,
            )

        if workfile_path:
            data["workfile_path"] = workfile_path
            # Backwards compatibility to be able to use 'workfile_path'
            #   argument with older ayon-core
            # use_last_workfile = False
            data["last_workfile_path"] = workfile_path
            data["start_last_workfile"] = True

        elif use_last_workfile is not None:
            data["start_last_workfile"] = use_last_workfile

        # TODO handle raise errors
        failed = True
        message = None
        detail = None
        try:
            app_manager = self.get_applications_manager()
            app_manager.launch(app_name, **data)
            failed = False

        except (
            ApplicationLaunchFailed,
            ApplicationExecutableNotFound,
            ApplicationNotFound,
        ) as exc:
            message = str(exc)
            self.log.warning(f"Application launch failed: {message}")

        except Exception as exc:
            message = "An unexpected error happened"
            detail = "".join(traceback.format_exception(*sys.exc_info()))
            self.log.warning(
                f"Application launch failed: {str(exc)}",
                exc_info=True
            )

        if not failed:
            return

        if not headless:
            self._show_launch_error_dialog(message, detail)
        sys.exit(1)

    def webserver_initialization(self, manager: "WebServerManager") -> None:
        """Initialize webserver.

        Add localhost handler for icons requests.

        This was added for ftrack which is showing icons

        Args:
            manager (WebServerManager): Webserver manager.

        """
        def _cache_icon(filename: str, data: bytes | None) -> None:
            self.__class__._icons_cache[filename] = data
            if len(self.__class__._icons_cache) > 256:
                self.__class__._icons_cache.pop(
                    next(iter(self.__class__._icons_cache))
                )

        async def _get_web_icon(request):
            from aiohttp import web, ClientSession

            filename: str = os.path.basename(request.match_info["filename"])
            # TODO find better way how to cache
            if filename not in self.__class__._icons_cache:
                base_url = ayon_api.get_base_url()
                url = (
                    f"{base_url}/api/addons/{self.name}/{self.version}"
                    f"/icons/{filename}"
                )
                data = None
                async with ClientSession() as session:
                    async with session.get(url) as resp:
                        if resp.status != 200:
                            data = await resp.read()

                _cache_icon(filename, data)

            body = self.__class__._icons_cache[filename]
            if body is None:
                raise web.HTTPNotFound()
            return web.Response(body=body)

        manager.add_addon_route(
            self.name,
            "/icons/{filename}",
            "GET",
            _get_web_icon,
        )

    # --- CLI ---
    def cli(self, addon_click_group) -> None:
        main_group = click_wrap.group(
            self._cli_main, name=self.name, help="Applications addon"
        )
        (
            main_group.command(
                self._cli_extract_environments,
                name="extractenvironments",
                help=(
                    "Extract environment variables for context into json file"
                )
            )
            .argument("output_json_path")
            .option("--project", help="Project name", default=None)
            .option("--folder", help="Folder path", default=None)
            .option("--task", help="Task name", default=None)
            .option("--app", help="Full application name", default=None)
            .option(
                "--envgroup",
                help="Environment group (e.g. \"farm\")",
                default=None
            )
        )
        (
            main_group.command(
                self._cli_launch_context_names,
                name="launch",
                help="Launch application"
            )
            .option("--app", required=True, help="Full application name")
            .option("--project", required=True, help="Project name")
            .option("--folder", required=True, help="Folder path")
            .option("--task", required=True, help="Task name")
            .option(
                "--workfile-path",
                required=False,
                help="Workfile path",
                default=None,
            )
            .option(
                "--use-last-workfile",
                help="Use last workfile",
                default=None,
            )
        )
        (
            main_group.command(
                self._cli_launch_with_task_id,
                name="launch-by-id",
                help="Launch application"
            )
            .option("--app", required=True, help="Full application name")
            .option("--project", required=True, help="Project name")
            .option("--task-id", required=True, help="Task id")
            .option(
                "--workfile-path",
                required=False,
                help="Workfile path",
                default=None,
            )
            .option(
                "--use-last-workfile",
                help="Use last workfile",
                default=None,
            )
        )
        (
            main_group.command(
                self._cli_launch_with_workfile_id,
                name="launch-by-workfile-id",
                help="Launch application using workfile id"
            )
            .option("--app", required=True, help="Full application name")
            .option("--project", required=True, help="Project name")
            .option("--workfile-id", required=True, help="Workfile id")
        )
        (
            main_group.command(
                self._cli_launch_with_debug_terminal,
                name="launch-debug-terminal",
                help="Launch with debug terminal"
            )
            .option("--project", required=True, help="Project name")
            .option("--task-id", required=True, help="Task id")
            .option(
                "--app",
                required=False,
                help="Full application name",
                default=None,
            )
        )
        # Convert main command to click object and add it to parent group
        addon_click_group.add_command(
            main_group.to_click_obj()
        )

    def _cli_main(self) -> None:
        pass

    def _cli_extract_environments(
        self,
        output_json_path: str,
        project: str,
        folder: str,
        task: str,
        app: str,
        envgroup: str,
    ) -> None:
        """Produces json file with environment based on project and app.

        Called by farm integration to propagate environment into farm jobs.

        Args:
            output_json_path (str): Output json file path.
            project (str): Project name.
            folder (str): Folder path.
            task (str): Task name.
            app (str): Full application name e.g. 'maya/2024'.
            envgroup (str): Environment group.

        """
        if all((project, folder, task, app)):
            env = self.get_farm_publish_environment_variables(
                project, folder, task, app, env_group=envgroup,
            )
        else:
            env = os.environ.copy()

        output_dir = os.path.dirname(output_json_path)
        os.makedirs(output_dir, exist_ok=True)

        with open(output_json_path, "w") as file_stream:
            json.dump(env, file_stream, indent=4)

    def _cli_launch_context_names(
        self,
        project: str,
        folder: str,
        task: str,
        app: str,
        workfile_path: Optional[str] = None,
        use_last_workfile: Optional["BoolArg"] = None,
    ) -> None:
        """Launch application.

        Args:
            project (str): Project name.
            folder (str): Folder path.
            task (str): Task name.
            app (str): Full application name e.g. 'maya/2024'.
            workfile_path (str | None): Workfile path to use.
            use_last_workfile (Literal["1", "0"] | None): Explicitly tell
                to use last workfile.

        """
        if workfile_path:
            use_last_workfile = False

        elif use_last_workfile is not None:
            use_last_workfile = env_value_to_bool(
                use_last_workfile, default=None
            )

        self.launch_application(
            app,
            project,
            folder,
            task,
            workfile_path=workfile_path,
            use_last_workfile=use_last_workfile,
        )

    def _cli_launch_with_task_id(
        self,
        project: str,
        task_id: str,
        app: str,
        workfile_path: Optional[str] = None,
        use_last_workfile: Optional["BoolArg"] = None,
    ) -> None:
        """Launch application using project name, task id and full app name.

        Args:
            project (str): Project name.
            task_id (str): Task id.
            app (str): Full application name e.g. 'maya/2024'.
            workfile_path (str | None): Workfile path to use.
            use_last_workfile (Literal["1", "0"] | None): Explicitly tell
                to use last workfile.

        """
        if workfile_path:
            use_last_workfile = False

        elif use_last_workfile is not None:
            use_last_workfile = env_value_to_bool(
                value=use_last_workfile, default=None
            )

        task_entity = ayon_api.get_task_by_id(
            project, task_id, fields={"name", "folderId"}
        )
        folder_entity = ayon_api.get_folder_by_id(
            project, task_entity["folderId"], fields={"path"}
        )
        self.launch_application(
            app,
            project,
            folder_entity["path"],
            task_entity["name"],
            workfile_path=workfile_path,
            use_last_workfile=use_last_workfile,
        )

    def _cli_launch_with_workfile_id(
        self,
        project: str,
        workfile_id: str,
        app: str,
    ) -> None:
        from ayon_core.pipeline import Anatomy

        workfile_entity = ayon_api.get_workfile_info_by_id(
            project, workfile_id
        )
        task_id = workfile_entity["taskId"]
        task_entity = ayon_api.get_task_by_id(
            project, task_id, fields={"name", "folderId"}
        )
        folder_entity = ayon_api.get_folder_by_id(
            project, task_entity["folderId"], fields={"path"}
        )
        anatomy = Anatomy(project)
        workfile_path = anatomy.fill_root(workfile_entity["path"])
        self.launch_application(
            app,
            project,
            folder_entity["path"],
            task_entity["name"],
            workfile_path=workfile_path,
        )

    def _cli_launch_with_debug_terminal(
        self,
        project: str,
        task_id: str,
        app: Optional[str],
    ) -> None:
        from .ui.debug_terminal_launch import run_with_debug_terminal

        run_with_debug_terminal(self, project, task_id, app)

    def _show_launch_error_dialog(self, message: str, detail: str) -> None:
        script_path = os.path.join(
            APPLICATIONS_ADDON_ROOT, "ui", "launch_failed_dialog.py"
        )
        with tempfile.NamedTemporaryFile("w", delete=False) as tmp:
            tmp_path = tmp.name
            json.dump(
                {"message": message, "detail": detail},
                tmp.file
            )

        try:
            run_ayon_launcher_process(
                "--skip-bootstrap",
                script_path,
                tmp_path,
                add_sys_paths=True,
                creationflags=0,
            )

        finally:
            os.remove(tmp_path)

delete_custom_icon(filename) classmethod

Delete custom icon from AYON server.

Parameters:

Name Type Description Default
filename str

Icon filename which will be deleted from the server.

required
Source code in client/ayon_applications/addon.py
229
230
231
232
233
234
235
236
237
238
239
240
@classmethod
def delete_custom_icon(cls, filename: str) -> None:
    """Delete custom icon from AYON server.

    Args:
        filename (str): Icon filename which will be deleted
            from the server.

    """
    endpoint = f"addons/{cls.name}/{cls.version}/customIcons/{filename}"
    response = ayon_api.delete(endpoint)
    response.raise_for_status()

get_app_environments_for_context(project_name, folder_path, task_name, full_app_name, env_group=None, launch_type=None, env=None)

Calculate environment variables for launch context.

Parameters:

Name Type Description Default
project_name str

Project name.

required
folder_path str

Folder path.

required
task_name str

Task name.

required
full_app_name str

Full application name.

required
env_group Optional[str]

Environment group.

None
launch_type Optional[str]

Launch type.

None
env Optional[dict[str, str]]

Environment variables to update.

None

Returns:

Type Description
dict[str, str]

dict[str, str]: Environment variables for context.

Source code in client/ayon_applications/addon.py
 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
def get_app_environments_for_context(
    self,
    project_name: str,
    folder_path: str,
    task_name: str,
    full_app_name: str,
    env_group: Optional[str] = None,
    launch_type: Optional[str] = None,
    env: Optional[dict[str, str]] = None,
) -> dict[str, str]:
    """Calculate environment variables for launch context.

    Args:
        project_name (str): Project name.
        folder_path (str): Folder path.
        task_name (str): Task name.
        full_app_name (str): Full application name.
        env_group (Optional[str]): Environment group.
        launch_type (Optional[str]): Launch type.
        env (Optional[dict[str, str]]): Environment variables to update.

    Returns:
        dict[str, str]: Environment variables for context.

    """
    from ayon_applications.utils import get_app_environments_for_context

    if not full_app_name:
        return {}

    return get_app_environments_for_context(
        project_name,
        folder_path,
        task_name,
        full_app_name,
        env_group=env_group,
        launch_type=launch_type,
        env=env,
        addons_manager=self.manager
    )

get_app_icon_path(icon_filename)

DEPRECATED Get icon path.

Parameters:

Name Type Description Default
icon_filename str

Icon filename.

required

Returns:

Type Description
str

Optional[str]: Icon path or None if not found.

Source code in client/ayon_applications/addon.py
183
184
185
186
187
188
189
190
191
192
193
def get_app_icon_path(self, icon_filename: str) -> str:
    """DEPRECATED Get icon path.

    Args:
        icon_filename (str): Icon filename.

    Returns:
        Optional[str]: Icon path or None if not found.

    """
    return get_app_icon_path(icon_filename)

get_app_icon_url(icon, server=False) classmethod

Get icon path.

icon filename can be either a full URL (http/https/file/...) or a bare filename. Full URLs are used as is while bare filenames resolve to the addons icons folder.

Method does not validate if icon filename exist on server.

Parameters:

Name Type Description Default
icon dict[str, Any] | str

Icon name.

required
server bool

Return url to AYON server.

False

Returns:

Type Description
str | None

str | None: Icon path or None is server url is not available.

Source code in client/ayon_applications/addon.py
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
@classmethod
def get_app_icon_url(
    cls, icon: dict[str, Any] | str, server: bool = False
) -> str | None:
    """Get icon path.

    icon filename can be either a full URL (http/https/file/...)
    or a bare filename. Full URLs are used as is while bare filenames
    resolve to the addons icons folder.

    Method does not validate if icon filename exist on server.

    Args:
        icon (dict[str, Any] | str): Icon name.
        server (bool): Return url to AYON server.

    Returns:
        str | None: Icon path or None is server url is not
            available.

    """
    if not icon:
        return None

    if isinstance(icon, str):
        icon_filename = icon
    elif isinstance(icon, dict):
        # NOTE At this moment the url always leads to addon's icons
        #   endpoint and last part of path is filename
        url = icon.get("url")
        if not isinstance(url, str):
            return None
        icon_filename = os.path.basename(url)

    else:
        return None

    # check if its a full URL
    try:
        url = urllib.parse.urlparse(icon_filename)
        if url.scheme:
            return icon_filename
    except Exception:
        pass

    icon_name = os.path.basename(icon_filename)
    if server:
        base_url = ayon_api.get_base_url()
        return (
            f"{base_url}/api/addons/{cls.name}/{cls.version}"
            f"/icons/{icon_name}"
        )
    server_url = os.getenv("AYON_WEBSERVER_URL")
    if not server_url:
        return None
    return "/".join([
        server_url, "addons", cls.name, "icons", icon_name
    ])

get_application_items(project_name=None, task_id=None, *, variant=None, version=None) classmethod

Get application items.

This is meant as api for other addons to get application items for a given context. Can also filter applications for a specific task.

It does handle project bundles and settings variant automatically.

Parameters:

Name Type Description Default
project_name str | None

Project name.

None
task_id str | None

Task id for which applications are fitlered.

None
variant str | None

Settings variant. Current settings variant is used if not passed in.

None
version str | None

Specific version of applications addon to get items for. If None, it will use the version resolved for current context (variant and project).

None

Example application dict (may vary based on applications addon version): { "host_name": str "full_name": str "full_label": str "group_label": str "variant_label": str "icon": dict[str, str] | None "show_grouped": bool }

Returns:

Type Description
list[dict[str, Any]]

list[dict]: Application items.

Source code in client/ayon_applications/addon.py
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
@classmethod
def get_application_items(
    cls,
    project_name: str | None = None,
    task_id: str | None = None,
    *,
    variant: str | None = None,
    version: str | None = None,
) -> list[dict[str, Any]]:
    """Get application items.

    This is meant as api for other addons to get application items for
        a given context. Can also filter applications for a specific task.

    It does handle project bundles and settings variant automatically.

    Args:
        project_name (str | None): Project name.
        task_id (str | None): Task id for which applications are fitlered.
        variant (str | None): Settings variant. Current settings variant
            is used if not passed in.
        version (str | None): Specific version of applications addon
            to get items for. If None, it will use the version
            resolved for current context (variant and project).

    Example application dict (may vary based on applications
        addon version):
        {
            "host_name": str
            "full_name": str
            "full_label": str
            "group_label": str
            "variant_label": str
            "icon": dict[str, str] | None
            "show_grouped": bool
        }

    Returns:
        list[dict]: Application items.

    """
    if variant is None:
        variant = get_settings_variant()

    query_params = {"variant": variant}
    if version is not None:
        query_params["version"] = version

    query = urllib.parse.urlencode(query_params)
    context_path = ""
    if project_name:
        context_path = f"/{project_name}"
        if task_id:
            context_path = f"{context_path}/task/{task_id}"

    response = ayon_api.get(
        f"addons/{cls.name}/{cls.version}/"
        f"apps{context_path}?{query}"
    )
    app_items = response.data["applications"]

    # Fill icon urls with 'addon_url' and prepare icon definitions
    if not version:
        version = cls.version
    addon_url = f"/addons/{cls.name}/{version}"

    for app_item in app_items:
        icon = app_item["icon"]
        if not icon:
            continue
        try:
            url = icon["url"].format(addon_url=addon_url)
        except Exception:
            continue
        app_item["icon"] = {
            "type": "ayon_url",
            "url": url.lstrip("/"),
        }
    return app_items

get_applications_manager(settings=None)

Get applications manager.

Parameters:

Name Type Description Default
settings Optional[dict]

Studio/project settings.

None

Returns:

Name Type Description
ApplicationManager 'ApplicationManager'

Applications manager.

Source code in client/ayon_applications/addon.py
151
152
153
154
155
156
157
158
159
160
161
162
163
def get_applications_manager(
    self, settings: Optional[dict[str, Any]] = None
) -> "ApplicationManager":
    """Get applications manager.

    Args:
        settings (Optional[dict]): Studio/project settings.

    Returns:
        ApplicationManager: Applications manager.

    """
    return ApplicationManager(settings)

get_custom_icons_info() classmethod

List custom icons available on the server.

Returns:

Type Description
list[dict[str, str]]

list[dict[str, str]]: List of custom icons.

Source code in client/ayon_applications/addon.py
195
196
197
198
199
200
201
202
203
204
205
206
@classmethod
def get_custom_icons_info(cls) -> list[dict[str, str]]:
    """List custom icons available on the server.

    Returns:
        list[dict[str, str]]: List of custom icons.

    """
    endpoint = f"addons/{cls.name}/{cls.version}/customIcons"
    response = ayon_api.get(endpoint)
    response.raise_for_status()
    return response.data["icons"]

get_farm_publish_environment_variables(project_name, folder_path, task_name, full_app_name=None, env_group=None)

Calculate environment variables for farm publish.

Parameters:

Name Type Description Default
project_name str

Project name.

required
folder_path str

Folder path.

required
task_name str

Task name.

required
env_group Optional[str]

Environment group.

None
full_app_name Optional[str]

Full application name. Value from environment variable 'AYON_APP_NAME' is used if 'None' is passed.

None

Returns:

Type Description
dict[str, str]

dict[str, str]: Environment variables for farm publish.

Source code in client/ayon_applications/addon.py
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
def get_farm_publish_environment_variables(
    self,
    project_name: str,
    folder_path: str,
    task_name: str,
    full_app_name: Optional[str] = None,
    env_group: Optional[str] = None,
) -> dict[str, str]:
    """Calculate environment variables for farm publish.

    Args:
        project_name (str): Project name.
        folder_path (str): Folder path.
        task_name (str): Task name.
        env_group (Optional[str]): Environment group.
        full_app_name (Optional[str]): Full application name. Value from
            environment variable 'AYON_APP_NAME' is used if 'None' is
            passed.

    Returns:
        dict[str, str]: Environment variables for farm publish.

    """
    if full_app_name is None:
        full_app_name = os.getenv("AYON_APP_NAME")

    return self.get_app_environments_for_context(
        project_name,
        folder_path,
        task_name,
        full_app_name,
        env_group=env_group,
        launch_type=LaunchTypes.farm_publish
    )

get_tool_items(project_name=None, *, variant=None, version=None) classmethod

Get tool items.

This is meant as api for other addons to get tools items for a given context.

It does handle project bundles and settings variant automatically.

Parameters:

Name Type Description Default
project_name str | None

Project name.

None
variant str | None

Settings variant. Current settings variant is used if not passed in.

None
version str | None

Specific version of applications addon to get items for. If None, it will use the version resolved for current context (variant and project).

None

Example tool dict (may vary based on applications addon version): { "full_name": str, "full_label": str, "group_label": str, "variant_label": str, "host_names": list[str], "app_variants": list[str], }

Returns:

Type Description
list[dict[str, Any]]

list[dict]: Tool items.

Source code in client/ayon_applications/addon.py
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
@classmethod
def get_tool_items(
    cls,
    project_name: str | None = None,
    *,
    variant: str | None = None,
    version: str | None = None,
) -> list[dict[str, Any]]:
    """Get tool items.

    This is meant as api for other addons to get tools items for a given
        context.

    It does handle project bundles and settings variant automatically.

    Args:
        project_name (str | None): Project name.
        variant (str | None): Settings variant. Current settings variant
            is used if not passed in.
        version (str | None): Specific version of applications addon
            to get items for. If None, it will use the version
            resolved for current context (variant and project).

    Example tool dict (may vary based on applications addon version):
        {
            "full_name": str,
            "full_label": str,
            "group_label": str,
            "variant_label": str,
            "host_names": list[str],
            "app_variants": list[str],
        }

    Returns:
        list[dict]: Tool items.

    """
    if variant is None:
        variant = get_settings_variant()

    query_params = {"variant": variant}
    if version is not None:
        query_params["version"] = version

    query = urllib.parse.urlencode(query_params)

    context_path = ""
    if project_name:
        context_path = f"/{project_name}"

    response = ayon_api.get(
        f"addons/{cls.name}/{cls.version}/"
        f"tools{context_path}?{query}"
    )
    return response.data["applications"]

launch_application(app_name, project_name, folder_path, task_name, workfile_path=None, use_last_workfile=None)

Launch application.

Parameters:

Name Type Description Default
app_name str

Full application name e.g. 'maya/2024'.

required
project_name str

Project name.

required
folder_path str

Folder path.

required
task_name str

Task name.

required
workfile_path Optional[str]

Workfile path to use.

None
use_last_workfile Optional[bool]

Explicitly tell to use or not use last workfile. Ignored if 'workfile_path' is passed.

None
Source code in client/ayon_applications/addon.py
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
def launch_application(
    self,
    app_name: str,
    project_name: str,
    folder_path: str,
    task_name: str,
    workfile_path: Optional[str] = None,
    use_last_workfile: Optional[bool] = None,
):
    """Launch application.

    Args:
        app_name (str): Full application name e.g. 'maya/2024'.
        project_name (str): Project name.
        folder_path (str): Folder path.
        task_name (str): Task name.
        workfile_path (Optional[str]): Workfile path to use.
        use_last_workfile (Optional[bool]): Explicitly tell to use or
            not use last workfile. Ignored if 'workfile_path' is passed.

    """
    ensure_addons_are_process_ready(
        addon_name=self.name,
        addon_version=self.version,
        project_name=project_name,
    )
    headless = is_headless_mode_enabled()

    data = {
        "project_name": project_name,
        "folder_path": folder_path,
        "task_name": task_name,
    }
    # Backwards compatibility 'workfile_path' was added
    #   before 'use_last_workfile'
    if isinstance(workfile_path, bool):
        use_last_workfile = workfile_path
        workfile_path = None
        warnings.warn(
            "Passed 'use_last_workfile' as positional argument."
            " Use explicit 'use_last_workfile' keyword argument instead.",
            DeprecationWarning,
            stacklevel=2,
        )

    if workfile_path:
        data["workfile_path"] = workfile_path
        # Backwards compatibility to be able to use 'workfile_path'
        #   argument with older ayon-core
        # use_last_workfile = False
        data["last_workfile_path"] = workfile_path
        data["start_last_workfile"] = True

    elif use_last_workfile is not None:
        data["start_last_workfile"] = use_last_workfile

    # TODO handle raise errors
    failed = True
    message = None
    detail = None
    try:
        app_manager = self.get_applications_manager()
        app_manager.launch(app_name, **data)
        failed = False

    except (
        ApplicationLaunchFailed,
        ApplicationExecutableNotFound,
        ApplicationNotFound,
    ) as exc:
        message = str(exc)
        self.log.warning(f"Application launch failed: {message}")

    except Exception as exc:
        message = "An unexpected error happened"
        detail = "".join(traceback.format_exception(*sys.exc_info()))
        self.log.warning(
            f"Application launch failed: {str(exc)}",
            exc_info=True
        )

    if not failed:
        return

    if not headless:
        self._show_launch_error_dialog(message, detail)
    sys.exit(1)

on_action_trigger()

Action triggered when the tray icon is clicked.

Source code in client/ayon_applications/addon.py
63
64
65
66
67
68
69
70
71
72
73
def on_action_trigger(self) -> None:
    """Action triggered when the tray icon is clicked."""
    from ayon_applications.ui.process_monitor import (
        ProcessMonitorWindow,
    )
    if self._process_monitor_window is None:
        self._process_monitor_window = ProcessMonitorWindow()

    self._process_monitor_window.show()
    self._process_monitor_window.raise_()
    self._process_monitor_window.activateWindow()

tray_init()

Initialize the tray action.

Source code in client/ayon_applications/addon.py
59
60
61
def tray_init(self) -> None:
    """Initialize the tray action."""
    self._process_monitor_window: Optional[ProcessMonitorWindow] = None

upload_custom_icon(path, filename=None) classmethod

Upload custom icon to AYON server.

Parameters:

Name Type Description Default
path str

Path to icon file.

required
filename str | None

Icon filename which will be used to store the icon on the server. This value is then used in settings.

None
Source code in client/ayon_applications/addon.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
@classmethod
def upload_custom_icon(
    cls, path: str, filename: str | None = None
) -> None:
    """Upload custom icon to AYON server.

    Args:
        path (str): Path to icon file.
        filename (str | None): Icon filename which will be used
            to store the icon on the server. This value is then used in
            settings.

    """
    if filename is None:
        filename = os.path.basename(path)
    endpoint = f"addons/{cls.name}/{cls.version}/customIcons/{filename}"
    response = ayon_api.upload_file(
        endpoint, path
    )
    response.raise_for_status()

webserver_initialization(manager)

Initialize webserver.

Add localhost handler for icons requests.

This was added for ftrack which is showing icons

Parameters:

Name Type Description Default
manager WebServerManager

Webserver manager.

required
Source code in client/ayon_applications/addon.py
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
def webserver_initialization(self, manager: "WebServerManager") -> None:
    """Initialize webserver.

    Add localhost handler for icons requests.

    This was added for ftrack which is showing icons

    Args:
        manager (WebServerManager): Webserver manager.

    """
    def _cache_icon(filename: str, data: bytes | None) -> None:
        self.__class__._icons_cache[filename] = data
        if len(self.__class__._icons_cache) > 256:
            self.__class__._icons_cache.pop(
                next(iter(self.__class__._icons_cache))
            )

    async def _get_web_icon(request):
        from aiohttp import web, ClientSession

        filename: str = os.path.basename(request.match_info["filename"])
        # TODO find better way how to cache
        if filename not in self.__class__._icons_cache:
            base_url = ayon_api.get_base_url()
            url = (
                f"{base_url}/api/addons/{self.name}/{self.version}"
                f"/icons/{filename}"
            )
            data = None
            async with ClientSession() as session:
                async with session.get(url) as resp:
                    if resp.status != 200:
                        data = await resp.read()

            _cache_icon(filename, data)

        body = self.__class__._icons_cache[filename]
        if body is None:
            raise web.HTTPNotFound()
        return web.Response(body=body)

    manager.add_addon_route(
        self.name,
        "/icons/{filename}",
        "GET",
        _get_web_icon,
    )

EnvironmentTool

Hold information about application tool.

Structure of tool information.

Parameters:

Name Type Description Default
variant_data dict

Variant data with environments and host and app variant filters.

required
group EnvironmentToolGroup

Name of group which wraps tool.

required
Source code in client/ayon_applications/defs.py
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
class EnvironmentTool:
    """Hold information about application tool.

    Structure of tool information.

    Args:
        variant_data (dict): Variant data with environments and
            host and app variant filters.
        group (EnvironmentToolGroup): Name of group which wraps tool.

    """
    def __init__(
        self,
        variant_data: dict[str, Any],
        group: EnvironmentToolGroup,
    ):
        # Backwards compatibility 3.9.1 - 3.9.2
        # - 'variant_data' contained only environments but contain also host
        #   and application variant filters
        name = variant_data["name"]
        label = variant_data["label"]
        host_names = variant_data["host_names"]
        app_variants = variant_data["app_variants"]

        environment = {}
        try:
            environment = json.loads(variant_data["environment"])
        except Exception:
            pass

        self.host_names = host_names
        self.app_variants = app_variants
        self.name = name
        self.variant_label = label
        self.label = " ".join((group.label, label))
        self.group = group

        self._environment = environment
        self.full_name = "/".join((group.name, name))

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}> - {self.full_name}"

    @property
    def environment(self) -> dict[str, str]:
        return copy.deepcopy(self._environment)

    def is_valid_for_app(self, app: Application) -> bool:
        """Is tool valid for an application.

        Args:
            app (Application): Application for which are prepared environments.

        """
        if self.app_variants and app.full_name not in self.app_variants:
            return False

        if self.host_names and app.host_name not in self.host_names:
            return False
        return True

is_valid_for_app(app)

Is tool valid for an application.

Parameters:

Name Type Description Default
app Application

Application for which are prepared environments.

required
Source code in client/ayon_applications/defs.py
422
423
424
425
426
427
428
429
430
431
432
433
434
def is_valid_for_app(self, app: Application) -> bool:
    """Is tool valid for an application.

    Args:
        app (Application): Application for which are prepared environments.

    """
    if self.app_variants and app.full_name not in self.app_variants:
        return False

    if self.host_names and app.host_name not in self.host_names:
        return False
    return True

EnvironmentToolGroup

Hold information about environment tool group.

Environment tool group may hold different variants of same tool and set environments that are same for all of them.

e.g. "mtoa" may have different versions but all environments except one are same.

Parameters:

Name Type Description Default
data dict

Group information with variants.

required
manager ApplicationManager

Manager that creates the group.

required
Source code in client/ayon_applications/defs.py
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
class EnvironmentToolGroup:
    """Hold information about environment tool group.

    Environment tool group may hold different variants of same tool and set
    environments that are same for all of them.

    e.g. "mtoa" may have different versions but all environments except one
        are same.

    Args:
        data (dict): Group information with variants.
        manager (ApplicationManager): Manager that creates the group.

    """
    def __init__(
        self,
        data: dict[str, Any],
        manager: "ApplicationManager",
    ):
        name = data["name"]
        label = data["label"]

        self.name = name
        self.label = label
        self._data = data
        self.manager = manager

        environment = {}
        try:
            environment = json.loads(data["environment"])
        except Exception:
            pass
        self._environment = environment

        variants = data.get("variants") or []
        variants_by_name = {}
        for variant_data in variants:
            tool = EnvironmentTool(variant_data, self)
            variants_by_name[tool.name] = tool
        self.variants = variants_by_name

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}> - {self.name}"

    def __iter__(self) -> Generator["EnvironmentTool", None, None]:
        for variant in self.variants.values():
            yield variant

    @property
    def environment(self) -> dict[str, str]:
        return copy.deepcopy(self._environment)

LaunchTypes

Launch types are filters for pre/post-launch hooks.

Please use these variables in case they'll change values.

Source code in client/ayon_applications/defs.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class LaunchTypes:
    """Launch types are filters for pre/post-launch hooks.

    Please use these variables in case they'll change values.
    """

    # Local launch - application is launched on local machine
    local = "local"
    # Farm render job - application is on farm
    farm_render = "farm-render"
    # Farm publish job - integration post-render job
    farm_publish = "farm-publish"
    # Remote launch - application is launched on remote machine from which
    #     can be started publishing
    remote = "remote"
    # Automated launch - application is launched with automated publishing
    automated = "automated"

UndefinedApplicationExecutable

Bases: ApplicationExecutable

Some applications do not require executable path from settings.

In that case this class is used to "fake" existing executable.

Source code in client/ayon_applications/defs.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
class UndefinedApplicationExecutable(ApplicationExecutable):
    """Some applications do not require executable path from settings.

    In that case this class is used to "fake" existing executable.
    """
    def __init__(self):
        pass

    def __str__(self) -> str:
        return self.__class__.__name__

    def __repr__(self) -> str:
        return f"<{self.__class__.__name__}>"

    def as_args(self) -> list[str]:
        return []

    def exists(self) -> bool:
        return True