Skip to content

integrate_reviewables

Integrate SyncSketch reviewables.

IntegrateReviewables

Bases: InstancePlugin, AYONPyblishPluginMixin

Integrate SyncSketch reviewables.

Uploads representations as reviewables to SyncSketch. Representations need to be tagged with "syncsketchreview" tag.

Source code in client/ayon_syncsketch/plugins/publish/integrate_reviewables.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
 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
class IntegrateReviewables(pyblish.api.InstancePlugin,
                           AYONPyblishPluginMixin):
    """Integrate SyncSketch reviewables.

    Uploads representations as reviewables to SyncSketch.
    Representations need to be tagged with "syncsketchreview" tag.
    """
    settings_category = "syncsketch"

    order = pyblish.api.IntegratorOrder
    label = "Integrate SyncSketch reviewables"

    representation_tag = "syncsketchreview"
    review_item_profiles = []

    def filter_review_item_profiles(
        self, family, host_name, task_name, task_type
    ):
        if not self.review_item_profiles:
            return []

        filtering_criteria = {
            "families": family,
            "hosts": host_name,
            "tasks": task_name,
            "task_types": task_type
        }

        matching_profile = filter_profiles(
            self.review_item_profiles, filtering_criteria)

        self.log.debug("Matching profile: {}".format(matching_profile))
        return matching_profile

    def _format_template(self, instance, template, formatting_data):
        """Format template with data from instance and anatomy data."""

        nested_keys = ["folder", "task"]
        fill_pairs_keys = [
            "variant", "family", "app", "user", "subset",
            "host", "output", "ext", "name", "short", "version",
            "type"
        ]

        def prepare_template_data_pairs(data):
            """Prepare data for template formatting."""
            fill_pairs = {}
            for key, value in data.items():
                if key in nested_keys and isinstance(value, dict):
                    self.log.debug("Nested key: {}:{}".format(key, value))
                    fill_pairs[key] = prepare_template_data_pairs(value)
                elif key in fill_pairs_keys and isinstance(value, str):
                    self.log.debug("Key: {}:{}".format(key, value))
                    fill_pairs.update(prepare_template_data({key: value}))
                elif key in fill_pairs_keys and not isinstance(value, str):
                    fill_pairs[key] = value
            return fill_pairs

        formatting_data = deepcopy(formatting_data)
        host_name = instance.context.data["hostName"]
        variant = instance.data["variant"]
        formatting_data.update({
            "variant": variant,
            "app": host_name,
            "host": host_name
        })

        formatting_data_pairs = prepare_template_data_pairs(formatting_data)
        self.log.debug("Formatting data pairs: {}".format(
            pformat(formatting_data_pairs)
        ))

        formatted_name = StringTemplate(
                template).format(formatting_data_pairs)

        self.log.debug("Formatted name: {}".format(formatted_name))

        return formatted_name

    def process(self, instance):
        self.log.info("Integrating SyncSketch reviewables...")

        # get the attribute values from data
        instance.data["attributeValues"] = self.get_attr_values_from_data(
            instance.data)
        upload_to_syncsketch = instance.data["attributeValues"].get(
            "SyncSketchUpload", True)

        # skip if instance without representation
        representations = [
            repre for repre in instance.data.get("representations", [])
            if repre.get("tags", []) and self.representation_tag in repre["tags"]  # noqa: E501
        ]
        if not representations or not upload_to_syncsketch:
            self.log.info("Skipping SyncSketch publishing: `{}`".format(
                instance.data["name"]))
            return

        context = instance.context
        user_name = context.data["user"]
        anatomy_data = instance.data["anatomyData"]
        syncsketch_id = context.data["syncsketchProjectId"]
        server_config = context.data["syncsketchServerConfig"]
        version_entity = instance.data["versionEntity"]
        server_handler = self.get_server_handler(context, server_config)

        self.log.debug("Syncsketch Project ID: {}".format(syncsketch_id))
        self.log.debug("Version entity id: {}".format(version_entity["_id"]))

        # making sure the server is available
        response = server_handler.is_connected()
        if not response:
            raise requests.exceptions.ConnectionError(
                "SyncSketch connection failed.")

        # filter review item profiles
        self.matching_profile = self.filter_review_item_profiles(
            instance.data["family"],
            instance.context.data["hostName"],
            anatomy_data["task"]["name"],
            anatomy_data["task"]["type"]
        )

        if not self.matching_profile:
            raise KnownPublishError(
                "No matching profile for SyncSketch review item found.")

        # get review list the project
        review_list_id = self.get_review_list_id(
            instance, server_handler, syncsketch_id)

        self.log.info("Review list ID: {}".format(review_list_id))

        review_item_id, review_item_name = self.upload_reviewable(
            instance,
            representations,
            server_handler,
            review_list_id,
            user_name
        )

        # update version entity with review item ID
        if review_item_id:
            # update review item with avalon version entity ID
            self.update_version_entity(
                server_handler,
                version_entity,
                review_item_id,
                review_item_name
            )

            version_attributes = instance.data.get("versionAttributes", {})
            version_attributes["syncsketchId"] = review_item_id
            instance.data["versionAttributes"] = version_attributes
        else:
            raise KnownPublishError("SyncSketch upload failed.")

    def update_version_entity(
            self, server_handler, version_entity,
            review_item_id, review_item_name
        ):
        """Update version entity with review item ID."""
        data = {
            # updating name without extension (duplicity issue)
            "name": os.path.splitext(review_item_name)[0],
            "metadata": json.dumps({"ayonVersionID": version_entity["_id"]})
        }
        self.log.debug("Version entity id: {}".format(version_entity["_id"]))
        self.log.debug("Review item id: {}".format(review_item_id))

        # update review item with version entity ID
        result = server_handler.update_review_item(review_item_id, data)

        self.log.debug("Review item updated: {}".format(result))

    def upload_reviewable(
        self, instance, representations,
        server_handler, review_list_id, user_name
    ):
        """Upload reviewable to SyncSketch."""
        anatomy_data = instance.data["anatomyData"]

        # loop representations representations with tag "syncsketchreview"
        for representation in representations:
            formatting_data = deepcopy(anatomy_data)
            # update anatomy data with representation data
            formatting_data["ext"] = representation["ext"]
            if representation.get("output"):
                formatting_data["output"] = representation["output"]

            # solving the review item name
            review_item_name_template = self.matching_profile[
                "review_item_name_template"]

            if not review_item_name_template:
                raise KnownPublishError(
                    "Name in matching profile for "
                    "SyncSketch review item not filled. "
                    "\n\nProfile data: {}".format(
                        self.matching_profile)
                )

            self.log.debug("Review item name template: {}".format(
                review_item_name_template))

            # add extension
            review_item_name_template = (
                review_item_name_template + " .{ext}")

            # format the file_name template
            review_item_name = self._format_template(
                instance, review_item_name_template, formatting_data
            )

            # get the file path only for single file representations
            file_path = os.path.join(
                representation["stagingDir"], representation["files"]
            )

            self.log.debug("Uploading reviewable: {}".format(file_path))
            self.log.debug("Review item name: {}".format(review_item_name))

            # upload them to SyncSketch
            response = server_handler.upload_review_item(
                review_list_id, file_path,
                artist_name=user_name,
                file_name=review_item_name,
                no_convert_flag=False,
                item_parent_id=False
            )
            # get ID of the uploaded media: response["id"] and review item name
            return response.get("id"), review_item_name

    def get_review_list_id(self, instance, server_handler, project_id):
        """Get review list ID by name."""
        context = instance.context
        # get the review list ID from cached context.data if exists
        review_list_id = context.data.get("review_list_id")
        if review_list_id:
            self.log.debug("Cached Review list ID: {}".format(review_list_id))
            return review_list_id

        # solving the review list name
        matching_profile_review_list_name = self.matching_profile[
            "list_name_template"]

        if not matching_profile_review_list_name:
            raise KnownPublishError(
                "Name in matching profile for "
                "SyncSketch review list not filled. "
                "\n\nProfile data: {}".format(
                    self.matching_profile)
            )

        self.log.debug("Review list name template: {}".format(
                matching_profile_review_list_name))

        # format the review list name template
        review_list_name = self._format_template(
            instance,
            matching_profile_review_list_name,
            instance.data["anatomyData"]
        )

        self.log.debug("Review list Name: {}".format(review_list_name))

        # get the review list ID from SyncSketch project
        response = server_handler.get_reviews_by_project_id(project_id)
        for review in response["objects"]:
            if review["name"] == review_list_name:
                review_list_id = review["id"]
                self.log.debug(
                    "Existing Review list ID: {}".format(review_list_id))
                break

        # if review list not found, create it
        if not review_list_id:
            response = server_handler.create_review(
                project_id, review_list_name)
            review_list_id = response["id"]
            self.log.debug("Created Review list ID: {}".format(review_list_id))

        context.data["review_list_id"] = review_list_id

        return review_list_id

    def get_server_handler(self, context, server_config):
        # cache the server handler into context.data
        if "syncsketchServerHandler" not in context.data:
            context.data["syncsketchServerHandler"] = ServerCommunication(
                user_auth=server_config["auth_user"],
                api_key=server_config["auth_token"],
                host=server_config["url"],
                log=self.log
            )
        return context.data["syncsketchServerHandler"]

    @classmethod
    def get_attribute_defs(cls):
        return [
            BoolDef(
                "SyncSketchUpload",
                default=True,
                label="SyncSketch Upload"
            )
        ]

