Skip to content

host

AbstractHost

Bases: ABC

Abstract definition of host implementation.

Source code in client/ayon_core/host/abstract.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class AbstractHost(ABC):
    """Abstract definition of host implementation."""
    @property
    @abstractmethod
    def log(self) -> logging.Logger:
        pass

    @property
    @abstractmethod
    def name(self) -> str:
        """Host name."""
        pass

    @abstractmethod
    def get_app_information(self) -> ApplicationInformation:
        """Information about the application where host is running.

        Returns:
            ApplicationInformation: Application information.

        """
        pass

    @abstractmethod
    def get_current_context(self) -> HostContextData:
        """Get the current context of the host.

        Current context is defined by project name, folder path and task name.

        Returns:
            HostContextData: The current context of the host.

        """
        pass

    @abstractmethod
    def set_current_context(
        self,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        reason: ContextChangeReason = ContextChangeReason.undefined,
        project_entity: Optional[dict[str, Any]] = None,
        anatomy: Optional[Anatomy] = None,
    ) -> HostContextData:
        """Change context of the host.

        Args:
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            reason (ContextChangeReason): Reason for change.
            project_entity (dict[str, Any]): Project entity.
            anatomy (Anatomy): Anatomy entity.

        """
        pass

    @abstractmethod
    def get_current_project_name(self) -> str:
        """Get the current project name.

        Returns:
            Optional[str]: The current project name.

        """
        pass

    @abstractmethod
    def get_current_folder_path(self) -> Optional[str]:
        """Get the current folder path.

        Returns:
            Optional[str]: The current folder path.

        """
        pass

    @abstractmethod
    def get_current_task_name(self) -> Optional[str]:
        """Get the current task name.

        Returns:
            Optional[str]: The current task name.

        """
        pass

    @abstractmethod
    def get_context_title(self) -> str:
        """Get the context title used in UIs."""
        pass

name abstractmethod property

Host name.

get_app_information() abstractmethod

Information about the application where host is running.

Returns:

Name Type Description
ApplicationInformation ApplicationInformation

Application information.

Source code in client/ayon_core/host/abstract.py
43
44
45
46
47
48
49
50
51
@abstractmethod
def get_app_information(self) -> ApplicationInformation:
    """Information about the application where host is running.

    Returns:
        ApplicationInformation: Application information.

    """
    pass

get_context_title() abstractmethod

Get the context title used in UIs.

Source code in client/ayon_core/host/abstract.py
117
118
119
120
@abstractmethod
def get_context_title(self) -> str:
    """Get the context title used in UIs."""
    pass

get_current_context() abstractmethod

Get the current context of the host.

Current context is defined by project name, folder path and task name.

Returns:

Name Type Description
HostContextData HostContextData

The current context of the host.

Source code in client/ayon_core/host/abstract.py
53
54
55
56
57
58
59
60
61
62
63
@abstractmethod
def get_current_context(self) -> HostContextData:
    """Get the current context of the host.

    Current context is defined by project name, folder path and task name.

    Returns:
        HostContextData: The current context of the host.

    """
    pass

get_current_folder_path() abstractmethod

Get the current folder path.

Returns:

Type Description
Optional[str]

Optional[str]: The current folder path.

Source code in client/ayon_core/host/abstract.py
 97
 98
 99
100
101
102
103
104
105
@abstractmethod
def get_current_folder_path(self) -> Optional[str]:
    """Get the current folder path.

    Returns:
        Optional[str]: The current folder path.

    """
    pass

get_current_project_name() abstractmethod

Get the current project name.

Returns:

Type Description
str

Optional[str]: The current project name.

Source code in client/ayon_core/host/abstract.py
87
88
89
90
91
92
93
94
95
@abstractmethod
def get_current_project_name(self) -> str:
    """Get the current project name.

    Returns:
        Optional[str]: The current project name.

    """
    pass

get_current_task_name() abstractmethod

Get the current task name.

Returns:

Type Description
Optional[str]

Optional[str]: The current task name.

Source code in client/ayon_core/host/abstract.py
107
108
109
110
111
112
113
114
115
@abstractmethod
def get_current_task_name(self) -> Optional[str]:
    """Get the current task name.

    Returns:
        Optional[str]: The current task name.

    """
    pass

set_current_context(folder_entity, task_entity, *, reason=ContextChangeReason.undefined, project_entity=None, anatomy=None) abstractmethod

Change context of the host.

Parameters:

Name Type Description Default
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
reason ContextChangeReason

Reason for change.

undefined
project_entity dict[str, Any]

Project entity.

None
anatomy Anatomy

Anatomy entity.

None
Source code in client/ayon_core/host/abstract.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@abstractmethod
def set_current_context(
    self,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    reason: ContextChangeReason = ContextChangeReason.undefined,
    project_entity: Optional[dict[str, Any]] = None,
    anatomy: Optional[Anatomy] = None,
) -> HostContextData:
    """Change context of the host.

    Args:
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        reason (ContextChangeReason): Reason for change.
        project_entity (dict[str, Any]): Project entity.
        anatomy (Anatomy): Anatomy entity.

    """
    pass

ApplicationInformation dataclass

Application information.

Attributes:

Name Type Description
app_name Optional[str]

Application name. e.g. Maya, NukeX, Nuke

app_version Optional[str]

Application version. e.g. 15.2.1

Source code in client/ayon_core/host/abstract.py
17
18
19
20
21
22
23
24
25
26
27
@dataclass
class ApplicationInformation:
    """Application information.

    Attributes:
        app_name (Optional[str]): Application name. e.g. Maya, NukeX, Nuke
        app_version (Optional[str]): Application version. e.g. 15.2.1

    """
    app_name: Optional[str] = None
    app_version: Optional[str] = None

ContextChangeReason

Bases: StrEnum

Reasons for context change in the host.

Source code in client/ayon_core/host/constants.py
11
12
13
14
15
class ContextChangeReason(StrEnum):
    """Reasons for context change in the host."""
    undefined = "undefined"
    workfile_open = "workfile.opened"
    workfile_save = "workfile.saved"

HostBase

Bases: AbstractHost

Base of host implementation class.

Host is pipeline implementation of DCC application. This class should help to identify what must/should/can be implemented for specific functionality.

Compared to 'avalon' concept: What was before considered as functions in host implementation folder. The host implementation should primarily care about adding ability of creation (mark products to be published) and optionally about referencing published representations as containers.

Host may need extend some functionality like working with workfiles or loading. Not all host implementations may allow that for those purposes can be logic extended with implementing functions for the purpose. There are prepared interfaces to be able identify what must be implemented to be able use that functionality. - current statement is that it is not required to inherit from interfaces but all of the methods are validated (only their existence!)

Installation of host before (avalon concept):

from ayon_core.pipeline import install_host
import ayon_core.hosts.maya.api as host

install_host(host)

Installation of host now:

from ayon_core.pipeline import install_host
from ayon_core.hosts.maya.api import MayaHost

host = MayaHost()
install_host(host)
Todo
  • move content of 'install_host' as method of this class
    • register host object
    • install global plugin paths
  • store registered plugin paths to this object
  • handle current context (project, asset, task)
    • this must be done in many separated steps
  • have it's object of host tools instead of using globals

This implementation will probably change over time when more functionality and responsibility will be added.

Source code in client/ayon_core/host/host.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
class HostBase(AbstractHost):
    """Base of host implementation class.

    Host is pipeline implementation of DCC application. This class should help
    to identify what must/should/can be implemented for specific functionality.

    Compared to 'avalon' concept:
    What was before considered as functions in host implementation folder. The
    host implementation should primarily care about adding ability of creation
    (mark products to be published) and optionally about referencing published
    representations as containers.

    Host may need extend some functionality like working with workfiles
    or loading. Not all host implementations may allow that for those purposes
    can be logic extended with implementing functions for the purpose. There
    are prepared interfaces to be able identify what must be implemented to
    be able use that functionality.
    - current statement is that it is not required to inherit from interfaces
        but all of the methods are validated (only their existence!)

    # Installation of host before (avalon concept):
    ```python
    from ayon_core.pipeline import install_host
    import ayon_core.hosts.maya.api as host

    install_host(host)
    ```

    # Installation of host now:
    ```python
    from ayon_core.pipeline import install_host
    from ayon_core.hosts.maya.api import MayaHost

    host = MayaHost()
    install_host(host)
    ```

    Todo:
        - move content of 'install_host' as method of this class
            - register host object
            - install global plugin paths
        - store registered plugin paths to this object
        - handle current context (project, asset, task)
            - this must be done in many separated steps
        - have it's object of host tools instead of using globals

    This implementation will probably change over time when more
        functionality and responsibility will be added.
    """

    _log = None

    def __init__(self):
        """Initialization of host.

        Register DCC callbacks, host specific plugin paths, targets etc.
        (Part of what 'install' did in 'avalon' concept.)

        Note:
            At this moment global "installation" must happen before host
            installation. Because of this current limitation it is recommended
            to implement 'install' method which is triggered after global
            'install'.
        """

        pass

    def get_app_information(self) -> ApplicationInformation:
        """Running application information.

        Host integration should override this method and return correct
            information.

        Returns:
            ApplicationInformation: Application information.

        """
        return ApplicationInformation()

    def install(self):
        """Install host specific functionality.

        This is where should be added menu with tools, registered callbacks
        and other host integration initialization.

        It is called automatically when 'ayon_core.pipeline.install_host' is
        triggered.

        """
        pass

    @property
    def log(self) -> logging.Logger:
        if self._log is None:
            self._log = logging.getLogger(self.__class__.__name__)
        return self._log

    def get_current_project_name(self) -> str:
        """
        Returns:
            str: Current project name.

        """
        return os.environ["AYON_PROJECT_NAME"]

    def get_current_folder_path(self) -> Optional[str]:
        """
        Returns:
            Optional[str]: Current asset name.

        """
        return os.environ.get("AYON_FOLDER_PATH")

    def get_current_task_name(self) -> Optional[str]:
        """
        Returns:
            Optional[str]: Current task name.

        """
        return os.environ.get("AYON_TASK_NAME")

    def get_current_context(self) -> HostContextData:
        """Get current context information.

        This method should be used to get current context of host. Usage of
        this method can be crucial for host implementations in DCCs where
        can be opened multiple workfiles at one moment and change of context
        can't be caught properly.

        Returns:
            HostContextData: Current context with 'project_name',
                'folder_path' and 'task_name'.

        """
        return {
            "project_name": self.get_current_project_name(),
            "folder_path": self.get_current_folder_path(),
            "task_name": self.get_current_task_name()
        }

    def set_current_context(
        self,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        reason: ContextChangeReason = ContextChangeReason.undefined,
        project_entity: Optional[dict[str, Any]] = None,
        anatomy: Optional[Anatomy] = None,
    ) -> HostContextData:
        """Set current context information.

        This method should be used to set current context of host. Usage of
        this method can be crucial for host implementations in DCCs where
        can be opened multiple workfiles at one moment and change of context
        can't be caught properly.

        Notes:
            This method should not care about change of workdir and expect any
                of the arguments.

        Args:
            folder_entity (Optional[dict[str, Any]]): Folder entity.
            task_entity (Optional[dict[str, Any]]): Task entity.
            reason (ContextChangeReason): Reason for context change.
            project_entity (Optional[dict[str, Any]]): Project entity data.
            anatomy (Optional[Anatomy]): Anatomy instance for the project.

        Returns:
            dict[str, Optional[str]]: Context information with project name,
                folder path and task name.

        """
        from ayon_core.pipeline import Anatomy

        folder_path = folder_entity["path"]
        task_name = task_entity["name"]

        context = self.get_current_context()
        # Don't do anything if context did not change
        if (
            context["folder_path"] == folder_path
            and context["task_name"] == task_name
        ):
            return context

        project_name = self.get_current_project_name()
        if project_entity is None:
            project_entity = ayon_api.get_project(project_name)

        if anatomy is None:
            anatomy = Anatomy(project_name, project_entity=project_entity)

        context_change_data = ContextChangeData(
            project_entity,
            folder_entity,
            task_entity,
            reason,
            anatomy,
        )
        self._before_context_change(context_change_data)
        self._set_current_context(context_change_data)
        self._after_context_change(context_change_data)

        return self._emit_context_change_event(
            project_name,
            folder_path,
            task_name,
        )

    def get_context_title(self):
        """Context title shown for UI purposes.

        Should return current context title if possible.

        Note:
            This method is used only for UI purposes so it is possible to
                return some logical title for contextless cases.
            Is not meant for "Context menu" label.

        Returns:
            str: Context title.
            None: Default title is used based on UI implementation.
        """

        # Use current context to fill the context title
        current_context = self.get_current_context()
        project_name = current_context["project_name"]
        folder_path = current_context["folder_path"]
        task_name = current_context["task_name"]
        items = []
        if project_name:
            items.append(project_name)
            if folder_path:
                items.append(folder_path.lstrip("/"))
                if task_name:
                    items.append(task_name)
        if items:
            return "/".join(items)
        return None

    @contextlib.contextmanager
    def maintained_selection(self):
        """Some functionlity will happen but selection should stay same.

        This is DCC specific. Some may not allow to implement this ability
        that is reason why default implementation is empty context manager.

        Yields:
            None: Yield when is ready to restore selected at the end.
        """

        try:
            yield
        finally:
            pass

    def _emit_context_change_event(
        self,
        project_name: str,
        folder_path: Optional[str],
        task_name: Optional[str],
    ) -> HostContextData:
        """Emit context change event.

        Args:
            project_name (str): Name of the project.
            folder_path (Optional[str]): Path of the folder.
            task_name (Optional[str]): Name of the task.

        Returns:
            HostContextData: Data send to context change event.

        """
        data: HostContextData = {
            "project_name": project_name,
            "folder_path": folder_path,
            "task_name": task_name,
        }
        emit_event("taskChanged", data)
        return data

    def _set_current_context(
        self, context_change_data: ContextChangeData
    ) -> None:
        """Method that changes the context in host.

        Can be overriden for hosts that do need different handling of context
            than using environment variables.

        Args:
            context_change_data (ContextChangeData): Context change related
                data.

        """
        project_name = self.get_current_project_name()
        folder_path = None
        task_name = None
        if context_change_data.folder_entity:
            folder_path = context_change_data.folder_entity["path"]
            if context_change_data.task_entity:
                task_name = context_change_data.task_entity["name"]

        envs = {
            "AYON_PROJECT_NAME": project_name,
            "AYON_FOLDER_PATH": folder_path,
            "AYON_TASK_NAME": task_name,
        }

        # Update the Session and environments. Pop from environments all
        #   keys with value set to None.
        for key, value in envs.items():
            if value is None:
                os.environ.pop(key, None)
            else:
                os.environ[key] = value

    def _before_context_change(self, context_change_data: ContextChangeData):
        """Before context is changed.

        This method is called before the context is changed in the host.

        Can be overridden to implement host specific logic.

        Args:
            context_change_data (ContextChangeData): Object with information
                about context change.

        """
        pass

    def _after_context_change(self, context_change_data: ContextChangeData):
        """After context is changed.

        This method is called after the context is changed in the host.

        Can be overridden to implement host specific logic.

        Args:
            context_change_data (ContextChangeData): Object with information
                about context change.

        """
        pass

