Skip to content

host

3dequalizer host implementation.

Note

3dequalizer 7.1v2 uses Python 3.7.9 3dequalizer 8.0 uses Python 3.9.x

AYONJSONEncoder

Bases: JSONEncoder

Custom JSON encoder for dataclasses.

Source code in client/ayon_equalizer/api/host.py
49
50
51
52
53
54
55
56
57
58
59
class AYONJSONEncoder(json.JSONEncoder):
    """Custom JSON encoder for dataclasses."""

    def default(self, obj: object) -> Union[dict, object]:
        """Encode dataclasses as dict."""
        if dataclasses.is_dataclass(obj):
            # type: obj: dataclasses.dataclass
            return dataclasses.asdict(obj)
        if isinstance(obj, CreatedInstance):
            return dict(obj)
        return super().default(obj)

default(obj)

Encode dataclasses as dict.

Source code in client/ayon_equalizer/api/host.py
52
53
54
55
56
57
58
59
def default(self, obj: object) -> Union[dict, object]:
    """Encode dataclasses as dict."""
    if dataclasses.is_dataclass(obj):
        # type: obj: dataclasses.dataclass
        return dataclasses.asdict(obj)
    if isinstance(obj, CreatedInstance):
        return dict(obj)
    return super().default(obj)

EqualizerHost

Bases: HostBase, IWorkfileHost, ILoadHost, IPublishHost

3DEqualizer host implementation.

Source code in client/ayon_equalizer/api/host.py
 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
class EqualizerHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
    """3DEqualizer host implementation."""

    name = "equalizer"
    _instance = None

    def __new__(cls):
        """Singleton implementation."""
        # singleton - ensure only one instance of the host is created.
        # This is necessary because 3DEqualizer doesn't have a way to
        # store custom data, so we need to store it in the project notes.
        if not hasattr(cls, "_instance") or not cls._instance:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        """Initialize the host."""
        self._qapp = None
        super().__init__()

    def workfile_has_unsaved_changes(self) -> bool:
        """Return the state of the current workfile.

        3DEqualizer returns state as 1 or zero, so we need to invert it.

        Returns:
            bool: True if the current workfile has unsaved changes.

        """
        return not bool(tde4.isProjectUpToDate())

    def get_workfile_extensions(self) -> list[str]:
        """Return the workfile extensions for 3DEqualizer."""
        return [".3de"]

    def save_workfile(self, dst_path: Optional[str]=None) -> str:
        """Save the current workfile.

        Arguments:
            dst_path (str): Destination path to save the workfile.

        """
        if not dst_path:
            dst_path = tde4.getProjectPath()
        result = tde4.saveProject(dst_path, True)  # noqa: FBT003
        if not bool(result):
            err_msg = f"Failed to save workfile {dst_path}."
            raise RuntimeError(err_msg)

        return dst_path

    def open_workfile(self, filepath: str) -> str:
        """Open a workfile in 3DEqualizer."""
        result = tde4.loadProject(filepath, True)  # noqa: FBT003
        if not bool(result):
            err_msg = f"Failed to open workfile {filepath}."
            raise RuntimeError(err_msg)

        return filepath

    def get_current_workfile(self) -> str:
        """Return the current workfile path."""
        return tde4.getProjectPath()

    def get_containers(self) -> Generator[Container, Any, Optional[list]]:
        """Get containers from the current workfile."""
        # sourcery skip: use-named-expression
        data = self.get_ayon_data() or {}
        for container in data.get(EQUALIZER_CONTAINERS_KEY, []):
            # convert dict to dataclass
            _container = Container(**container)
            # check if the container is valid
            if _container.name and _container.namespace:
                yield dataclasses.asdict(_container)

    def add_container(self, container: Container) -> None:
        """Add a container to the current workfile.

        Args:
            container (Container): Container to add.

        """
        data = self.get_ayon_data()

        containers = [
            _container for _container in self.get_containers()
            # Remove existing container with the same name and namespace to
            # avoid duplicates.
            if not (
                container.name == _container["name"]
                and container.namespace == _container["namespace"]
            )
        ]
        data[EQUALIZER_CONTAINERS_KEY] = [
            *containers, dataclasses.asdict(container)
        ]

        self.update_ayon_data(data)

    def _create_ayon_data(self) -> None:
        """Create AYON data in the current project."""
        tde4.setProjectNotes(
            f"{tde4.getProjectNotes()}\n"
            f"{AYON_METADATA_GUARD}\n")
        # this is really necessary otherwise the data is not saved
        tde4.updateGUI()

    def get_ayon_data(self) -> dict:
        """Get AYON context data from the current project.

        3Dequalizer doesn't have any custom node or other
        place to store metadata, so we store context data in
        the project notes encoded as JSON and wrapped in a
        special guard string `AYON_CONTEXT::...::AYON_CONTEXT_END`.

        Returns:
            dict: Context data.

        """
        # sourcery skip: use-named-expression
        m = re.search(AYON_METADATA_REGEX, tde4.getProjectNotes())
        if not m:
            self._create_ayon_data()
            return {}
        try:
            context = json.loads(m["context"]) if m else {}
        except ValueError:
            self.log.debug("AYON data is not valid json")
            # AYON data not found or invalid, create empty placeholder
            self._create_ayon_data()
            return {}

        return context

    def update_ayon_data(self, data: dict) -> None:
        """Update AYON context data in the current project.

        Serialize context data as json and store it in the
        project notes. If the context data is not found, create
        a placeholder there. See `get_context_data` for more info.

        Args:
            data (dict): Context data.

        """
        original_data = self.get_ayon_data()

        updated_data = original_data.copy()
        updated_data.update(data)
        update_str = json.dumps(
            updated_data or {}, indent=4, cls=AYONJSONEncoder)

        tde4.setProjectNotes(
            re.sub(
                AYON_METADATA_REGEX,
                AYON_METADATA_GUARD.format(update_str),
                tde4.getProjectNotes(),
            )
        )
        tde4.updateGUI()

    def get_context_data(self) -> dict:
        """Get context data from the current project."""
        data = self.get_ayon_data()

        return data.get(EQUALIZER_CONTEXT_KEY, {})

    def update_context_data(self, data: dict, changes: dict) -> None:
        """Update context data in the current project.

        Args:
            data (dict): Context data.
            changes (dict): Changes to the context data.

        Raises:
            RuntimeError: If the context data is not found.

        """
        if not data:
            return
        ayon_data = self.get_ayon_data()
        ayon_data[EQUALIZER_CONTEXT_KEY] = data
        self.update_ayon_data(ayon_data)


    def get_publish_instances(self) -> list[dict]:
        """Get publish instances from the current project."""
        data = self.get_ayon_data()
        return data.get(EQUALIZER_INSTANCES_KEY, [])

    def add_publish_instance(self, instance_data: dict) -> None:
        """Add a publish instance to the current project.

        Args:
            instance_data (dict): Publish instance to add.

        """
        data = self.get_ayon_data()
        publish_instances = self.get_publish_instances()
        publish_instances.append(instance_data)
        data[EQUALIZER_INSTANCES_KEY] = publish_instances

        self.update_ayon_data(data)

    def update_publish_instance(
            self,
            instance_id: str,
            data: dict,
    ) -> None:
        """Update a publish instance in the current project.

        Args:
            instance_id (str): Publish instance id to update.
            data (dict): Data to update.

        """
        ayon_data = self.get_ayon_data()
        publish_instances = self.get_publish_instances()
        for idx, publish_instance in enumerate(publish_instances):
            if publish_instance["instance_id"] == instance_id:
                publish_instances[idx] = data
                break
        ayon_data[EQUALIZER_INSTANCES_KEY] = publish_instances

        self.update_ayon_data(ayon_data)

    def write_create_instances(
            self, instances: list[dict]) -> None:
        """Write publish instances to the current project."""
        ayon_data = self.get_ayon_data()
        ayon_data[EQUALIZER_INSTANCES_KEY] = instances
        self.update_ayon_data(ayon_data)

    def remove_create_instance(self, instance_id: str) -> None:
        """Remove a publish instance from the current project.

        Args:
            instance_id (str): Publish instance id to remove.

        """
        data = self.get_ayon_data()
        publish_instances = self.get_publish_instances()
        publish_instances = [
            publish_instance
            for publish_instance in publish_instances
            if publish_instance["instance_id"] != instance_id
        ]
        data[EQUALIZER_INSTANCES_KEY] = publish_instances

        self.update_ayon_data(data)


    def install(self) -> None:
        """Install the host."""
        if not QtCore.QCoreApplication.instance():
            app = QtWidgets.QApplication([])
            self._qapp = app
            self._qapp.setQuitOnLastWindowClosed(False)

        pyblish.api.register_host("equalizer")

        pyblish.api.register_plugin_path(PUBLISH_PATH)
        register_loader_plugin_path(LOAD_PATH)
        register_creator_plugin_path(CREATE_PATH)

        try:
            heartbeat_interval = int(
                os.getenv("AYON_TDE4_HEARTBEAT_INTERVAL")) or 100
        except (ValueError, TypeError):
            self.log.warning(
                "AYON_TDE4_HEARTBEAT_INTERVAL is not a valid integer")
            heartbeat_interval = 100

        tde4.setTimerCallbackFunction(
            "EqualizerHost._timer", heartbeat_interval)

    @staticmethod
    def _timer() -> None:
        """Timer callback function."""
        QtWidgets.QApplication.instance().processEvents(
            QtCore.QEventLoop.AllEvents)

    @classmethod
    def get_host(cls) -> EqualizerHost:
        """Get the host instance."""
        return cls._instance

    def get_main_window(self) -> QtWidgets.QWidget:
        """Get the main window of the host application."""
        return self._qapp.activeWindow()

