Skip to content

Commit 3d4e713

Browse files
fix(LAB-3532): handle external_ids arg for add_metadata and set_metadata (#1891)
Co-authored-by: paulruelle <[email protected]>
1 parent 5b019d8 commit 3d4e713

File tree

2 files changed

+175
-13
lines changed

2 files changed

+175
-13
lines changed

src/kili/entrypoints/mutations/asset/__init__.py

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions
1111
from kili.core.helpers import is_empty_list_with_warning
1212
from kili.core.utils.pagination import mutate_from_paginated_call
13-
from kili.domain.asset import AssetFilters, AssetId
13+
from kili.domain.asset import AssetExternalId, AssetFilters, AssetId
1414
from kili.domain.project import ProjectId
1515
from kili.entrypoints.base import BaseOperationEntrypointMixin
1616
from kili.entrypoints.mutations.asset.helpers import (
@@ -412,16 +412,18 @@ def generate_variables(batch: Dict) -> Dict:
412412
def add_metadata(
413413
self,
414414
json_metadata: List[Dict[str, Union[str, int, float]]],
415-
asset_ids: List[str],
416415
project_id: str,
416+
asset_ids: Optional[List[str]] = None,
417+
external_ids: Optional[List[str]] = None,
417418
) -> List[Dict[Literal["id"], str]]:
418419
"""Add metadata to assets without overriding existing metadata.
419420
420421
Args:
421422
json_metadata: List of metadata dictionaries to add to each asset.
422423
Each dictionary contains key/value pairs to be added to the asset's metadata.
423-
asset_ids: The asset IDs to modify.
424424
project_id: The project ID.
425+
asset_ids: The asset IDs to modify.
426+
external_ids: The external asset IDs to modify (if `asset_ids` is not already provided).
425427
426428
Returns:
427429
A list of dictionaries with the asset ids.
@@ -432,21 +434,39 @@ def add_metadata(
432434
{"key1": "value1", "key2": "value2"},
433435
{"key3": "value3"}
434436
],
435-
asset_ids=["ckg22d81r0jrg0885unmuswj8", "ckg22d81s0jrh0885pdxfd03n"],
436-
project_id="cm92to3cx012u7l0w6kij9qvx"
437+
project_id="cm92to3cx012u7l0w6kij9qvx",
438+
asset_ids=["ckg22d81r0jrg0885unmuswj8", "ckg22d81s0jrh0885pdxfd03n"]
439+
)
440+
441+
# Or using external IDs
442+
>>> kili.add_metadata(
443+
json_metadata=[
444+
{"key1": "value1", "key2": "value2"},
445+
{"key3": "value3"}
446+
],
447+
project_id="cm92to3cx012u7l0w6kij9qvx",
448+
external_ids=["asset1", "asset2"]
437449
)
438450
"""
439451
if is_empty_list_with_warning("add_metadata", "json_metadata", json_metadata):
440452
return []
441453

454+
if (asset_ids is not None and external_ids is not None) or (
455+
asset_ids is None and external_ids is None
456+
):
457+
raise MissingArgumentError("Please provide either `asset_ids` or `external_ids`.")
458+
442459
assets = self.kili_api_gateway.list_assets(
443460
AssetFilters(
444-
project_id=ProjectId(project_id), asset_id_in=cast(List[AssetId], asset_ids)
461+
project_id=ProjectId(project_id),
462+
asset_id_in=cast(List[AssetId], asset_ids),
463+
external_id_in=cast(List[AssetExternalId], external_ids),
445464
),
446465
["id", "jsonMetadata"],
447466
QueryOptions(disable_tqdm=True),
448467
)
449468

469+
resolved_asset_ids = []
450470
json_metadatas = []
451471
for i, asset in enumerate(assets):
452472
current_metadata = asset.get("jsonMetadata", {}) if asset.get("jsonMetadata") else {}
@@ -455,26 +475,29 @@ def add_metadata(
455475
current_metadata.update(new_metadata)
456476

457477
json_metadatas.append(current_metadata)
478+
resolved_asset_ids.append(asset["id"])
458479

459480
return self.update_properties_in_assets(
460-
asset_ids=asset_ids,
481+
asset_ids=cast(List[str], resolved_asset_ids),
461482
json_metadatas=json_metadatas,
462483
)
463484

464485
@typechecked
465486
def set_metadata(
466487
self,
467488
json_metadata: List[Dict[str, Union[str, int, float]]],
468-
asset_ids: List[str],
469489
project_id: str,
490+
asset_ids: Optional[List[str]] = None,
491+
external_ids: Optional[List[str]] = None,
470492
) -> List[Dict[Literal["id"], str]]:
471493
"""Set metadata on assets, replacing any existing metadata.
472494
473495
Args:
474496
json_metadata: List of metadata dictionaries to set on each asset.
475497
Each dictionary contains key/value pairs to be set as the asset's metadata.
476-
asset_ids: The asset IDs to modify.
477498
project_id: The project ID.
499+
asset_ids: The asset IDs to modify (if `external_ids` is not already provided).
500+
external_ids: The external asset IDs to modify (if `asset_ids` is not already provided).
478501
479502
Returns:
480503
A list of dictionaries with the asset ids.
@@ -485,21 +508,39 @@ def set_metadata(
485508
{"key1": "value1", "key2": "value2"},
486509
{"key3": "value3"}
487510
],
488-
asset_ids=["ckg22d81r0jrg0885unmuswj8", "ckg22d81s0jrh0885pdxfd03n"],
489511
project_id="cm92to3cx012u7l0w6kij9qvx"
512+
asset_ids=["ckg22d81r0jrg0885unmuswj8", "ckg22d81s0jrh0885pdxfd03n"]
513+
)
514+
515+
# Or using external IDs
516+
>>> kili.set_metadata(
517+
json_metadata=[
518+
{"key1": "value1", "key2": "value2"},
519+
{"key3": "value3"}
520+
],
521+
project_id="cm92to3cx012u7l0w6kij9qvx",
522+
external_ids=["asset1", "asset2"]
490523
)
491524
"""
492525
if is_empty_list_with_warning("set_metadata", "json_metadata", json_metadata):
493526
return []
494527

528+
if (asset_ids is not None and external_ids is not None) or (
529+
asset_ids is None and external_ids is None
530+
):
531+
raise MissingArgumentError("Please provide either `asset_ids` or `external_ids`.")
532+
495533
assets = self.kili_api_gateway.list_assets(
496534
AssetFilters(
497-
project_id=ProjectId(project_id), asset_id_in=cast(List[AssetId], asset_ids)
535+
project_id=ProjectId(project_id),
536+
asset_id_in=cast(List[AssetId], asset_ids),
537+
external_id_in=cast(List[AssetExternalId], external_ids),
498538
),
499539
["id", "jsonMetadata"],
500540
QueryOptions(disable_tqdm=True),
501541
)
502542

543+
resolved_asset_ids = []
503544
json_metadatas = []
504545
for i, asset in enumerate(assets):
505546
current_metadata = asset.get("jsonMetadata", {}) if asset.get("jsonMetadata") else {}
@@ -513,9 +554,10 @@ def set_metadata(
513554
preserved_metadata.update(new_metadata)
514555

515556
json_metadatas.append(preserved_metadata)
557+
resolved_asset_ids.append(asset["id"])
516558

517559
return self.update_properties_in_assets(
518-
asset_ids=asset_ids,
560+
asset_ids=cast(List[str], resolved_asset_ids),
519561
json_metadatas=json_metadatas,
520562
)
521563

tests/integration/presentation/test_metadata.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from kili.adapters.kili_api_gateway.helpers.queries import QueryOptions
88
from kili.adapters.kili_api_gateway.kili_api_gateway import KiliAPIGateway
9-
from kili.domain.asset import AssetFilters, AssetId
9+
from kili.domain.asset import AssetExternalId, AssetFilters, AssetId
1010
from kili.domain.project import ProjectId
1111
from kili.entrypoints.mutations.asset import MutationsAsset
1212

@@ -229,3 +229,123 @@ def test_multiple_assets_with_different_metadata_structures(mocker: pytest_mock.
229229

230230
assert add_metadata_call_args["json_metadatas"] == expected_add_metadata
231231
assert set_metadata_call_args["json_metadatas"] == expected_set_metadata
232+
233+
234+
def test_add_metadata_with_external_ids(mocker: pytest_mock.MockerFixture):
235+
"""Test that add_metadata works correctly with external IDs."""
236+
kili_api_gateway = mocker.Mock(spec=KiliAPIGateway)
237+
238+
existing_assets = [
239+
{
240+
"id": "asset1",
241+
"jsonMetadata": {"text": "Some text", "existing1": "value1"},
242+
},
243+
{
244+
"id": "asset2",
245+
"jsonMetadata": {"imageUrl": "http://example.com/image.jpg", "existing2": "value2"},
246+
},
247+
]
248+
249+
kili_api_gateway.list_assets.return_value = existing_assets
250+
251+
update_mock = mocker.patch.object(MutationsAsset, "update_properties_in_assets")
252+
update_mock.return_value = [{"id": "asset1"}, {"id": "asset2"}]
253+
254+
mutations_asset = MutationsAsset()
255+
mutations_asset.kili_api_gateway = kili_api_gateway
256+
mutations_asset.graphql_client = mocker.Mock()
257+
258+
project_id = "project1"
259+
external_ids = ["ext1", "ext2"]
260+
new_metadata: List[Dict[str, Union[str, float, int]]] = [
261+
{"new_key1": "new_value1"},
262+
{"new_key2": "new_value2"},
263+
]
264+
265+
result = mutations_asset.add_metadata(
266+
json_metadata=new_metadata, external_ids=external_ids, project_id=project_id
267+
)
268+
269+
kili_api_gateway.list_assets.assert_called_once_with(
270+
AssetFilters(
271+
project_id=ProjectId(project_id),
272+
external_id_in=cast(List[AssetExternalId], external_ids),
273+
),
274+
["id", "jsonMetadata"],
275+
QueryOptions(disable_tqdm=True),
276+
)
277+
278+
update_mock.assert_called_once()
279+
call_args = update_mock.call_args[1]
280+
281+
expected_metadata = [
282+
{"text": "Some text", "existing1": "value1", "new_key1": "new_value1"},
283+
{
284+
"imageUrl": "http://example.com/image.jpg",
285+
"existing2": "value2",
286+
"new_key2": "new_value2",
287+
},
288+
]
289+
290+
assert call_args["json_metadatas"] == expected_metadata
291+
assert result == [{"id": "asset1"}, {"id": "asset2"}]
292+
293+
294+
def test_set_metadata_with_external_ids(mocker: pytest_mock.MockerFixture):
295+
"""Test that set_metadata works correctly with external IDs."""
296+
kili_api_gateway = mocker.Mock(spec=KiliAPIGateway)
297+
298+
existing_assets = [
299+
{
300+
"id": "asset1",
301+
"jsonMetadata": {"text": "Some text", "existing1": "value1", "should_be_removed": True},
302+
},
303+
{
304+
"id": "asset2",
305+
"jsonMetadata": {
306+
"imageUrl": "http://example.com/image.jpg",
307+
"existing2": "value2",
308+
"also_remove": "yes",
309+
},
310+
},
311+
]
312+
313+
kili_api_gateway.list_assets.return_value = existing_assets
314+
315+
update_mock = mocker.patch.object(MutationsAsset, "update_properties_in_assets")
316+
update_mock.return_value = [{"id": "asset1"}, {"id": "asset2"}]
317+
318+
mutations_asset = MutationsAsset()
319+
mutations_asset.kili_api_gateway = kili_api_gateway
320+
mutations_asset.graphql_client = mocker.Mock()
321+
322+
project_id = "project1"
323+
external_ids = ["ext1", "ext2"]
324+
new_metadata: List[Dict[str, Union[str, float, int]]] = [
325+
{"new_key1": "new_value1"},
326+
{"new_key2": "new_value2"},
327+
]
328+
329+
result = mutations_asset.set_metadata(
330+
json_metadata=new_metadata, external_ids=external_ids, project_id=project_id
331+
)
332+
333+
kili_api_gateway.list_assets.assert_called_once_with(
334+
AssetFilters(
335+
project_id=ProjectId(project_id),
336+
external_id_in=cast(List[AssetExternalId], external_ids),
337+
),
338+
["id", "jsonMetadata"],
339+
QueryOptions(disable_tqdm=True),
340+
)
341+
342+
update_mock.assert_called_once()
343+
call_args = update_mock.call_args[1]
344+
345+
expected_metadata = [
346+
{"text": "Some text", "new_key1": "new_value1"},
347+
{"imageUrl": "http://example.com/image.jpg", "new_key2": "new_value2"},
348+
]
349+
350+
assert call_args["json_metadatas"] == expected_metadata
351+
assert result == [{"id": "asset1"}, {"id": "asset2"}]

0 commit comments

Comments
 (0)