__init__()

Initialization of host.

Register DCC callbacks, host specific plugin paths, targets etc. (Part of what 'install' did in 'avalon' concept.)

Note

At this moment global "installation" must happen before host installation. Because of this current limitation it is recommended to implement 'install' method which is triggered after global 'install'.

Source code in client/ayon_core/host/host.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def __init__(self):
    """Initialization of host.

    Register DCC callbacks, host specific plugin paths, targets etc.
    (Part of what 'install' did in 'avalon' concept.)

    Note:
        At this moment global "installation" must happen before host
        installation. Because of this current limitation it is recommended
        to implement 'install' method which is triggered after global
        'install'.
    """

    pass

get_app_information()

Running application information.

Host integration should override this method and return correct information.

Returns:

Name Type Description
ApplicationInformation ApplicationInformation

Application information.

Source code in client/ayon_core/host/host.py
 99
100
101
102
103
104
105
106
107
108
109
def get_app_information(self) -> ApplicationInformation:
    """Running application information.

    Host integration should override this method and return correct
        information.

    Returns:
        ApplicationInformation: Application information.

    """
    return ApplicationInformation()

get_context_title()

Context title shown for UI purposes.

Should return current context title if possible.

Note

This method is used only for UI purposes so it is possible to return some logical title for contextless cases. Is not meant for "Context menu" label.

Returns:

Name Type Description
str

Context title.

None

Default title is used based on UI implementation.

Source code in client/ayon_core/host/host.py
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
def get_context_title(self):
    """Context title shown for UI purposes.

    Should return current context title if possible.

    Note:
        This method is used only for UI purposes so it is possible to
            return some logical title for contextless cases.
        Is not meant for "Context menu" label.

    Returns:
        str: Context title.
        None: Default title is used based on UI implementation.
    """

    # Use current context to fill the context title
    current_context = self.get_current_context()
    project_name = current_context["project_name"]
    folder_path = current_context["folder_path"]
    task_name = current_context["task_name"]
    items = []
    if project_name:
        items.append(project_name)
        if folder_path:
            items.append(folder_path.lstrip("/"))
            if task_name:
                items.append(task_name)
    if items:
        return "/".join(items)
    return None

get_current_context()

Get current context information.

This method should be used to get current context of host. Usage of this method can be crucial for host implementations in DCCs where can be opened multiple workfiles at one moment and change of context can't be caught properly.

Returns:

Name Type Description
HostContextData HostContextData

Current context with 'project_name', 'folder_path' and 'task_name'.

Source code in client/ayon_core/host/host.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def get_current_context(self) -> HostContextData:
    """Get current context information.

    This method should be used to get current context of host. Usage of
    this method can be crucial for host implementations in DCCs where
    can be opened multiple workfiles at one moment and change of context
    can't be caught properly.

    Returns:
        HostContextData: Current context with 'project_name',
            'folder_path' and 'task_name'.

    """
    return {
        "project_name": self.get_current_project_name(),
        "folder_path": self.get_current_folder_path(),
        "task_name": self.get_current_task_name()
    }

get_current_folder_path()

Returns:

Type Description
Optional[str]

Optional[str]: Current asset name.

Source code in client/ayon_core/host/host.py
137
138
139
140
141
142
143
def get_current_folder_path(self) -> Optional[str]:
    """
    Returns:
        Optional[str]: Current asset name.

    """
    return os.environ.get("AYON_FOLDER_PATH")

get_current_project_name()

Returns:

Name Type Description
str str

Current project name.

Source code in client/ayon_core/host/host.py
129
130
131
132
133
134
135
def get_current_project_name(self) -> str:
    """
    Returns:
        str: Current project name.

    """
    return os.environ["AYON_PROJECT_NAME"]

get_current_task_name()

Returns:

Type Description
Optional[str]

Optional[str]: Current task name.

Source code in client/ayon_core/host/host.py
145
146
147
148
149
150
151
def get_current_task_name(self) -> Optional[str]:
    """
    Returns:
        Optional[str]: Current task name.

    """
    return os.environ.get("AYON_TASK_NAME")

install()

Install host specific functionality.

This is where should be added menu with tools, registered callbacks and other host integration initialization.

It is called automatically when 'ayon_core.pipeline.install_host' is triggered.

Source code in client/ayon_core/host/host.py
111
112
113
114
115
116
117
118
119
120
121
def install(self):
    """Install host specific functionality.

    This is where should be added menu with tools, registered callbacks
    and other host integration initialization.

    It is called automatically when 'ayon_core.pipeline.install_host' is
    triggered.

    """
    pass

maintained_selection()

Some functionlity will happen but selection should stay same.

This is DCC specific. Some may not allow to implement this ability that is reason why default implementation is empty context manager.

Yields:

Name Type Description
None

Yield when is ready to restore selected at the end.

Source code in client/ayon_core/host/host.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
@contextlib.contextmanager
def maintained_selection(self):
    """Some functionlity will happen but selection should stay same.

    This is DCC specific. Some may not allow to implement this ability
    that is reason why default implementation is empty context manager.

    Yields:
        None: Yield when is ready to restore selected at the end.
    """

    try:
        yield
    finally:
        pass

set_current_context(folder_entity, task_entity, *, reason=ContextChangeReason.undefined, project_entity=None, anatomy=None)

Set current context information.

This method should be used to set current context of host. Usage of this method can be crucial for host implementations in DCCs where can be opened multiple workfiles at one moment and change of context can't be caught properly.

Notes

This method should not care about change of workdir and expect any of the arguments.

Parameters:

Name Type Description Default
folder_entity Optional[dict[str, Any]]

Folder entity.

required
task_entity Optional[dict[str, Any]]

Task entity.

required
reason ContextChangeReason

Reason for context change.

undefined
project_entity Optional[dict[str, Any]]

Project entity data.

None
anatomy Optional[Anatomy]

Anatomy instance for the project.

None

Returns:

Type Description
HostContextData

dict[str, Optional[str]]: Context information with project name, folder path and task name.

Source code in client/ayon_core/host/host.py
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
def set_current_context(
    self,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    reason: ContextChangeReason = ContextChangeReason.undefined,
    project_entity: Optional[dict[str, Any]] = None,
    anatomy: Optional[Anatomy] = None,
) -> HostContextData:
    """Set current context information.

    This method should be used to set current context of host. Usage of
    this method can be crucial for host implementations in DCCs where
    can be opened multiple workfiles at one moment and change of context
    can't be caught properly.

    Notes:
        This method should not care about change of workdir and expect any
            of the arguments.

    Args:
        folder_entity (Optional[dict[str, Any]]): Folder entity.
        task_entity (Optional[dict[str, Any]]): Task entity.
        reason (ContextChangeReason): Reason for context change.
        project_entity (Optional[dict[str, Any]]): Project entity data.
        anatomy (Optional[Anatomy]): Anatomy instance for the project.

    Returns:
        dict[str, Optional[str]]: Context information with project name,
            folder path and task name.

    """
    from ayon_core.pipeline import Anatomy

    folder_path = folder_entity["path"]
    task_name = task_entity["name"]

    context = self.get_current_context()
    # Don't do anything if context did not change
    if (
        context["folder_path"] == folder_path
        and context["task_name"] == task_name
    ):
        return context

    project_name = self.get_current_project_name()
    if project_entity is None:
        project_entity = ayon_api.get_project(project_name)

    if anatomy is None:
        anatomy = Anatomy(project_name, project_entity=project_entity)

    context_change_data = ContextChangeData(
        project_entity,
        folder_entity,
        task_entity,
        reason,
        anatomy,
    )
    self._before_context_change(context_change_data)
    self._set_current_context(context_change_data)
    self._after_context_change(context_change_data)

    return self._emit_context_change_event(
        project_name,
        folder_path,
        task_name,
    )

HostDirmap

Bases: ABC

Abstract class for running dirmap on a workfile in a host.

Dirmap is used to translate paths inside of host workfile from one OS to another. (Eg. arstist created workfile on Win, different artists opens same file on Linux.)

Expects methods to be implemented inside of host

on_dirmap_enabled: run host code for enabling dirmap do_dirmap: run host code to do actual remapping