get_review_list_id(instance, server_handler, project_id)

Get review list ID by name.

Source code in client/ayon_syncsketch/plugins/publish/integrate_reviewables.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def get_review_list_id(self, instance, server_handler, project_id):
    """Get review list ID by name."""
    context = instance.context
    # get the review list ID from cached context.data if exists
    review_list_id = context.data.get("review_list_id")
    if review_list_id:
        self.log.debug("Cached Review list ID: {}".format(review_list_id))
        return review_list_id

    # solving the review list name
    matching_profile_review_list_name = self.matching_profile[
        "list_name_template"]

    if not matching_profile_review_list_name:
        raise KnownPublishError(
            "Name in matching profile for "
            "SyncSketch review list not filled. "
            "\n\nProfile data: {}".format(
                self.matching_profile)
        )

    self.log.debug("Review list name template: {}".format(
            matching_profile_review_list_name))

    # format the review list name template
    review_list_name = self._format_template(
        instance,
        matching_profile_review_list_name,
        instance.data["anatomyData"]
    )

    self.log.debug("Review list Name: {}".format(review_list_name))

    # get the review list ID from SyncSketch project
    response = server_handler.get_reviews_by_project_id(project_id)
    for review in response["objects"]:
        if review["name"] == review_list_name:
            review_list_id = review["id"]
            self.log.debug(
                "Existing Review list ID: {}".format(review_list_id))
            break

    # if review list not found, create it
    if not review_list_id:
        response = server_handler.create_review(
            project_id, review_list_name)
        review_list_id = response["id"]
        self.log.debug("Created Review list ID: {}".format(review_list_id))

    context.data["review_list_id"] = review_list_id

    return review_list_id