__init__()

Initialize the host.

Source code in client/ayon_equalizer/api/host.py
77
78
79
80
def __init__(self):
    """Initialize the host."""
    self._qapp = None
    super().__init__()

__new__()

Singleton implementation.

Source code in client/ayon_equalizer/api/host.py
68
69
70
71
72
73
74
75
def __new__(cls):
    """Singleton implementation."""
    # singleton - ensure only one instance of the host is created.
    # This is necessary because 3DEqualizer doesn't have a way to
    # store custom data, so we need to store it in the project notes.
    if not hasattr(cls, "_instance") or not cls._instance:
        cls._instance = super().__new__(cls)
    return cls._instance

add_container(container)

Add a container to the current workfile.

Parameters:

Name Type Description Default
container Container

Container to add.

required
Source code in client/ayon_equalizer/api/host.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def add_container(self, container: Container) -> None:
    """Add a container to the current workfile.

    Args:
        container (Container): Container to add.

    """
    data = self.get_ayon_data()

    containers = [
        _container for _container in self.get_containers()
        # Remove existing container with the same name and namespace to
        # avoid duplicates.
        if not (
            container.name == _container["name"]
            and container.namespace == _container["namespace"]
        )
    ]
    data[EQUALIZER_CONTAINERS_KEY] = [
        *containers, dataclasses.asdict(container)
    ]

    self.update_ayon_data(data)

add_publish_instance(instance_data)

Add a publish instance to the current project.

Parameters:

Name Type Description Default
instance_data dict

Publish instance to add.

required
Source code in client/ayon_equalizer/api/host.py
252
253
254
255
256
257
258
259
260
261
262
263
264
def add_publish_instance(self, instance_data: dict) -> None:
    """Add a publish instance to the current project.

    Args:
        instance_data (dict): Publish instance to add.

    """
    data = self.get_ayon_data()
    publish_instances = self.get_publish_instances()
    publish_instances.append(instance_data)
    data[EQUALIZER_INSTANCES_KEY] = publish_instances

    self.update_ayon_data(data)

get_ayon_data()

Get AYON context data from the current project.

3Dequalizer doesn't have any custom node or other place to store metadata, so we store context data in the project notes encoded as JSON and wrapped in a special guard string AYON_CONTEXT::...::AYON_CONTEXT_END.

Returns:

Name Type Description
dict dict

Context data.

Source code in client/ayon_equalizer/api/host.py
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
def get_ayon_data(self) -> dict:
    """Get AYON context data from the current project.

    3Dequalizer doesn't have any custom node or other
    place to store metadata, so we store context data in
    the project notes encoded as JSON and wrapped in a
    special guard string `AYON_CONTEXT::...::AYON_CONTEXT_END`.

    Returns:
        dict: Context data.

    """
    # sourcery skip: use-named-expression
    m = re.search(AYON_METADATA_REGEX, tde4.getProjectNotes())
    if not m:
        self._create_ayon_data()
        return {}
    try:
        context = json.loads(m["context"]) if m else {}
    except ValueError:
        self.log.debug("AYON data is not valid json")
        # AYON data not found or invalid, create empty placeholder
        self._create_ayon_data()
        return {}

    return context