Source code in client/ayon_core/host/dirmap.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class HostDirmap(ABC):
    """Abstract class for running dirmap on a workfile in a host.

    Dirmap is used to translate paths inside of host workfile from one
    OS to another. (Eg. arstist created workfile on Win, different artists
    opens same file on Linux.)

    Expects methods to be implemented inside of host:
        on_dirmap_enabled: run host code for enabling dirmap
        do_dirmap: run host code to do actual remapping
    """

    def __init__(
        self,
        host_name,
        project_name,
        project_settings=None,
        sitesync_addon=None
    ):
        self.host_name = host_name
        self.project_name = project_name
        self._project_settings = project_settings
        self._sitesync_addon = sitesync_addon
        # to limit reinit of Modules
        self._sitesync_addon_discovered = sitesync_addon is not None
        self._log = None

    @property
    def sitesync_addon(self):
        if not self._sitesync_addon_discovered:
            self._sitesync_addon_discovered = True
            manager = AddonsManager()
            self._sitesync_addon = manager.get("sitesync")
        return self._sitesync_addon

    @property
    def project_settings(self):
        if self._project_settings is None:
            self._project_settings = get_project_settings(self.project_name)
        return self._project_settings

    @property
    def log(self):
        if self._log is None:
            self._log = Logger.get_logger(self.__class__.__name__)
        return self._log

    @abstractmethod
    def on_enable_dirmap(self):
        """Run host dependent operation for enabling dirmap if necessary."""
        pass

    @abstractmethod
    def dirmap_routine(self, source_path, destination_path):
        """Run host dependent remapping from source_path to destination_path"""
        pass

    def process_dirmap(self, mapping=None):
        # type: (dict) -> None
        """Go through all paths in Settings and set them using `dirmap`.

            If artists has Site Sync enabled, take dirmap mapping directly from
            Local Settings when artist is syncing workfile locally.

        """

        if not mapping:
            mapping = self.get_mappings()
        if not mapping:
            return

        self.on_enable_dirmap()

        for k, sp in enumerate(mapping["source_path"]):
            dst = mapping["destination_path"][k]
            try:
                # add trailing slash if missing
                sp = os.path.join(sp, '')
                dst = os.path.join(dst, '')
                print("{} -> {}".format(sp, dst))
                self.dirmap_routine(sp, dst)
            except IndexError:
                # missing corresponding destination path
                self.log.error((
                    "invalid dirmap mapping, missing corresponding"
                    " destination directory."
                ))
                break
            except RuntimeError:
                self.log.error(
                    "invalid path {} -> {}, mapping not registered".format(
                        sp, dst
                    )
                )
                continue

    def get_mappings(self):
        """Get translation from source_path to destination_path.

            It checks if Site Sync is enabled and user chose to use local
            site, in that case configuration in Local Settings takes precedence
        """
        mapping_sett = self.project_settings[self.host_name].get("dirmap", {})
        local_mapping = self._get_local_sync_dirmap()
        mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping)
        if not mapping_enabled:
            return {}

        mapping = (
            local_mapping
            or mapping_sett["paths"]
            or {}
        )

        if (
            not mapping
            or not mapping.get("destination_path")
            or not mapping.get("source_path")
        ):
            return {}
        self.log.info("Processing directory mapping ...")
        self.log.info("mapping:: {}".format(mapping))
        return mapping

    def _get_local_sync_dirmap(self):
        """
            Returns dirmap if synch to local project is enabled.

            Only valid mapping is from roots of remote site to local site set
            in Local Settings.

            Returns:
                dict : { "source_path": [XXX], "destination_path": [YYYY]}
        """
        project_name = self.project_name

        sitesync_addon = self.sitesync_addon
        mapping = {}
        if (
            sitesync_addon is None
            or not sitesync_addon.enabled
            or not sitesync_addon.is_project_enabled(project_name, True)
        ):
            return mapping

        active_site = sitesync_addon.get_local_normalized_site(
            sitesync_addon.get_active_site(project_name))
        remote_site = sitesync_addon.get_local_normalized_site(
            sitesync_addon.get_remote_site(project_name))
        self.log.debug(
            "active {} - remote {}".format(active_site, remote_site)
        )

        if active_site == "local" and active_site != remote_site:
            sync_settings = sitesync_addon.get_sync_project_setting(
                project_name,
                exclude_locals=False,
                cached=False)

            # overrides for roots set in `Site Settings`
            active_roots_overrides = self._get_site_root_overrides(
                sitesync_addon, project_name, active_site)

            remote_roots_overrides = self._get_site_root_overrides(
                sitesync_addon, project_name, remote_site)

            current_platform = platform.system().lower()
            remote_provider = sitesync_addon.get_provider_for_site(
                project_name, remote_site
            )
            # dirmap has sense only with regular disk provider, in the workfile
            # won't be root on cloud or sftp provider so fallback to studio
            if remote_provider != "local_drive":
                remote_site = "studio"
            for root_name, active_site_dir in active_roots_overrides.items():
                remote_site_dir = (
                    remote_roots_overrides.get(root_name)
                    or sync_settings["sites"][remote_site]["root"][root_name]
                )

                if isinstance(remote_site_dir, dict):
                    remote_site_dir = remote_site_dir.get(current_platform)

                if not remote_site_dir:
                    continue

                if os.path.isdir(active_site_dir):
                    if "destination_path" not in mapping:
                        mapping["destination_path"] = []
                    mapping["destination_path"].append(active_site_dir)

                    if "source_path" not in mapping:
                        mapping["source_path"] = []
                    mapping["source_path"].append(remote_site_dir)

            self.log.debug("local sync mapping:: {}".format(mapping))
        return mapping

    def _get_site_root_overrides(
        self, sitesync_addon, project_name, site_name
    ):
        """Safely handle root overrides.

        SiteSync raises ValueError for non local or studio sites.
        """
        # TODO: could be removed when `get_site_root_overrides` is not raising
        #     an Error but just returns {}
        try:
            site_roots_overrides = sitesync_addon.get_site_root_overrides(
                project_name, site_name)
        except ValueError:
            site_roots_overrides = {}
        self.log.debug("{} roots overrides {}".format(
            site_name, site_roots_overrides))

        return site_roots_overrides

dirmap_routine(source_path, destination_path) abstractmethod

Run host dependent remapping from source_path to destination_path

Source code in client/ayon_core/host/dirmap.py
70
71
72
73
@abstractmethod
def dirmap_routine(self, source_path, destination_path):
    """Run host dependent remapping from source_path to destination_path"""
    pass

get_mappings()

Get translation from source_path to destination_path.

It checks if Site Sync is enabled and user chose to use local site, in that case configuration in Local Settings takes precedence

Source code in client/ayon_core/host/dirmap.py
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
def get_mappings(self):
    """Get translation from source_path to destination_path.

        It checks if Site Sync is enabled and user chose to use local
        site, in that case configuration in Local Settings takes precedence
    """
    mapping_sett = self.project_settings[self.host_name].get("dirmap", {})
    local_mapping = self._get_local_sync_dirmap()
    mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping)
    if not mapping_enabled:
        return {}

    mapping = (
        local_mapping
        or mapping_sett["paths"]
        or {}
    )

    if (
        not mapping
        or not mapping.get("destination_path")
        or not mapping.get("source_path")
    ):
        return {}
    self.log.info("Processing directory mapping ...")
    self.log.info("mapping:: {}".format(mapping))
    return mapping

on_enable_dirmap() abstractmethod

Run host dependent operation for enabling dirmap if necessary.

Source code in client/ayon_core/host/dirmap.py
65
66
67
68
@abstractmethod
def on_enable_dirmap(self):
    """Run host dependent operation for enabling dirmap if necessary."""
    pass

process_dirmap(mapping=None)

Go through all paths in Settings and set them using dirmap.

If artists has Site Sync enabled, take dirmap mapping directly from Local Settings when artist is syncing workfile locally.

Source code in client/ayon_core/host/dirmap.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
def process_dirmap(self, mapping=None):
    # type: (dict) -> None
    """Go through all paths in Settings and set them using `dirmap`.

        If artists has Site Sync enabled, take dirmap mapping directly from
        Local Settings when artist is syncing workfile locally.

    """

    if not mapping:
        mapping = self.get_mappings()
    if not mapping:
        return

    self.on_enable_dirmap()

    for k, sp in enumerate(mapping["source_path"]):
        dst = mapping["destination_path"][k]
        try:
            # add trailing slash if missing
            sp = os.path.join(sp, '')
            dst = os.path.join(dst, '')
            print("{} -> {}".format(sp, dst))
            self.dirmap_routine(sp, dst)
        except IndexError:
            # missing corresponding destination path
            self.log.error((
                "invalid dirmap mapping, missing corresponding"
                " destination directory."
            ))
            break
        except RuntimeError:
            self.log.error(
                "invalid path {} -> {}, mapping not registered".format(
                    sp, dst
                )
            )
            continue

ILoadHost

Bases: AbstractHost

Implementation requirements to be able use reference of representations.

The load plugins can do referencing even without implementation of methods here, but switch and removement of containers would not be possible.

Questions
  • Is list container dependency of host or load plugins?
  • Should this be directly in HostBase?
    • how to find out if referencing is available?
    • do we need to know that?
Source code in client/ayon_core/host/interfaces/interfaces.py
 8
 9
10
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
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
class ILoadHost(AbstractHost):
    """Implementation requirements to be able use reference of representations.

    The load plugins can do referencing even without implementation of methods
    here, but switch and removement of containers would not be possible.

    Questions:
        - Is list container dependency of host or load plugins?
        - Should this be directly in HostBase?
            - how to find out if referencing is available?
            - do we need to know that?
    """

    @staticmethod
    def get_missing_load_methods(host):
        """Look for missing methods on "old type" host implementation.

        Method is used for validation of implemented functions related to
        loading. Checks only existence of methods.

        Args:
            Union[ModuleType, AbstractHost]: Object of host where to look for
                required methods.

        Returns:
            list[str]: Missing method implementations for loading workflow.
        """

        if isinstance(host, ILoadHost):
            return []

        required = ["ls"]
        missing = []
        for name in required:
            if not hasattr(host, name):
                missing.append(name)
        return missing

    @staticmethod
    def validate_load_methods(host):
        """Validate implemented methods of "old type" host for load workflow.

        Args:
            Union[ModuleType, AbstractHost]: Object of host to validate.

        Raises:
            MissingMethodsError: If there are missing methods on host
                implementation.
        """
        missing = ILoadHost.get_missing_load_methods(host)
        if missing:
            raise MissingMethodsError(host, missing)

    @abstractmethod
    def get_containers(self):
        """Retrieve referenced containers from scene.

        This can be implemented in hosts where referencing can be used.

        Todo:
            Rename function to something more self explanatory.
                Suggestion: 'get_containers'

        Returns:
            list[dict]: Information about loaded containers.
        """

        pass

    # --- Deprecated method names ---
    def ls(self):
        """Deprecated variant of 'get_containers'.

        Todo:
            Remove when all usages are replaced.
        """

        return self.get_containers()

get_containers() abstractmethod

Retrieve referenced containers from scene.

This can be implemented in hosts where referencing can be used.

Todo

Rename function to something more self explanatory. Suggestion: 'get_containers'

Returns:

Type Description

list[dict]: Information about loaded containers.

Source code in client/ayon_core/host/interfaces/interfaces.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@abstractmethod
def get_containers(self):
    """Retrieve referenced containers from scene.

    This can be implemented in hosts where referencing can be used.

    Todo:
        Rename function to something more self explanatory.
            Suggestion: 'get_containers'

    Returns:
        list[dict]: Information about loaded containers.
    """

    pass

get_missing_load_methods(host) staticmethod

Look for missing methods on "old type" host implementation.

Method is used for validation of implemented functions related to loading. Checks only existence of methods.

Parameters:

Name Type Description Default
Union[ModuleType, AbstractHost]

Object of host where to look for required methods.

required

Returns:

Type Description

list[str]: Missing method implementations for loading workflow.

Source code in client/ayon_core/host/interfaces/interfaces.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@staticmethod
def get_missing_load_methods(host):
    """Look for missing methods on "old type" host implementation.

    Method is used for validation of implemented functions related to
    loading. Checks only existence of methods.

    Args:
        Union[ModuleType, AbstractHost]: Object of host where to look for
            required methods.

    Returns:
        list[str]: Missing method implementations for loading workflow.
    """

    if isinstance(host, ILoadHost):
        return []

    required = ["ls"]
    missing = []
    for name in required:
        if not hasattr(host, name):
            missing.append(name)
    return missing

ls()

Deprecated variant of 'get_containers'.

Todo

Remove when all usages are replaced.

Source code in client/ayon_core/host/interfaces/interfaces.py
78
79
80
81
82
83
84
85
def ls(self):
    """Deprecated variant of 'get_containers'.

    Todo:
        Remove when all usages are replaced.
    """

    return self.get_containers()

validate_load_methods(host) staticmethod

Validate implemented methods of "old type" host for load workflow.

Parameters:

Name Type Description Default
Union[ModuleType, AbstractHost]

Object of host to validate.

required

Raises:

Type Description
MissingMethodsError

If there are missing methods on host implementation.

Source code in client/ayon_core/host/interfaces/interfaces.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@staticmethod
def validate_load_methods(host):
    """Validate implemented methods of "old type" host for load workflow.

    Args:
        Union[ModuleType, AbstractHost]: Object of host to validate.

    Raises:
        MissingMethodsError: If there are missing methods on host
            implementation.
    """
    missing = ILoadHost.get_missing_load_methods(host)
    if missing:
        raise MissingMethodsError(host, missing)

INewPublisher

Bases: IPublishHost

Legacy interface replaced by 'IPublishHost'.

Deprecated

'INewPublisher' is replaced by 'IPublishHost' please change your imports. There is no "reasonable" way hot mark these classes as deprecated to show warning of wrong import. Deprecated since 3.14. will be removed in 3.15.

Source code in client/ayon_core/host/interfaces/interfaces.py
178
179
180
181
182
183
184
185
186
187
188
189
class INewPublisher(IPublishHost):
    """Legacy interface replaced by 'IPublishHost'.

    Deprecated:
        'INewPublisher' is replaced by 'IPublishHost' please change your
        imports.
        There is no "reasonable" way hot mark these classes as deprecated
        to show warning of wrong import. Deprecated since 3.14.* will be
        removed in 3.15.*
    """

    pass

IPublishHost

Bases: AbstractHost

Functions related to new creation system in new publisher.

New publisher is not storing information only about each created instance but also some global data. At this moment are data related only to context publish plugins but that can extend in future.

