@@ -351,3 +351,123 @@ def _delete_entity(self, collection: str, entity_name: str) -> None:
351351 raise DestinationOperationError (
352352 f"failed to delete entity '{ entity_name } ': { e } "
353353 )
354+
355+ # ---------- Label operations ----------
356+
357+ def _get_labels (self , collection : str , name : str ) -> List [Dict [str , Any ]]:
358+ """Return labels for an entity in a collection.
359+
360+ Args:
361+ collection: Collection name ("instance" or "subaccount").
362+ name: Entity name.
363+
364+ Returns:
365+ List of raw label dicts. Returns empty list if entity has no labels.
366+
367+ Raises:
368+ HttpError: If entity is not found (404).
369+ DestinationOperationError: On file read errors.
370+ """
371+ try :
372+ data = self ._read ()
373+ entry = self ._find_by_name (data .get (collection , []), name )
374+ if entry is None :
375+ raise HttpError (
376+ f"entity '{ name } ' not found" ,
377+ status_code = 404 ,
378+ response_text = "Not Found" ,
379+ )
380+ return list (entry .get ("labels" , []))
381+ except HttpError :
382+ raise
383+ except Exception as e :
384+ raise DestinationOperationError (f"failed to get labels for '{ name } ': { e } " )
385+
386+ def _set_labels (
387+ self , collection : str , name : str , labels : List [Dict [str , Any ]]
388+ ) -> None :
389+ """Replace all labels for an entity in a collection (PUT semantics).
390+
391+ Args:
392+ collection: Collection name ("instance" or "subaccount").
393+ name: Entity name.
394+ labels: List of raw label dicts to store.
395+
396+ Raises:
397+ HttpError: If entity is not found (404).
398+ DestinationOperationError: On file read/write errors.
399+ """
400+ try :
401+ with self ._lock :
402+ data = self ._read ()
403+ lst = data .setdefault (collection , [])
404+ idx = self ._index_by_name (lst , name )
405+ if idx < 0 :
406+ raise HttpError (
407+ f"entity '{ name } ' not found" ,
408+ status_code = 404 ,
409+ response_text = "Not Found" ,
410+ )
411+ lst [idx ]["labels" ] = labels
412+ self ._write (data )
413+ except HttpError :
414+ raise
415+ except Exception as e :
416+ raise DestinationOperationError (f"failed to set labels for '{ name } ': { e } " )
417+
418+ def _patch_labels_in_store (
419+ self ,
420+ collection : str ,
421+ name : str ,
422+ action : str ,
423+ patch_labels : List [Dict [str , Any ]],
424+ ) -> None :
425+ """Add or remove labels for an entity in a collection (PATCH semantics).
426+
427+ ADD: upsert by key — if the key exists update its values, otherwise append.
428+ DELETE: remove entries whose key matches any incoming label key.
429+
430+ Args:
431+ collection: Collection name ("instance" or "subaccount").
432+ name: Entity name.
433+ action: "ADD" or "DELETE".
434+ patch_labels: List of raw label dicts to apply.
435+
436+ Raises:
437+ HttpError: If entity is not found (404).
438+ DestinationOperationError: On unknown action or file read/write errors.
439+ """
440+ try :
441+ with self ._lock :
442+ data = self ._read ()
443+ lst = data .setdefault (collection , [])
444+ idx = self ._index_by_name (lst , name )
445+ if idx < 0 :
446+ raise HttpError (
447+ f"entity '{ name } ' not found" ,
448+ status_code = 404 ,
449+ response_text = "Not Found" ,
450+ )
451+ current : List [Dict [str , Any ]] = list (lst [idx ].get ("labels" , []))
452+
453+ if action == "ADD" :
454+ key_map = {lbl ["key" ]: lbl for lbl in current }
455+ for incoming in patch_labels :
456+ key_map [incoming ["key" ]] = incoming
457+ current = list (key_map .values ())
458+ elif action == "DELETE" :
459+ keys_to_remove = {lbl ["key" ] for lbl in patch_labels }
460+ current = [
461+ lbl for lbl in current if lbl ["key" ] not in keys_to_remove
462+ ]
463+ else :
464+ raise DestinationOperationError (
465+ f"unknown patch action: '{ action } ' — must be 'ADD' or 'DELETE'"
466+ )
467+
468+ lst [idx ]["labels" ] = current
469+ self ._write (data )
470+ except (HttpError , DestinationOperationError ):
471+ raise
472+ except Exception as e :
473+ raise DestinationOperationError (f"failed to patch labels for '{ name } ': { e } " )
0 commit comments