get_containers()

Get containers from the current workfile.

Source code in client/ayon_equalizer/api/host.py
126
127
128
129
130
131
132
133
134
135
def get_containers(self) -> Generator[Container, Any, Optional[list]]:
    """Get containers from the current workfile."""
    # sourcery skip: use-named-expression
    data = self.get_ayon_data() or {}
    for container in data.get(EQUALIZER_CONTAINERS_KEY, []):
        # convert dict to dataclass
        _container = Container(**container)
        # check if the container is valid
        if _container.name and _container.namespace:
            yield dataclasses.asdict(_container)

get_context_data()

Get context data from the current project.

Source code in client/ayon_equalizer/api/host.py
223
224
225
226
227
def get_context_data(self) -> dict:
    """Get context data from the current project."""
    data = self.get_ayon_data()

    return data.get(EQUALIZER_CONTEXT_KEY, {})

get_current_workfile()

Return the current workfile path.

Source code in client/ayon_equalizer/api/host.py
122
123
124
def get_current_workfile(self) -> str:
    """Return the current workfile path."""
    return tde4.getProjectPath()

get_host() classmethod

Get the host instance.

Source code in client/ayon_equalizer/api/host.py
344
345
346
347
@classmethod
def get_host(cls) -> EqualizerHost:
    """Get the host instance."""
    return cls._instance

get_main_window()

Get the main window of the host application.

Source code in client/ayon_equalizer/api/host.py
349
350
351
def get_main_window(self) -> QtWidgets.QWidget:
    """Get the main window of the host application."""
    return self._qapp.activeWindow()

get_publish_instances()

Get publish instances from the current project.

Source code in client/ayon_equalizer/api/host.py
247
248
249
250
def get_publish_instances(self) -> list[dict]:
    """Get publish instances from the current project."""
    data = self.get_ayon_data()
    return data.get(EQUALIZER_INSTANCES_KEY, [])

get_workfile_extensions()

Return the workfile extensions for 3DEqualizer.

Source code in client/ayon_equalizer/api/host.py
93
94
95
def get_workfile_extensions(self) -> list[str]:
    """Return the workfile extensions for 3DEqualizer."""
    return [".3de"]

install()

Install the host.

Source code in client/ayon_equalizer/api/host.py
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
def install(self) -> None:
    """Install the host."""
    if not QtCore.QCoreApplication.instance():
        app = QtWidgets.QApplication([])
        self._qapp = app
        self._qapp.setQuitOnLastWindowClosed(False)

    pyblish.api.register_host("equalizer")

    pyblish.api.register_plugin_path(PUBLISH_PATH)
    register_loader_plugin_path(LOAD_PATH)
    register_creator_plugin_path(CREATE_PATH)

    try:
        heartbeat_interval = int(
            os.getenv("AYON_TDE4_HEARTBEAT_INTERVAL")) or 100
    except (ValueError, TypeError):
        self.log.warning(
            "AYON_TDE4_HEARTBEAT_INTERVAL is not a valid integer")
        heartbeat_interval = 100

    tde4.setTimerCallbackFunction(
        "EqualizerHost._timer", heartbeat_interval)

open_workfile(filepath)

Open a workfile in 3DEqualizer.

Source code in client/ayon_equalizer/api/host.py
113
114
115
116
117
118
119
120
def open_workfile(self, filepath: str) -> str:
    """Open a workfile in 3DEqualizer."""
    result = tde4.loadProject(filepath, True)  # noqa: FBT003
    if not bool(result):
        err_msg = f"Failed to open workfile {filepath}."
        raise RuntimeError(err_msg)

    return filepath

remove_create_instance(instance_id)

Remove a publish instance from the current project.

Parameters:

Name Type Description Default
instance_id str

Publish instance id to remove.

required
Source code in client/ayon_equalizer/api/host.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
def remove_create_instance(self, instance_id: str) -> None:
    """Remove a publish instance from the current project.

    Args:
        instance_id (str): Publish instance id to remove.

    """
    data = self.get_ayon_data()
    publish_instances = self.get_publish_instances()
    publish_instances = [
        publish_instance
        for publish_instance in publish_instances
        if publish_instance["instance_id"] != instance_id
    ]
    data[EQUALIZER_INSTANCES_KEY] = publish_instances

    self.update_ayon_data(data)