Source code in client/ayon_core/host/interfaces/interfaces.py
 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
class IPublishHost(AbstractHost):
    """Functions related to new creation system in new publisher.

    New publisher is not storing information only about each created instance
    but also some global data. At this moment are data related only to context
    publish plugins but that can extend in future.
    """

    @staticmethod
    def get_missing_publish_methods(host):
        """Look for missing methods on "old type" host implementation.

        Method is used for validation of implemented functions related to
        new publish creation. Checks only existence of methods.

        Args:
            Union[ModuleType, AbstractHost]: Host module where to look for
                required methods.

        Returns:
            list[str]: Missing method implementations for new publisher
                workflow.
        """

        if isinstance(host, IPublishHost):
            return []

        required = [
            "get_context_data",
            "update_context_data",
            "get_context_title",
            "get_current_context",
        ]
        missing = []
        for name in required:
            if not hasattr(host, name):
                missing.append(name)
        return missing

    @staticmethod
    def validate_publish_methods(host):
        """Validate implemented methods of "old type" host.

        Args:
            Union[ModuleType, AbstractHost]: Host module to validate.

        Raises:
            MissingMethodsError: If there are missing methods on host
                implementation.
        """
        missing = IPublishHost.get_missing_publish_methods(host)
        if missing:
            raise MissingMethodsError(host, missing)

    @abstractmethod
    def get_context_data(self):
        """Get global data related to creation-publishing from workfile.

        These data are not related to any created instance but to whole
        publishing context. Not saving/returning them will cause that each
        reset of publishing resets all values to default ones.

        Context data can contain information about enabled/disabled publish
        plugins or other values that can be filled by artist.

        Returns:
            dict: Context data stored using 'update_context_data'.
        """

        pass

    @abstractmethod
    def update_context_data(self, data, changes):
        """Store global context data to workfile.

        Called when some values in context data has changed.

        Without storing the values in a way that 'get_context_data' would
        return them will each reset of publishing cause loose of filled values
        by artist. Best practice is to store values into workfile, if possible.

        Args:
            data (dict): New data as are.
            changes (dict): Only data that has been changed. Each value has
                tuple with '(<old>, <new>)' value.
        """

        pass

get_context_data() abstractmethod

Get global data related to creation-publishing from workfile.

These data are not related to any created instance but to whole publishing context. Not saving/returning them will cause that each reset of publishing resets all values to default ones.

Context data can contain information about enabled/disabled publish plugins or other values that can be filled by artist.

Returns:

Name Type Description
dict

Context data stored using 'update_context_data'.

Source code in client/ayon_core/host/interfaces/interfaces.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
@abstractmethod
def get_context_data(self):
    """Get global data related to creation-publishing from workfile.

    These data are not related to any created instance but to whole
    publishing context. Not saving/returning them will cause that each
    reset of publishing resets all values to default ones.

    Context data can contain information about enabled/disabled publish
    plugins or other values that can be filled by artist.

    Returns:
        dict: Context data stored using 'update_context_data'.
    """

    pass

get_missing_publish_methods(host) staticmethod

Look for missing methods on "old type" host implementation.

Method is used for validation of implemented functions related to new publish creation. Checks only existence of methods.

Parameters:

Name Type Description Default
Union[ModuleType, AbstractHost]

Host module where to look for required methods.

required

Returns:

Type Description

list[str]: Missing method implementations for new publisher workflow.

Source code in client/ayon_core/host/interfaces/interfaces.py
 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
@staticmethod
def get_missing_publish_methods(host):
    """Look for missing methods on "old type" host implementation.

    Method is used for validation of implemented functions related to
    new publish creation. Checks only existence of methods.

    Args:
        Union[ModuleType, AbstractHost]: Host module where to look for
            required methods.

    Returns:
        list[str]: Missing method implementations for new publisher
            workflow.
    """

    if isinstance(host, IPublishHost):
        return []

    required = [
        "get_context_data",
        "update_context_data",
        "get_context_title",
        "get_current_context",
    ]
    missing = []
    for name in required:
        if not hasattr(host, name):
            missing.append(name)
    return missing

update_context_data(data, changes) abstractmethod

Store global context data to workfile.

Called when some values in context data has changed.

Without storing the values in a way that 'get_context_data' would return them will each reset of publishing cause loose of filled values by artist. Best practice is to store values into workfile, if possible.

Parameters:

Name Type Description Default
data dict

New data as are.

required
changes dict

Only data that has been changed. Each value has tuple with '(, )' value.

required
Source code in client/ayon_core/host/interfaces/interfaces.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
@abstractmethod
def update_context_data(self, data, changes):
    """Store global context data to workfile.

    Called when some values in context data has changed.

    Without storing the values in a way that 'get_context_data' would
    return them will each reset of publishing cause loose of filled values
    by artist. Best practice is to store values into workfile, if possible.

    Args:
        data (dict): New data as are.
        changes (dict): Only data that has been changed. Each value has
            tuple with '(<old>, <new>)' value.
    """

    pass

validate_publish_methods(host) staticmethod

Validate implemented methods of "old type" host.

Parameters:

Name Type Description Default
Union[ModuleType, AbstractHost]

Host module to validate.

required

Raises:

Type Description
MissingMethodsError

If there are missing methods on host implementation.

Source code in client/ayon_core/host/interfaces/interfaces.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
@staticmethod
def validate_publish_methods(host):
    """Validate implemented methods of "old type" host.

    Args:
        Union[ModuleType, AbstractHost]: Host module to validate.

    Raises:
        MissingMethodsError: If there are missing methods on host
            implementation.
    """
    missing = IPublishHost.get_missing_publish_methods(host)
    if missing:
        raise MissingMethodsError(host, missing)

IWorkfileHost

Bases: AbstractHost

Implementation requirements to be able to use workfiles utils and tool.

Some of the methods are pre-implemented as they generally do the same in all host integrations.