update_version_entity(server_handler, version_entity, review_item_id, review_item_name)

Update version entity with review item ID.

Source code in client/ayon_syncsketch/plugins/publish/integrate_reviewables.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def update_version_entity(
        self, server_handler, version_entity,
        review_item_id, review_item_name
    ):
    """Update version entity with review item ID."""
    data = {
        # updating name without extension (duplicity issue)
        "name": os.path.splitext(review_item_name)[0],
        "metadata": json.dumps({"ayonVersionID": version_entity["_id"]})
    }
    self.log.debug("Version entity id: {}".format(version_entity["_id"]))
    self.log.debug("Review item id: {}".format(review_item_id))

    # update review item with version entity ID
    result = server_handler.update_review_item(review_item_id, data)

    self.log.debug("Review item updated: {}".format(result))

upload_reviewable(instance, representations, server_handler, review_list_id, user_name)

Upload reviewable to SyncSketch.

Source code in client/ayon_syncsketch/plugins/publish/integrate_reviewables.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
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
def upload_reviewable(
    self, instance, representations,
    server_handler, review_list_id, user_name
):
    """Upload reviewable to SyncSketch."""
    anatomy_data = instance.data["anatomyData"]

    # loop representations representations with tag "syncsketchreview"
    for representation in representations:
        formatting_data = deepcopy(anatomy_data)
        # update anatomy data with representation data
        formatting_data["ext"] = representation["ext"]
        if representation.get("output"):
            formatting_data["output"] = representation["output"]

        # solving the review item name
        review_item_name_template = self.matching_profile[
            "review_item_name_template"]

        if not review_item_name_template:
            raise KnownPublishError(
                "Name in matching profile for "
                "SyncSketch review item not filled. "
                "\n\nProfile data: {}".format(
                    self.matching_profile)
            )

        self.log.debug("Review item name template: {}".format(
            review_item_name_template))

        # add extension
        review_item_name_template = (
            review_item_name_template + " .{ext}")

        # format the file_name template
        review_item_name = self._format_template(
            instance, review_item_name_template, formatting_data
        )

        # get the file path only for single file representations
        file_path = os.path.join(
            representation["stagingDir"], representation["files"]
        )

        self.log.debug("Uploading reviewable: {}".format(file_path))
        self.log.debug("Review item name: {}".format(review_item_name))

        # upload them to SyncSketch
        response = server_handler.upload_review_item(
            review_list_id, file_path,
            artist_name=user_name,
            file_name=review_item_name,
            no_convert_flag=False,
            item_parent_id=False
        )
        # get ID of the uploaded media: response["id"] and review item name
        return response.get("id"), review_item_name