save_workfile(dst_path=None)

Save the current workfile.

Parameters:

Name Type Description Default
dst_path str

Destination path to save the workfile.

None
Source code in client/ayon_equalizer/api/host.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def save_workfile(self, dst_path: Optional[str]=None) -> str:
    """Save the current workfile.

    Arguments:
        dst_path (str): Destination path to save the workfile.

    """
    if not dst_path:
        dst_path = tde4.getProjectPath()
    result = tde4.saveProject(dst_path, True)  # noqa: FBT003
    if not bool(result):
        err_msg = f"Failed to save workfile {dst_path}."
        raise RuntimeError(err_msg)

    return dst_path

update_ayon_data(data)

Update AYON context data in the current project.

Serialize context data as json and store it in the project notes. If the context data is not found, create a placeholder there. See get_context_data for more info.

Parameters:

Name Type Description Default
data dict

Context data.

required
Source code in client/ayon_equalizer/api/host.py
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
def update_ayon_data(self, data: dict) -> None:
    """Update AYON context data in the current project.

    Serialize context data as json and store it in the
    project notes. If the context data is not found, create
    a placeholder there. See `get_context_data` for more info.

    Args:
        data (dict): Context data.

    """
    original_data = self.get_ayon_data()

    updated_data = original_data.copy()
    updated_data.update(data)
    update_str = json.dumps(
        updated_data or {}, indent=4, cls=AYONJSONEncoder)

    tde4.setProjectNotes(
        re.sub(
            AYON_METADATA_REGEX,
            AYON_METADATA_GUARD.format(update_str),
            tde4.getProjectNotes(),
        )
    )
    tde4.updateGUI()

update_context_data(data, changes)

Update context data in the current project.

Parameters:

Name Type Description Default
data dict

Context data.

required
changes dict

Changes to the context data.

required

Raises:

Type Description
RuntimeError

If the context data is not found.

Source code in client/ayon_equalizer/api/host.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
def update_context_data(self, data: dict, changes: dict) -> None:
    """Update context data in the current project.

    Args:
        data (dict): Context data.
        changes (dict): Changes to the context data.

    Raises:
        RuntimeError: If the context data is not found.

    """
    if not data:
        return
    ayon_data = self.get_ayon_data()
    ayon_data[EQUALIZER_CONTEXT_KEY] = data
    self.update_ayon_data(ayon_data)

update_publish_instance(instance_id, data)

Update a publish instance in the current project.

Parameters:

Name Type Description Default
instance_id str

Publish instance id to update.

required
data dict

Data to update.

required
Source code in client/ayon_equalizer/api/host.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
def update_publish_instance(
        self,
        instance_id: str,
        data: dict,
) -> None:
    """Update a publish instance in the current project.

    Args:
        instance_id (str): Publish instance id to update.
        data (dict): Data to update.

    """
    ayon_data = self.get_ayon_data()
    publish_instances = self.get_publish_instances()
    for idx, publish_instance in enumerate(publish_instances):
        if publish_instance["instance_id"] == instance_id:
            publish_instances[idx] = data
            break
    ayon_data[EQUALIZER_INSTANCES_KEY] = publish_instances

    self.update_ayon_data(ayon_data)

workfile_has_unsaved_changes()

Return the state of the current workfile.

3DEqualizer returns state as 1 or zero, so we need to invert it.

Returns:

Name Type Description
bool bool

True if the current workfile has unsaved changes.

Source code in client/ayon_equalizer/api/host.py
82
83
84
85
86
87
88
89
90
91
def workfile_has_unsaved_changes(self) -> bool:
    """Return the state of the current workfile.

    3DEqualizer returns state as 1 or zero, so we need to invert it.

    Returns:
        bool: True if the current workfile has unsaved changes.

    """
    return not bool(tde4.isProjectUpToDate())

write_create_instances(instances)

Write publish instances to the current project.

Source code in client/ayon_equalizer/api/host.py
288
289
290
291
292
293
def write_create_instances(
        self, instances: list[dict]) -> None:
    """Write publish instances to the current project."""
    ayon_data = self.get_ayon_data()
    ayon_data[EQUALIZER_INSTANCES_KEY] = instances
    self.update_ayon_data(ayon_data)