Source code in client/ayon_core/host/interfaces/workfiles.py
 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
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
class IWorkfileHost(AbstractHost):
    """Implementation requirements to be able to use workfiles utils and tool.

    Some of the methods are pre-implemented as they generally do the same in
        all host integrations.

    """
    @abstractmethod
    def save_workfile(self, dst_path: Optional[str] = None) -> None:
        """Save the currently opened scene.

        Args:
            dst_path (str): Where the current scene should be saved. Or use
                the current path if 'None' is passed.

        """
        pass

    @abstractmethod
    def open_workfile(self, filepath: str) -> None:
        """Open passed filepath in the host.

        Args:
            filepath (str): Path to workfile.

        """
        pass

    @abstractmethod
    def get_current_workfile(self) -> Optional[str]:
        """Retrieve a path to current opened file.

        Returns:
            Optional[str]: Path to the file which is currently opened. None if
                nothing is opened or the current workfile is unsaved.

        """
        return None

    def workfile_has_unsaved_changes(self) -> Optional[bool]:
        """Currently opened scene is saved.

        Not all hosts can know if the current scene is saved because the API
            of DCC does not support it.

        Returns:
            Optional[bool]: True if scene is saved and False if has unsaved
                modifications. None if can't tell if workfiles has
                modifications.

        """
        return None

    def get_workfile_extensions(self) -> list[str]:
        """Extensions that can be used to save the workfile to.

        Notes:
            Method may not be used if 'list_workfiles' and
                'list_published_workfiles' are re-implemented with different
                logic.

        Returns:
            list[str]: List of extensions that can be used for saving.

        """
        return []

    def save_workfile_with_context(
        self,
        filepath: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        version: Optional[int] = None,
        comment: Optional[str] = None,
        description: Optional[str] = None,
        prepared_data: Optional[SaveWorkfileOptionalData] = None,
    ) -> None:
        """Save the current workfile with context.

        Arguments 'rootless_path', 'workfile_entities', 'project_entity'
            and 'anatomy' can be filled to enhance efficiency if you already
            have access to the values.

        Argument 'project_settings' is used to calculate 'rootless_path'
            if it is not provided.

        Notes:
            Should this method care about context change?

        Args:
            filepath (str): Where the current scene should be saved.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            version (Optional[int]): Version of the workfile. Information
                for workfile entity. Recommended to fill.
            comment (Optional[str]): Comment for the workfile.
                Usually used in the filename template.
            description (Optional[str]): Artist note for the workfile entity.
            prepared_data (Optional[SaveWorkfileOptionalData]): Prepared data
                for speed enhancements.

        """
        project_name = self.get_current_project_name()
        save_workfile_context = get_save_workfile_context(
            project_name,
            filepath,
            folder_entity,
            task_entity,
            host_name=self.name,
            prepared_data=prepared_data,
        )

        self._before_workfile_save(save_workfile_context)
        event_data = self._get_workfile_event_data(
            project_name,
            folder_entity,
            task_entity,
            filepath,
        )
        self._emit_workfile_save_event(event_data, after_save=False)

        workdir = os.path.dirname(filepath)
        if not os.path.exists(workdir):
            os.makedirs(workdir, exist_ok=True)

        # Set 'AYON_WORKDIR' environment variable
        os.environ["AYON_WORKDIR"] = workdir

        self.set_current_context(
            folder_entity,
            task_entity,
            reason=ContextChangeReason.workfile_save,
            project_entity=save_workfile_context.project_entity,
            anatomy=save_workfile_context.anatomy,
        )

        self.save_workfile(filepath)

        self._save_workfile_entity(
            save_workfile_context,
            version,
            comment,
            description,
        )
        self._after_workfile_save(save_workfile_context)
        self._emit_workfile_save_event(event_data)

    def open_workfile_with_context(
        self,
        filepath: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        prepared_data: Optional[OpenWorkfileOptionalData] = None,
    ) -> None:
        """Open passed filepath in the host with context.

        This function should be used to open workfile in different context.

        Notes:
            Should this method care about context change?

        Args:
            filepath (str): Path to workfile.
            folder_entity (dict[str, Any]): Folder id.
            task_entity (dict[str, Any]): Task id.
            prepared_data (Optional[WorkfileOptionalData]): Prepared data
                for speed enhancements.

        """
        context = self.get_current_context()
        project_name = context["project_name"]

        open_workfile_context = get_open_workfile_context(
            project_name,
            filepath,
            folder_entity,
            task_entity,
            prepared_data=prepared_data,
        )

        workdir = os.path.dirname(filepath)
        # Set 'AYON_WORKDIR' environment variable
        os.environ["AYON_WORKDIR"] = workdir

        event_data = self._get_workfile_event_data(
            project_name, folder_entity, task_entity, filepath
        )
        self._before_workfile_open(open_workfile_context)
        self._emit_workfile_open_event(event_data, after_open=False)

        self.set_current_context(
            folder_entity,
            task_entity,
            reason=ContextChangeReason.workfile_open,
            project_entity=open_workfile_context.project_entity,
            anatomy=open_workfile_context.anatomy,
        )

        self.open_workfile(filepath)

        self._after_workfile_open(open_workfile_context)
        self._emit_workfile_open_event(event_data)

    def list_workfiles(
        self,
        project_name: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        prepared_data: Optional[ListWorkfilesOptionalData] = None,
    ) -> list[WorkfileInfo]:
        """List workfiles in the given task.

        The method should also return workfiles that are not available on
            disk, but are in the AYON database.

        Notes:
        - Better method name?
        - This method is pre-implemented as the logic can be shared across
            95% of host integrations. Ad-hoc implementation to give host
            integration workfile api functionality.

        Args:
            project_name (str): Project name.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            prepared_data (Optional[ListWorkfilesOptionalData]): Prepared
                data for speed enhancements.

        Returns:
            list[WorkfileInfo]: List of workfiles.

        """
        from ayon_core.pipeline.template_data import get_template_data
        from ayon_core.pipeline.workfile.path_resolving import (
            get_workdir_with_workdir_data,
            WorkfileDataParser,
        )

        extensions = self.get_workfile_extensions()
        if not extensions:
            return []

        list_workfiles_context = get_list_workfiles_context(
            project_name,
            folder_entity,
            task_entity,
            host_name=self.name,
            prepared_data=prepared_data,
        )

        workfile_entities_by_path = {}
        for workfile_entity in list_workfiles_context.workfile_entities:
            rootless_path = workfile_entity["path"]
            path = os.path.normpath(
                list_workfiles_context.anatomy.fill_root(rootless_path)
            )
            workfile_entities_by_path[path] = workfile_entity

        workdir_data = get_template_data(
            list_workfiles_context.project_entity,
            folder_entity,
            task_entity,
            host_name=self.name,
        )
        workdir = get_workdir_with_workdir_data(
            workdir_data,
            project_name,
            anatomy=list_workfiles_context.anatomy,
            template_key=list_workfiles_context.template_key,
            project_settings=list_workfiles_context.project_settings,
        )

        file_template = list_workfiles_context.anatomy.get_template_item(
            "work", list_workfiles_context.template_key, "file"
        )
        rootless_workdir = workdir.rootless
        if platform.system().lower() == "windows":
            rootless_workdir = rootless_workdir.replace("\\", "/")

        filenames = []
        if os.path.exists(workdir):
            filenames = list(os.listdir(workdir))

        data_parser = WorkfileDataParser(file_template, workdir_data)
        items = []
        for filename in filenames:
            # TODO add 'default' support for folders
            ext = os.path.splitext(filename)[1].lower()
            if ext not in extensions:
                continue

            filepath = os.path.join(workdir, filename)

            rootless_path = f"{rootless_workdir}/{filename}"
            workfile_entity = workfile_entities_by_path.pop(
                filepath, None
            )
            version = comment = None
            if workfile_entity is not None:
                _data = workfile_entity["data"]
                version = _data.get("version")
                comment = _data.get("comment")

            if version is None:
                parsed_data = data_parser.parse_data(filename)
                version = parsed_data.version
                comment = parsed_data.comment

            item = WorkfileInfo.new(
                filepath,
                rootless_path,
                version=version,
                comment=comment,
                available=True,
                workfile_entity=workfile_entity,
            )
            items.append(item)

        for filepath, workfile_entity in workfile_entities_by_path.items():
            # Workfile entity is not in the filesystem
            #   but it is in the database
            rootless_path = workfile_entity["path"]
            ext = os.path.splitext(rootless_path)[1].lower()
            if ext not in extensions:
                continue

            _data = workfile_entity["data"]
            version = _data.get("version")
            comment = _data.get("comment")
            if version is None:
                filename = os.path.basename(rootless_path)
                parsed_data = data_parser.parse_data(filename)
                version = parsed_data.version
                comment = parsed_data.comment

            available = os.path.exists(filepath)
            items.append(WorkfileInfo.new(
                filepath,
                rootless_path,
                version=version,
                comment=comment,
                available=available,
                workfile_entity=workfile_entity,
            ))

        return items

    def list_published_workfiles(
        self,
        project_name: str,
        folder_id: str,
        *,
        prepared_data: Optional[ListPublishedWorkfilesOptionalData] = None,
    ) -> list[PublishedWorkfileInfo]:
        """List published workfiles for the given folder.

        The default implementation looks for products with the 'workfile'
            product type.

        Pre-fetched entities have mandatory fields to be fetched:
            - Version: 'id', 'author', 'taskId'
            - Representation: 'id', 'versionId', 'files'

        Args:
            project_name (str): Project name.
            folder_id (str): Folder id.
            prepared_data (Optional[ListPublishedWorkfilesOptionalData]):
                Prepared data for speed enhancements.

        Returns:
            list[PublishedWorkfileInfo]: Published workfile information for
                the given context.

        """
        list_workfiles_context = get_list_published_workfiles_context(
            project_name,
            folder_id,
            prepared_data=prepared_data,
        )
        if not list_workfiles_context.repre_entities:
            return []

        versions_by_id = {
            version_entity["id"]: version_entity
            for version_entity in list_workfiles_context.version_entities
        }
        extensions = {
            ext.lstrip(".")
            for ext in self.get_workfile_extensions()
        }
        items = []
        for repre_entity in list_workfiles_context.repre_entities:
            version_id = repre_entity["versionId"]
            version_entity = versions_by_id[version_id]
            task_id = version_entity["taskId"]

            # Filter by extension
            workfile_path = None
            for repre_file in repre_entity["files"]:
                ext = (
                    os.path.splitext(repre_file["name"])[1]
                    .lower()
                    .lstrip(".")
                )
                if ext in extensions:
                    workfile_path = repre_file["path"]
                    break

            if not workfile_path:
                continue

            try:
                workfile_path = workfile_path.format(
                    root=list_workfiles_context.anatomy.roots
                )
            except Exception:
                self.log.warning(
                    "Failed to format workfile path.", exc_info=True
                )

            is_available = False
            file_size = file_modified = file_created = None
            if workfile_path and os.path.exists(workfile_path):
                filestat = os.stat(workfile_path)
                is_available = True
                file_size = filestat.st_size
                file_created = filestat.st_ctime
                file_modified = filestat.st_mtime

            workfile_item = PublishedWorkfileInfo.new(
                project_name,
                folder_id,
                task_id,
                repre_entity,
                filepath=workfile_path,
                author=version_entity["author"],
                available=is_available,
                file_size=file_size,
                file_created=file_created,
                file_modified=file_modified,
            )
            items.append(workfile_item)

        return items

    def copy_workfile(
        self,
        src_path: str,
        dst_path: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        version: Optional[int] = None,
        comment: Optional[str] = None,
        description: Optional[str] = None,
        open_workfile: bool = True,
        prepared_data: Optional[CopyWorkfileOptionalData] = None,
    ) -> None:
        """Save workfile path with target folder and task context.

        It is expected that workfile is saved to the current project, but
            can be copied from the other project.

        Arguments 'rootless_path', 'workfile_entities', 'project_entity'
            and 'anatomy' can be filled to enhance efficiency if you already
            have access to the values.

        Argument 'project_settings' is used to calculate 'rootless_path'
            if it is not provided.

        Args:
            src_path (str): Path to the source scene.
            dst_path (str): Where the scene should be saved.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            version (Optional[int]): Version of the workfile. Information
                for workfile entity. Recommended to fill.
            comment (Optional[str]): Comment for the workfile.
            description (Optional[str]): Artist note for the workfile entity.
            open_workfile (bool): Open workfile when copied.
            prepared_data (Optional[CopyWorkfileOptionalData]): Prepared data
                for speed enhancements.

        """
        project_name = self.get_current_project_name()
        copy_workfile_context: CopyWorkfileContext = get_copy_workfile_context(
            project_name,
            src_path,
            dst_path,
            folder_entity,
            task_entity,
            version=version,
            comment=comment,
            description=description,
            open_workfile=open_workfile,
            host_name=self.name,
            prepared_data=prepared_data,
        )
        self._copy_workfile(
            copy_workfile_context,
            version=version,
            comment=comment,
            description=description,
            open_workfile=open_workfile,
        )

    def copy_workfile_representation(
        self,
        src_project_name: str,
        src_representation_entity: dict[str, Any],
        dst_path: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        *,
        version: Optional[int] = None,
        comment: Optional[str] = None,
        description: Optional[str] = None,
        open_workfile: bool = True,
        prepared_data: Optional[CopyPublishedWorkfileOptionalData] = None,
    ) -> None:
        """Copy workfile representation.

        Use representation as a source for the workfile.

        Arguments 'rootless_path', 'workfile_entities', 'project_entity'
            and 'anatomy' can be filled to enhance efficiency if you already
            have access to the values.

        Argument 'project_settings' is used to calculate 'rootless_path'
            if it is not provided.

        Args:
            src_project_name (str): Project name.
            src_representation_entity (dict[str, Any]): Representation
                entity.
            dst_path (str): Where the scene should be saved.
            folder_entity (dict[str, Any): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            version (Optional[int]): Version of the workfile. Information
                for workfile entity. Recommended to fill.
            comment (Optional[str]): Comment for the workfile.
            description (Optional[str]): Artist note for the workfile entity.
            open_workfile (bool): Open workfile when copied.
            prepared_data (Optional[CopyPublishedWorkfileOptionalData]):
                Prepared data for speed enhancements.

        """
        project_name = self.get_current_project_name()
        copy_repre_workfile_context: CopyPublishedWorkfileContext = (
            get_copy_repre_workfile_context(
                project_name,
                src_project_name,
                src_representation_entity,
                dst_path,
                folder_entity,
                task_entity,
                version=version,
                comment=comment,
                description=description,
                open_workfile=open_workfile,
                host_name=self.name,
                prepared_data=prepared_data,
            )
        )
        self._copy_workfile(
            copy_repre_workfile_context,
            version=version,
            comment=comment,
            description=description,
            open_workfile=open_workfile,
        )

    # --- Deprecated method names ---
    @deprecated("Use 'get_workfile_extensions' instead")
    def file_extensions(self):
        """Deprecated variant of 'get_workfile_extensions'.

        Todo:
            Remove when all usages are replaced.

        """
        return self.get_workfile_extensions()

    @deprecated("Use 'save_workfile' instead")
    def save_file(self, dst_path=None):
        """Deprecated variant of 'save_workfile'.

        Todo:
            Remove when all usages are replaced

        """
        self.save_workfile(dst_path)

    @deprecated("Use 'open_workfile' instead")
    def open_file(self, filepath):
        """Deprecated variant of 'open_workfile'.

        Todo:
            Remove when all usages are replaced.

        """
        return self.open_workfile(filepath)

    @deprecated("Use 'get_current_workfile' instead")
    def current_file(self):
        """Deprecated variant of 'get_current_workfile'.

        Todo:
            Remove when all usages are replaced.

        """
        return self.get_current_workfile()

    @deprecated("Use 'workfile_has_unsaved_changes' instead")
    def has_unsaved_changes(self):
        """Deprecated variant of 'workfile_has_unsaved_changes'.

        Todo:
            Remove when all usages are replaced.

        """
        return self.workfile_has_unsaved_changes()

    def _copy_workfile(
        self,
        copy_workfile_context: CopyWorkfileContext,
        *,
        version: Optional[int],
        comment: Optional[str],
        description: Optional[str],
        open_workfile: bool,
    ) -> None:
        """Save workfile path with target folder and task context.

        It is expected that workfile is saved to the current project, but
            can be copied from the other project.

        Arguments 'rootless_path', 'workfile_entities', 'project_entity'
            and 'anatomy' can be filled to enhance efficiency if you already
            have access to the values.

        Argument 'project_settings' is used to calculate 'rootless_path'
            if it is not provided.

        Args:
            copy_workfile_context (CopyWorkfileContext): Prepared data
                for speed enhancements.
            version (Optional[int]): Version of the workfile. Information
                for workfile entity. Recommended to fill.
            comment (Optional[str]): Comment for the workfile.
            description (Optional[str]): Artist note for the workfile entity.
            open_workfile (bool): Open workfile when copied.

        """
        self._before_workfile_copy(copy_workfile_context)
        event_data = self._get_workfile_event_data(
            copy_workfile_context.project_name,
            copy_workfile_context.folder_entity,
            copy_workfile_context.task_entity,
            copy_workfile_context.dst_path,
        )
        self._emit_workfile_save_event(event_data, after_save=False)

        dst_dir = os.path.dirname(copy_workfile_context.dst_path)
        if not os.path.exists(dst_dir):
            os.makedirs(dst_dir, exist_ok=True)
        shutil.copy(
            copy_workfile_context.src_path,
            copy_workfile_context.dst_path
        )

        self._save_workfile_entity(
            copy_workfile_context,
            version,
            comment,
            description,
        )
        self._after_workfile_copy(copy_workfile_context)
        self._emit_workfile_save_event(event_data)

        if not open_workfile:
            return

        self.open_workfile_with_context(
            copy_workfile_context.dst_path,
            copy_workfile_context.folder_entity,
            copy_workfile_context.task_entity,
        )

    def _save_workfile_entity(
        self,
        save_workfile_context: SaveWorkfileContext,
        version: Optional[int],
        comment: Optional[str],
        description: Optional[str],
    ) -> Optional[dict[str, Any]]:
        """Create of update workfile entity to AYON based on provided data.

        Args:
            save_workfile_context (SaveWorkfileContext): Save workfile
                context with all prepared data.
            version (Optional[int]): Version of the workfile.
            comment (Optional[str]): Comment for the workfile.
            description (Optional[str]): Artist note for the workfile entity.

        Returns:
            Optional[dict[str, Any]]: Workfile entity.

        """
        from ayon_core.pipeline.workfile.utils import (
            save_workfile_info
        )

        project_name = self.get_current_project_name()
        if not description:
            description = None

        if not comment:
            comment = None

        rootless_path = save_workfile_context.rootless_path
        # It is not possible to create workfile infor without rootless path
        workfile_info = None
        if not rootless_path:
            return workfile_info

        if platform.system().lower() == "windows":
            rootless_path = rootless_path.replace("\\", "/")

        # Get application information
        app_info = self.get_app_information()
        data = {}
        if app_info.app_name:
            data["app_name"] = app_info.app_name
        if app_info.app_version:
            data["app_version"] = app_info.app_version

        # Use app group and app variant from applications addon (if available)
        app_addon_name = os.environ.get("AYON_APP_NAME")
        if not app_addon_name:
            app_addon_name = None

        app_addon_tools_s = os.environ.get("AYON_APP_TOOLS")
        app_addon_tools = []
        if app_addon_tools_s:
            app_addon_tools = app_addon_tools_s.split(";")

        data["ayon_app_name"] = app_addon_name
        data["ayon_app_tools"] = app_addon_tools

        workfile_info = save_workfile_info(
            project_name,
            save_workfile_context.task_entity["id"],
            rootless_path,
            self.name,
            version,
            comment,
            description,
            data=data,
            workfile_entities=save_workfile_context.workfile_entities,
        )
        return workfile_info

    def _create_extra_folders(
        self,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        workdir: str,
    ) -> None:
        """Create extra folders in the workdir.

        This method should be called when workfile is saved or copied.

        Args:
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            workdir (str): Workdir where workfile/s will be stored.

        """
        from ayon_core.pipeline.workfile.path_resolving import (
            create_workdir_extra_folders
        )

        project_name = self.get_current_project_name()

        # Create extra folders
        create_workdir_extra_folders(
            workdir,
            self.name,
            task_entity["taskType"],
            task_entity["name"],
            project_name
        )

    def _get_workfile_event_data(
        self,
        project_name: str,
        folder_entity: dict[str, Any],
        task_entity: dict[str, Any],
        filepath: str,
    ) -> dict[str, Optional[str]]:
        """Prepare workfile event data.

        Args:
            project_name (str): Name of the project where workfile lives.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            filepath (str): Path to the workfile.

        Returns:
            dict[str, Optional[str]]: Data for workfile event.

        """
        workdir, filename = os.path.split(filepath)
        return {
            "project_name": project_name,
            "folder_id": folder_entity["id"],
            "folder_path": folder_entity["path"],
            "task_id": task_entity["id"],
            "task_name": task_entity["name"],
            "host_name": self.name,
            "filepath": filepath,
            "filename": filename,
            "workdir_path": workdir,
        }

    def _before_workfile_open(
        self, open_workfile_context: OpenWorkfileContext
    ) -> None:
        """Before workfile is opened.

        This method is called before the workfile is opened in the host.

        Can be overridden to implement host specific logic.

        Args:
            open_workfile_context (OpenWorkfileContext): Context and path of
                workfile to open.

        """
        pass

    def _after_workfile_open(
        self, open_workfile_context: OpenWorkfileContext
    ) -> None:
        """After workfile is opened.

        This method is called after the workfile is opened in the host.

        Can be overridden to implement host specific logic.

        Args:
            open_workfile_context (OpenWorkfileContext): Context and path of
                opened workfile.

        """
        pass

    def _before_workfile_save(
        self, save_workfile_context: SaveWorkfileContext
    ) -> None:
        """Before workfile is saved.

        This method is called before the workfile is saved in the host.

        Can be overridden to implement host specific logic.

        Args:
            save_workfile_context (SaveWorkfileContext): Workfile path with
                target folder and task context.

        """
        pass

    def _after_workfile_save(
        self, save_workfile_context: SaveWorkfileContext
    ) -> None:
        """After workfile is saved.

        This method is called after the workfile is saved in the host.

        Can be overridden to implement host specific logic.

        Args:
            save_workfile_context (SaveWorkfileContext): Workfile path with
                target folder and task context.

        """
        workdir = os.path.dirname(save_workfile_context.dst_path)
        self._create_extra_folders(
            save_workfile_context.folder_entity,
            save_workfile_context.task_entity,
            workdir
        )

    def _before_workfile_copy(
        self, copy_workfile_context: CopyWorkfileContext
    ) -> None:
        """Before workfile is copied.

        This method is called before the workfile is copied by host
            integration.

        Can be overridden to implement host specific logic.

        Args:
            copy_workfile_context (CopyWorkfileContext): Source and destination
                path with context before workfile is copied.

        """
        pass

    def _after_workfile_copy(
        self, copy_workfile_context: CopyWorkfileContext
    ) -> None:
        """After workfile is copied.

        This method is called after the workfile is copied by host
            integration.

        Can be overridden to implement host specific logic.

        Args:
            copy_workfile_context (CopyWorkfileContext): Source and destination
                path with context after workfile is copied.

        """
        workdir = os.path.dirname(copy_workfile_context.dst_path)
        self._create_extra_folders(
            copy_workfile_context.folder_entity,
            copy_workfile_context.task_entity,
            workdir,
        )

    def _emit_workfile_open_event(
        self,
        event_data: dict[str, Optional[str]],
        after_open: bool = True,
    ) -> None:
        """Emit workfile save event.

        Emit event before and after workfile is opened.

        This method is not meant to be overridden.

        Other addons can listen to this event and do additional steps.

        Args:
            event_data (dict[str, Optional[str]]): Prepare event data.
            after_open (bool): Emit event after workfile is opened.

        """
        topics = []
        topic_end = "before"
        if after_open:
            topics.append("workfile.opened")
            topic_end = "after"

        # Keep backwards compatible event topic
        topics.append(f"workfile.open.{topic_end}")

        for topic in topics:
            emit_event(topic, event_data)

    def _emit_workfile_save_event(
        self,
        event_data: dict[str, Optional[str]],
        after_save: bool = True,
    ) -> None:
        """Emit workfile save event.

        Emit event before and after workfile is saved or copied.

        This method is not meant to be overridden.

        Other addons can listen to this event and do additional steps.

        Args:
            event_data (dict[str, Optional[str]]): Prepare event data.
            after_save (bool): Emit event after workfile is saved.

        """
        topics = []
        topic_end = "before"
        if after_save:
            topics.append("workfile.saved")
            topic_end = "after"

        # Keep backwards compatible event topic
        topics.append(f"workfile.save.{topic_end}")

        for topic in topics:
            emit_event(topic, event_data)

copy_workfile(src_path, dst_path, folder_entity, task_entity, *, version=None, comment=None, description=None, open_workfile=True, prepared_data=None)

Save workfile path with target folder and task context.

It is expected that workfile is saved to the current project, but can be copied from the other project.

Arguments 'rootless_path', 'workfile_entities', 'project_entity' and 'anatomy' can be filled to enhance efficiency if you already have access to the values.

Argument 'project_settings' is used to calculate 'rootless_path' if it is not provided.

Parameters:

Name Type Description Default
src_path str

Path to the source scene.

required
dst_path str

Where the scene should be saved.

required
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
version Optional[int]

Version of the workfile. Information for workfile entity. Recommended to fill.

None
comment Optional[str]

Comment for the workfile.

None
description Optional[str]

Artist note for the workfile entity.

None
open_workfile bool

Open workfile when copied.

True
prepared_data Optional[CopyWorkfileOptionalData]

Prepared data for speed enhancements.

None
Source code in client/ayon_core/host/interfaces/workfiles.py
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
def copy_workfile(
    self,
    src_path: str,
    dst_path: str,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    version: Optional[int] = None,
    comment: Optional[str] = None,
    description: Optional[str] = None,
    open_workfile: bool = True,
    prepared_data: Optional[CopyWorkfileOptionalData] = None,
) -> None:
    """Save workfile path with target folder and task context.

    It is expected that workfile is saved to the current project, but
        can be copied from the other project.

    Arguments 'rootless_path', 'workfile_entities', 'project_entity'
        and 'anatomy' can be filled to enhance efficiency if you already
        have access to the values.

    Argument 'project_settings' is used to calculate 'rootless_path'
        if it is not provided.

    Args:
        src_path (str): Path to the source scene.
        dst_path (str): Where the scene should be saved.
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        version (Optional[int]): Version of the workfile. Information
            for workfile entity. Recommended to fill.
        comment (Optional[str]): Comment for the workfile.
        description (Optional[str]): Artist note for the workfile entity.
        open_workfile (bool): Open workfile when copied.
        prepared_data (Optional[CopyWorkfileOptionalData]): Prepared data
            for speed enhancements.

    """
    project_name = self.get_current_project_name()
    copy_workfile_context: CopyWorkfileContext = get_copy_workfile_context(
        project_name,
        src_path,
        dst_path,
        folder_entity,
        task_entity,
        version=version,
        comment=comment,
        description=description,
        open_workfile=open_workfile,
        host_name=self.name,
        prepared_data=prepared_data,
    )
    self._copy_workfile(
        copy_workfile_context,
        version=version,
        comment=comment,
        description=description,
        open_workfile=open_workfile,
    )

copy_workfile_representation(src_project_name, src_representation_entity, dst_path, folder_entity, task_entity, *, version=None, comment=None, description=None, open_workfile=True, prepared_data=None)

Copy workfile representation.

Use representation as a source for the workfile.

Arguments 'rootless_path', 'workfile_entities', 'project_entity' and 'anatomy' can be filled to enhance efficiency if you already have access to the values.

Argument 'project_settings' is used to calculate 'rootless_path' if it is not provided.

Parameters:

Name Type Description Default
src_project_name str

Project name.

required
src_representation_entity dict[str, Any]

Representation entity.

required
dst_path str

Where the scene should be saved.

required
folder_entity dict[str, Any

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
version Optional[int]

Version of the workfile. Information for workfile entity. Recommended to fill.

None
comment Optional[str]

Comment for the workfile.

None
description Optional[str]

Artist note for the workfile entity.

None
open_workfile bool

Open workfile when copied.

True
prepared_data Optional[CopyPublishedWorkfileOptionalData]

Prepared data for speed enhancements.

None
Source code in client/ayon_core/host/interfaces/workfiles.py
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
def copy_workfile_representation(
    self,
    src_project_name: str,
    src_representation_entity: dict[str, Any],
    dst_path: str,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    version: Optional[int] = None,
    comment: Optional[str] = None,
    description: Optional[str] = None,
    open_workfile: bool = True,
    prepared_data: Optional[CopyPublishedWorkfileOptionalData] = None,
) -> None:
    """Copy workfile representation.

    Use representation as a source for the workfile.

    Arguments 'rootless_path', 'workfile_entities', 'project_entity'
        and 'anatomy' can be filled to enhance efficiency if you already
        have access to the values.

    Argument 'project_settings' is used to calculate 'rootless_path'
        if it is not provided.

    Args:
        src_project_name (str): Project name.
        src_representation_entity (dict[str, Any]): Representation
            entity.
        dst_path (str): Where the scene should be saved.
        folder_entity (dict[str, Any): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        version (Optional[int]): Version of the workfile. Information
            for workfile entity. Recommended to fill.
        comment (Optional[str]): Comment for the workfile.
        description (Optional[str]): Artist note for the workfile entity.
        open_workfile (bool): Open workfile when copied.
        prepared_data (Optional[CopyPublishedWorkfileOptionalData]):
            Prepared data for speed enhancements.

    """
    project_name = self.get_current_project_name()
    copy_repre_workfile_context: CopyPublishedWorkfileContext = (
        get_copy_repre_workfile_context(
            project_name,
            src_project_name,
            src_representation_entity,
            dst_path,
            folder_entity,
            task_entity,
            version=version,
            comment=comment,
            description=description,
            open_workfile=open_workfile,
            host_name=self.name,
            prepared_data=prepared_data,
        )
    )
    self._copy_workfile(
        copy_repre_workfile_context,
        version=version,
        comment=comment,
        description=description,
        open_workfile=open_workfile,
    )

current_file()

Deprecated variant of 'get_current_workfile'.

Todo

Remove when all usages are replaced.

Source code in client/ayon_core/host/interfaces/workfiles.py
1431
1432
1433
1434
1435
1436
1437
1438
1439
@deprecated("Use 'get_current_workfile' instead")
def current_file(self):
    """Deprecated variant of 'get_current_workfile'.

    Todo:
        Remove when all usages are replaced.

    """
    return self.get_current_workfile()

file_extensions()

Deprecated variant of 'get_workfile_extensions'.

Todo

Remove when all usages are replaced.

Source code in client/ayon_core/host/interfaces/workfiles.py
1401
1402
1403
1404
1405
1406
1407
1408
1409
@deprecated("Use 'get_workfile_extensions' instead")
def file_extensions(self):
    """Deprecated variant of 'get_workfile_extensions'.

    Todo:
        Remove when all usages are replaced.

    """
    return self.get_workfile_extensions()

get_current_workfile() abstractmethod

Retrieve a path to current opened file.

Returns:

Type Description
Optional[str]

Optional[str]: Path to the file which is currently opened. None if nothing is opened or the current workfile is unsaved.

Source code in client/ayon_core/host/interfaces/workfiles.py
853
854
855
856
857
858
859
860
861
862
@abstractmethod
def get_current_workfile(self) -> Optional[str]:
    """Retrieve a path to current opened file.

    Returns:
        Optional[str]: Path to the file which is currently opened. None if
            nothing is opened or the current workfile is unsaved.

    """
    return None

get_workfile_extensions()

Extensions that can be used to save the workfile to.

Notes

Method may not be used if 'list_workfiles' and 'list_published_workfiles' are re-implemented with different logic.

Returns:

Type Description
list[str]

list[str]: List of extensions that can be used for saving.

Source code in client/ayon_core/host/interfaces/workfiles.py
878
879
880
881
882
883
884
885
886
887
888
889
890
def get_workfile_extensions(self) -> list[str]:
    """Extensions that can be used to save the workfile to.

    Notes:
        Method may not be used if 'list_workfiles' and
            'list_published_workfiles' are re-implemented with different
            logic.

    Returns:
        list[str]: List of extensions that can be used for saving.

    """
    return []

has_unsaved_changes()

Deprecated variant of 'workfile_has_unsaved_changes'.

Todo

Remove when all usages are replaced.

Source code in client/ayon_core/host/interfaces/workfiles.py
1441
1442
1443
1444
1445
1446
1447
1448
1449
@deprecated("Use 'workfile_has_unsaved_changes' instead")
def has_unsaved_changes(self):
    """Deprecated variant of 'workfile_has_unsaved_changes'.

    Todo:
        Remove when all usages are replaced.

    """
    return self.workfile_has_unsaved_changes()

list_published_workfiles(project_name, folder_id, *, prepared_data=None)

List published workfiles for the given folder.

The default implementation looks for products with the 'workfile' product type.

Pre-fetched entities have mandatory fields to be fetched
  • Version: 'id', 'author', 'taskId'
  • Representation: 'id', 'versionId', 'files'

Parameters:

Name Type Description Default
project_name str

Project name.

required
folder_id str

Folder id.

required
prepared_data Optional[ListPublishedWorkfilesOptionalData]

Prepared data for speed enhancements.

None

Returns:

Type Description
list[PublishedWorkfileInfo]

list[PublishedWorkfileInfo]: Published workfile information for the given context.

Source code in client/ayon_core/host/interfaces/workfiles.py
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
def list_published_workfiles(
    self,
    project_name: str,
    folder_id: str,
    *,
    prepared_data: Optional[ListPublishedWorkfilesOptionalData] = None,
) -> list[PublishedWorkfileInfo]:
    """List published workfiles for the given folder.

    The default implementation looks for products with the 'workfile'
        product type.

    Pre-fetched entities have mandatory fields to be fetched:
        - Version: 'id', 'author', 'taskId'
        - Representation: 'id', 'versionId', 'files'

    Args:
        project_name (str): Project name.
        folder_id (str): Folder id.
        prepared_data (Optional[ListPublishedWorkfilesOptionalData]):
            Prepared data for speed enhancements.

    Returns:
        list[PublishedWorkfileInfo]: Published workfile information for
            the given context.

    """
    list_workfiles_context = get_list_published_workfiles_context(
        project_name,
        folder_id,
        prepared_data=prepared_data,
    )
    if not list_workfiles_context.repre_entities:
        return []

    versions_by_id = {
        version_entity["id"]: version_entity
        for version_entity in list_workfiles_context.version_entities
    }
    extensions = {
        ext.lstrip(".")
        for ext in self.get_workfile_extensions()
    }
    items = []
    for repre_entity in list_workfiles_context.repre_entities:
        version_id = repre_entity["versionId"]
        version_entity = versions_by_id[version_id]
        task_id = version_entity["taskId"]

        # Filter by extension
        workfile_path = None
        for repre_file in repre_entity["files"]:
            ext = (
                os.path.splitext(repre_file["name"])[1]
                .lower()
                .lstrip(".")
            )
            if ext in extensions:
                workfile_path = repre_file["path"]
                break

        if not workfile_path:
            continue

        try:
            workfile_path = workfile_path.format(
                root=list_workfiles_context.anatomy.roots
            )
        except Exception:
            self.log.warning(
                "Failed to format workfile path.", exc_info=True
            )

        is_available = False
        file_size = file_modified = file_created = None
        if workfile_path and os.path.exists(workfile_path):
            filestat = os.stat(workfile_path)
            is_available = True
            file_size = filestat.st_size
            file_created = filestat.st_ctime
            file_modified = filestat.st_mtime

        workfile_item = PublishedWorkfileInfo.new(
            project_name,
            folder_id,
            task_id,
            repre_entity,
            filepath=workfile_path,
            author=version_entity["author"],
            available=is_available,
            file_size=file_size,
            file_created=file_created,
            file_modified=file_modified,
        )
        items.append(workfile_item)

    return items

list_workfiles(project_name, folder_entity, task_entity, *, prepared_data=None)

List workfiles in the given task.

The method should also return workfiles that are not available on disk, but are in the AYON database.

Notes: - Better method name? - This method is pre-implemented as the logic can be shared across 95% of host integrations. Ad-hoc implementation to give host integration workfile api functionality.

Parameters:

Name Type Description Default
project_name str

Project name.

required
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
prepared_data Optional[ListWorkfilesOptionalData]

Prepared data for speed enhancements.

None

Returns:

Type Description
list[WorkfileInfo]

list[WorkfileInfo]: List of workfiles.

Source code in client/ayon_core/host/interfaces/workfiles.py
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
def list_workfiles(
    self,
    project_name: str,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    prepared_data: Optional[ListWorkfilesOptionalData] = None,
) -> list[WorkfileInfo]:
    """List workfiles in the given task.

    The method should also return workfiles that are not available on
        disk, but are in the AYON database.

    Notes:
    - Better method name?
    - This method is pre-implemented as the logic can be shared across
        95% of host integrations. Ad-hoc implementation to give host
        integration workfile api functionality.

    Args:
        project_name (str): Project name.
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        prepared_data (Optional[ListWorkfilesOptionalData]): Prepared
            data for speed enhancements.

    Returns:
        list[WorkfileInfo]: List of workfiles.

    """
    from ayon_core.pipeline.template_data import get_template_data
    from ayon_core.pipeline.workfile.path_resolving import (
        get_workdir_with_workdir_data,
        WorkfileDataParser,
    )

    extensions = self.get_workfile_extensions()
    if not extensions:
        return []

    list_workfiles_context = get_list_workfiles_context(
        project_name,
        folder_entity,
        task_entity,
        host_name=self.name,
        prepared_data=prepared_data,
    )

    workfile_entities_by_path = {}
    for workfile_entity in list_workfiles_context.workfile_entities:
        rootless_path = workfile_entity["path"]
        path = os.path.normpath(
            list_workfiles_context.anatomy.fill_root(rootless_path)
        )
        workfile_entities_by_path[path] = workfile_entity

    workdir_data = get_template_data(
        list_workfiles_context.project_entity,
        folder_entity,
        task_entity,
        host_name=self.name,
    )
    workdir = get_workdir_with_workdir_data(
        workdir_data,
        project_name,
        anatomy=list_workfiles_context.anatomy,
        template_key=list_workfiles_context.template_key,
        project_settings=list_workfiles_context.project_settings,
    )

    file_template = list_workfiles_context.anatomy.get_template_item(
        "work", list_workfiles_context.template_key, "file"
    )
    rootless_workdir = workdir.rootless
    if platform.system().lower() == "windows":
        rootless_workdir = rootless_workdir.replace("\\", "/")

    filenames = []
    if os.path.exists(workdir):
        filenames = list(os.listdir(workdir))

    data_parser = WorkfileDataParser(file_template, workdir_data)
    items = []
    for filename in filenames:
        # TODO add 'default' support for folders
        ext = os.path.splitext(filename)[1].lower()
        if ext not in extensions:
            continue

        filepath = os.path.join(workdir, filename)

        rootless_path = f"{rootless_workdir}/{filename}"
        workfile_entity = workfile_entities_by_path.pop(
            filepath, None
        )
        version = comment = None
        if workfile_entity is not None:
            _data = workfile_entity["data"]
            version = _data.get("version")
            comment = _data.get("comment")

        if version is None:
            parsed_data = data_parser.parse_data(filename)
            version = parsed_data.version
            comment = parsed_data.comment

        item = WorkfileInfo.new(
            filepath,
            rootless_path,
            version=version,
            comment=comment,
            available=True,
            workfile_entity=workfile_entity,
        )
        items.append(item)

    for filepath, workfile_entity in workfile_entities_by_path.items():
        # Workfile entity is not in the filesystem
        #   but it is in the database
        rootless_path = workfile_entity["path"]
        ext = os.path.splitext(rootless_path)[1].lower()
        if ext not in extensions:
            continue

        _data = workfile_entity["data"]
        version = _data.get("version")
        comment = _data.get("comment")
        if version is None:
            filename = os.path.basename(rootless_path)
            parsed_data = data_parser.parse_data(filename)
            version = parsed_data.version
            comment = parsed_data.comment

        available = os.path.exists(filepath)
        items.append(WorkfileInfo.new(
            filepath,
            rootless_path,
            version=version,
            comment=comment,
            available=available,
            workfile_entity=workfile_entity,
        ))

    return items

open_file(filepath)

Deprecated variant of 'open_workfile'.

Todo

Remove when all usages are replaced.

Source code in client/ayon_core/host/interfaces/workfiles.py
1421
1422
1423
1424
1425
1426
1427
1428
1429
@deprecated("Use 'open_workfile' instead")
def open_file(self, filepath):
    """Deprecated variant of 'open_workfile'.

    Todo:
        Remove when all usages are replaced.

    """
    return self.open_workfile(filepath)

open_workfile(filepath) abstractmethod

Open passed filepath in the host.

Parameters:

Name Type Description Default
filepath str

Path to workfile.

required
Source code in client/ayon_core/host/interfaces/workfiles.py
843
844
845
846
847
848
849
850
851
@abstractmethod
def open_workfile(self, filepath: str) -> None:
    """Open passed filepath in the host.

    Args:
        filepath (str): Path to workfile.

    """
    pass

open_workfile_with_context(filepath, folder_entity, task_entity, *, prepared_data=None)

Open passed filepath in the host with context.

This function should be used to open workfile in different context.

Notes

Should this method care about context change?

Parameters:

Name Type Description Default
filepath str

Path to workfile.

required
folder_entity dict[str, Any]

Folder id.

required
task_entity dict[str, Any]

Task id.

required
prepared_data Optional[WorkfileOptionalData]

Prepared data for speed enhancements.

None
Source code in client/ayon_core/host/interfaces/workfiles.py
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
def open_workfile_with_context(
    self,
    filepath: str,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    prepared_data: Optional[OpenWorkfileOptionalData] = None,
) -> None:
    """Open passed filepath in the host with context.

    This function should be used to open workfile in different context.

    Notes:
        Should this method care about context change?

    Args:
        filepath (str): Path to workfile.
        folder_entity (dict[str, Any]): Folder id.
        task_entity (dict[str, Any]): Task id.
        prepared_data (Optional[WorkfileOptionalData]): Prepared data
            for speed enhancements.

    """
    context = self.get_current_context()
    project_name = context["project_name"]

    open_workfile_context = get_open_workfile_context(
        project_name,
        filepath,
        folder_entity,
        task_entity,
        prepared_data=prepared_data,
    )

    workdir = os.path.dirname(filepath)
    # Set 'AYON_WORKDIR' environment variable
    os.environ["AYON_WORKDIR"] = workdir

    event_data = self._get_workfile_event_data(
        project_name, folder_entity, task_entity, filepath
    )
    self._before_workfile_open(open_workfile_context)
    self._emit_workfile_open_event(event_data, after_open=False)

    self.set_current_context(
        folder_entity,
        task_entity,
        reason=ContextChangeReason.workfile_open,
        project_entity=open_workfile_context.project_entity,
        anatomy=open_workfile_context.anatomy,
    )

    self.open_workfile(filepath)

    self._after_workfile_open(open_workfile_context)
    self._emit_workfile_open_event(event_data)

save_file(dst_path=None)

Deprecated variant of 'save_workfile'.

Todo

Remove when all usages are replaced

Source code in client/ayon_core/host/interfaces/workfiles.py
1411
1412
1413
1414
1415
1416
1417
1418
1419
@deprecated("Use 'save_workfile' instead")
def save_file(self, dst_path=None):
    """Deprecated variant of 'save_workfile'.

    Todo:
        Remove when all usages are replaced

    """
    self.save_workfile(dst_path)

save_workfile(dst_path=None) abstractmethod

Save the currently opened scene.

Parameters:

Name Type Description Default
dst_path str

Where the current scene should be saved. Or use the current path if 'None' is passed.

None
Source code in client/ayon_core/host/interfaces/workfiles.py
832
833
834
835
836
837
838
839
840
841
@abstractmethod
def save_workfile(self, dst_path: Optional[str] = None) -> None:
    """Save the currently opened scene.

    Args:
        dst_path (str): Where the current scene should be saved. Or use
            the current path if 'None' is passed.

    """
    pass

save_workfile_with_context(filepath, folder_entity, task_entity, *, version=None, comment=None, description=None, prepared_data=None)

Save the current workfile with context.

Arguments 'rootless_path', 'workfile_entities', 'project_entity' and 'anatomy' can be filled to enhance efficiency if you already have access to the values.

Argument 'project_settings' is used to calculate 'rootless_path' if it is not provided.

Notes

Should this method care about context change?

Parameters:

Name Type Description Default
filepath str

Where the current scene should be saved.

required
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
version Optional[int]

Version of the workfile. Information for workfile entity. Recommended to fill.

None
comment Optional[str]

Comment for the workfile. Usually used in the filename template.

None
description Optional[str]

Artist note for the workfile entity.

None
prepared_data Optional[SaveWorkfileOptionalData]

Prepared data for speed enhancements.

None
Source code in client/ayon_core/host/interfaces/workfiles.py
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
def save_workfile_with_context(
    self,
    filepath: str,
    folder_entity: dict[str, Any],
    task_entity: dict[str, Any],
    *,
    version: Optional[int] = None,
    comment: Optional[str] = None,
    description: Optional[str] = None,
    prepared_data: Optional[SaveWorkfileOptionalData] = None,
) -> None:
    """Save the current workfile with context.

    Arguments 'rootless_path', 'workfile_entities', 'project_entity'
        and 'anatomy' can be filled to enhance efficiency if you already
        have access to the values.

    Argument 'project_settings' is used to calculate 'rootless_path'
        if it is not provided.

    Notes:
        Should this method care about context change?

    Args:
        filepath (str): Where the current scene should be saved.
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        version (Optional[int]): Version of the workfile. Information
            for workfile entity. Recommended to fill.
        comment (Optional[str]): Comment for the workfile.
            Usually used in the filename template.
        description (Optional[str]): Artist note for the workfile entity.
        prepared_data (Optional[SaveWorkfileOptionalData]): Prepared data
            for speed enhancements.

    """
    project_name = self.get_current_project_name()
    save_workfile_context = get_save_workfile_context(
        project_name,
        filepath,
        folder_entity,
        task_entity,
        host_name=self.name,
        prepared_data=prepared_data,
    )

    self._before_workfile_save(save_workfile_context)
    event_data = self._get_workfile_event_data(
        project_name,
        folder_entity,
        task_entity,
        filepath,
    )
    self._emit_workfile_save_event(event_data, after_save=False)

    workdir = os.path.dirname(filepath)
    if not os.path.exists(workdir):
        os.makedirs(workdir, exist_ok=True)

    # Set 'AYON_WORKDIR' environment variable
    os.environ["AYON_WORKDIR"] = workdir

    self.set_current_context(
        folder_entity,
        task_entity,
        reason=ContextChangeReason.workfile_save,
        project_entity=save_workfile_context.project_entity,
        anatomy=save_workfile_context.anatomy,
    )

    self.save_workfile(filepath)

    self._save_workfile_entity(
        save_workfile_context,
        version,
        comment,
        description,
    )
    self._after_workfile_save(save_workfile_context)
    self._emit_workfile_save_event(event_data)

workfile_has_unsaved_changes()

Currently opened scene is saved.

Not all hosts can know if the current scene is saved because the API of DCC does not support it.

Returns:

Type Description
Optional[bool]

Optional[bool]: True if scene is saved and False if has unsaved modifications. None if can't tell if workfiles has modifications.

Source code in client/ayon_core/host/interfaces/workfiles.py
864
865
866
867
868
869
870
871
872
873
874
875
876
def workfile_has_unsaved_changes(self) -> Optional[bool]:
    """Currently opened scene is saved.

    Not all hosts can know if the current scene is saved because the API
        of DCC does not support it.

    Returns:
        Optional[bool]: True if scene is saved and False if has unsaved
            modifications. None if can't tell if workfiles has
            modifications.

    """
    return None

PublishedWorkfileInfo dataclass

Information about published workfile.

Host can copy and use the workfile using this information object.

Attributes:

Name Type Description
project_name str

Name of the project where workfile lives.

folder_id str

Folder id under which is workfile stored.

task_id Optional[str]

Task id under which is workfile stored.

representation_id str

Representation id of the workfile.

filepath str

Path to the workfile.

created_at float

Timestamp when the workfile representation was created.

author str

Author of the workfile representation.

available bool

True if workfile is available on the machine.

file_size Optional[float]

Size of the workfile in bytes.

file_created Optional[float]

Timestamp when the workfile was created on the filesystem.

file_modified Optional[float]

Timestamp when the workfile was modified on the filesystem.

Source code in client/ayon_core/host/interfaces/workfiles.py
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
@dataclass
class PublishedWorkfileInfo:
    """Information about published workfile.

    Host can copy and use the workfile using this information object.

    Attributes:
        project_name (str): Name of the project where workfile lives.
        folder_id (str): Folder id under which is workfile stored.
        task_id (Optional[str]): Task id under which is workfile stored.
        representation_id (str): Representation id of the workfile.
        filepath (str): Path to the workfile.
        created_at (float): Timestamp when the workfile representation
            was created.
        author (str): Author of the workfile representation.
        available (bool): True if workfile is available on the machine.
        file_size (Optional[float]): Size of the workfile in bytes.
        file_created (Optional[float]): Timestamp when the workfile was
            created on the filesystem.
        file_modified (Optional[float]): Timestamp when the workfile was
            modified on the filesystem.

    """
    project_name: str
    folder_id: str
    task_id: Optional[str]
    representation_id: str
    filepath: str
    created_at: float
    author: str
    available: bool
    file_size: Optional[float]
    file_created: Optional[float]
    file_modified: Optional[float]

    @classmethod
    def new(
        cls,
        project_name: str,
        folder_id: str,
        task_id: Optional[str],
        repre_entity: dict[str, Any],
        *,
        filepath: str,
        author: str,
        available: bool,
        file_size: Optional[float],
        file_modified: Optional[float],
        file_created: Optional[float],
    ) -> "PublishedWorkfileInfo":
        created_at = arrow.get(repre_entity["createdAt"]).to("local")

        return cls(
            project_name=project_name,
            folder_id=folder_id,
            task_id=task_id,
            representation_id=repre_entity["id"],
            filepath=filepath,
            created_at=created_at.float_timestamp,
            author=author,
            available=available,
            file_size=file_size,
            file_created=file_created,
            file_modified=file_modified,
        )

    def to_data(self) -> dict[str, Any]:
        """Converts file item to data.

        Returns:
            dict[str, Any]: Workfile item data.

        """
        return asdict(self)

    @classmethod
    def from_data(cls, data: dict[str, Any]) -> "PublishedWorkfileInfo":
        """Converts data to workfile item.

        Args:
            data (dict[str, Any]): Workfile item data.

        Returns:
            PublishedWorkfileInfo: File item.

        """
        return PublishedWorkfileInfo(**data)

from_data(data) classmethod

Converts data to workfile item.

Parameters:

Name Type Description Default
data dict[str, Any]

Workfile item data.

required

Returns:

Name Type Description
PublishedWorkfileInfo 'PublishedWorkfileInfo'

File item.

Source code in client/ayon_core/host/interfaces/workfiles.py
811
812
813
814
815
816
817
818
819
820
821
822
@classmethod
def from_data(cls, data: dict[str, Any]) -> "PublishedWorkfileInfo":
    """Converts data to workfile item.

    Args:
        data (dict[str, Any]): Workfile item data.

    Returns:
        PublishedWorkfileInfo: File item.

    """
    return PublishedWorkfileInfo(**data)

to_data()

Converts file item to data.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Workfile item data.

Source code in client/ayon_core/host/interfaces/workfiles.py
802
803
804
805
806
807
808
809
def to_data(self) -> dict[str, Any]:
    """Converts file item to data.

    Returns:
        dict[str, Any]: Workfile item data.

    """
    return asdict(self)

WorkfileInfo dataclass

Information about workfile.

Host can open, copy and use the workfile using this information object.

Attributes:

Name Type Description
filepath str

Path to the workfile.

rootless_path str

Path to the workfile without the root. And without backslashes on Windows.

version Optional[int]

Version of the workfile.

comment Optional[str]

Comment of the workfile.

file_size Optional[float]

Size of the workfile in bytes.

file_created Optional[float]

Timestamp when the workfile was created on the filesystem.

file_modified Optional[float]

Timestamp when the workfile was modified on the filesystem.

workfile_entity_id Optional[str]

Workfile entity id. If None then the workfile is not in the database.

description str

Description of the workfile.

created_by Optional[str]

User id of the user who created the workfile entity.

updated_by Optional[str]

User id of the user who updated the workfile entity.

available bool

True if workfile is available on the machine.

Source code in client/ayon_core/host/interfaces/workfiles.py
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
@dataclass
class WorkfileInfo:
    """Information about workfile.

    Host can open, copy and use the workfile using this information object.

    Attributes:
        filepath (str): Path to the workfile.
        rootless_path (str): Path to the workfile without the root. And without
            backslashes on Windows.
        version (Optional[int]): Version of the workfile.
        comment (Optional[str]): Comment of the workfile.
        file_size (Optional[float]): Size of the workfile in bytes.
        file_created (Optional[float]): Timestamp when the workfile was
            created on the filesystem.
        file_modified (Optional[float]): Timestamp when the workfile was
            modified on the filesystem.
        workfile_entity_id (Optional[str]): Workfile entity id. If None then
            the workfile is not in the database.
        description (str): Description of the workfile.
        created_by (Optional[str]): User id of the user who created the
            workfile entity.
        updated_by (Optional[str]): User id of the user who updated the
            workfile entity.
        available (bool): True if workfile is available on the machine.

    """
    filepath: str
    rootless_path: str
    version: Optional[int]
    comment: Optional[str]
    file_size: Optional[float]
    file_created: Optional[float]
    file_modified: Optional[float]
    workfile_entity_id: Optional[str]
    description: str
    created_by: Optional[str]
    updated_by: Optional[str]
    available: bool

    @classmethod
    def new(
        cls,
        filepath: str,
        rootless_path: str,
        *,
        version: Optional[int],
        comment: Optional[str],
        available: bool,
        workfile_entity: dict[str, Any],
    ):
        file_size = file_modified = file_created = None
        if filepath and os.path.exists(filepath):
            filestat = os.stat(filepath)
            file_size = filestat.st_size
            file_created = filestat.st_ctime
            file_modified = filestat.st_mtime

        if workfile_entity is None:
            workfile_entity = {}

        attrib = {}
        if workfile_entity:
            attrib = workfile_entity["attrib"]

        return cls(
            filepath=filepath,
            rootless_path=rootless_path,
            version=version,
            comment=comment,
            file_size=file_size,
            file_created=file_created,
            file_modified=file_modified,
            workfile_entity_id=workfile_entity.get("id"),
            description=attrib.get("description") or "",
            created_by=workfile_entity.get("createdBy"),
            updated_by=workfile_entity.get("updatedBy"),
            available=available,
        )

    def to_data(self) -> dict[str, Any]:
        """Converts file item to data.

        Returns:
            dict[str, Any]: Workfile item data.

        """
        return asdict(self)

    @classmethod
    def from_data(cls, data: dict[str, Any]) -> WorkfileInfo:
        """Converts data to workfile item.

        Args:
            data (dict[str, Any]): Workfile item data.

        Returns:
            WorkfileInfo: File item.

        """
        return WorkfileInfo(**data)

from_data(data) classmethod

Converts data to workfile item.

Parameters:

Name Type Description Default
data dict[str, Any]

Workfile item data.

required

Returns:

Name Type Description
WorkfileInfo WorkfileInfo

File item.

Source code in client/ayon_core/host/interfaces/workfiles.py
722
723
724
725
726
727
728
729
730
731
732
733
@classmethod
def from_data(cls, data: dict[str, Any]) -> WorkfileInfo:
    """Converts data to workfile item.

    Args:
        data (dict[str, Any]): Workfile item data.

    Returns:
        WorkfileInfo: File item.

    """
    return WorkfileInfo(**data)

to_data()

Converts file item to data.

Returns:

Type Description
dict[str, Any]

dict[str, Any]: Workfile item data.

Source code in client/ayon_core/host/interfaces/workfiles.py
713
714
715
716
717
718
719
720
def to_data(self) -> dict[str, Any]:
    """Converts file item to data.

    Returns:
        dict[str, Any]: Workfile item data.

    """
    return asdict(self)