Source code for flexicon.code.Notebook.AnthropologyOperations

#
#   AnthropologyOperations.py
#
#   Class: AnthropologyOperations
#          Anthropological/cultural item operations for FieldWorks Language
#          Explorer projects via SIL Language and Culture Model (LCM) API.
#
#   Platform: Python.NET
#             FieldWorks Version 9+
#
#   Copyright 2025
#
#   KNOWN ISSUE: The property AnthroCode referenced in this class does NOT
#   exist on CmAnthroItem in LibLCM. CmAnthroItem has NO unique properties -
#   it only inherits from CmPossibility (Name, Abbreviation, Description, etc.).
#
#   OCM (Outline of Cultural Materials) codes may need to be stored in the
#   Abbreviation field instead. The AnthroCode methods use hasattr() checks
#   to prevent runtime errors, but they will silently do nothing if the
#   property doesn't exist.
#

# Import FLEx LCM types
from SIL.LCModel import (
    ICmAnthroItem,
    ICmAnthroItemFactory,
    IText,
    ICmPerson,
    ICmPossibility,
)
from SIL.LCModel.Core.KernelInterfaces import ITsString
from SIL.LCModel.Core.Text import TsStringUtils
from System import Guid, DateTime

# Import flexlibs exceptions
from ..FLExProject import (
    FP_ParameterError,
)
from ..BaseOperations import BaseOperations, OperationsMethod, wrap_enumerable
from ..Shared.string_utils import normalize_match_key
from ..Shared.catalog_backed import _LCMNativeCatalogImportMixin


[docs] class AnthropologyOperations(BaseOperations, _LCMNativeCatalogImportMixin): """ This class provides operations for managing anthropological and cultural items in a FieldWorks project. Anthropological items (ICmAnthroItem) are used to categorize and organize cultural information collected during fieldwork. They support hierarchical organization, OCM (Outline of Cultural Materials) codes, and linking to texts and researchers. These items help linguists document cultural context alongside linguistic data. This class should be accessed via FLExProject.Anthropology property. Usage:: from flexlibs2 import FLExProject project = FLExProject() project.OpenProject("my project", writeEnabled=True) # Create a new anthropology item item = project.Anthropology.Create("Marriage Customs", "MAR") # Set OCM code project.Anthropology.SetAnthroCode(item, "586") # Set description project.Anthropology.SetDescription(item, "Traditional marriage practices and ceremonies") # Create hierarchical structure subitem = project.Anthropology.CreateSubitem(item, "Wedding Ceremony", "WED") # Link to texts text = project.Texts.Find("Wedding Story 1") if text: project.Anthropology.AddText(item, text) # Get all items linked to a specific text for anthro_item in project.Anthropology.GetItemsForText(text): name = project.Anthropology.GetName(anthro_item) code = project.Anthropology.GetAnthroCode(anthro_item) print(f"{name} ({code})") project.CloseProject() """ # ---- _LCMNativeCatalogImportMixin configuration ------------------- # OCM ships as LCM-native XML in <FWCodeDir>/Templates/OCM.xml; # ImportCatalog delegates to XmlList.ImportList via the mixin's # _import_lcm_native_catalog helper. See catalog_backed.py. CATALOG_FILE = "OCM.xml" FRAME_CATALOG_FILE = "OCM-Frame.xml" CATALOG_SUBDIR = "Templates" LCM_FIELD_NAME = "AnthroList" LANG_PROJECT_LIST_ATTR = "AnthroListOA" DOMAIN_ITEM_LABEL_SINGULAR = "anthropology item" DOMAIN_ITEM_LABEL_PLURAL = "items" def __init__(self, project): """ Initialize AnthropologyOperations with a FLExProject instance. Args: project: The FLExProject instance to operate on. """ super().__init__(project) def __WSHandle(self, wsHandle): """ Internal helper for writing system handles. Args: wsHandle: Writing system handle or None for default analysis WS. Returns: int: The writing system handle to use. """ if wsHandle is None: return self.project.project.DefaultAnalWs return self.project._FLExProject__WSHandle(wsHandle, self.project.project.DefaultAnalWs) def __ValidatedItemHvo(self, item_or_hvo): """ Internal function to validate and convert item_or_hvo to HVO. Args: item_or_hvo: Either an ICmAnthroItem object or its HVO (integer). Returns: int: The HVO of the anthropology item. Raises: FP_NullParameterError: If item_or_hvo is None. """ self._ValidateParam(item_or_hvo, "item_or_hvo") try: hvo = item_or_hvo.Hvo except AttributeError: hvo = item_or_hvo return hvo def __GetItemObject(self, item_or_hvo): """ Internal function to get ICmAnthroItem object from item_or_hvo. Args: item_or_hvo: Either an ICmAnthroItem object or its HVO (integer). Returns: ICmAnthroItem: The anthropology item object. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the object doesn't exist or isn't valid. """ hvo = self.__ValidatedItemHvo(item_or_hvo) try: obj = self.project.Object(hvo) return ICmAnthroItem(obj) except Exception: raise FP_ParameterError(f"Invalid anthropology item object or HVO: {item_or_hvo}") # --- Core CRUD Operations --- @wrap_enumerable @OperationsMethod def GetAll(self, recursive=True, **kwargs): """ Get all anthropology items in the project. Args: recursive (bool): If True (default), returns every item in the hierarchy (depth-first, parents before children). If False, returns only top-level items and the caller must descend via GetSubitems. Returns: list: List of ICmAnthroItem objects. Example: >>> for item in project.Anthropology.GetAll(): ... print(project.Anthropology.GetName(item)) # Includes subitems >>> # Top-level only >>> for item in project.Anthropology.GetAll(recursive=False): ... print(project.Anthropology.GetName(item)) See Also: Find, FindByCode, GetSubitems """ self._RejectLegacyKwargs(kwargs, { "flat": ("recursive", "semantics inverted: flat=True is now recursive=True"), }) anthro_list = self.project.lp.AnthroListOA if anthro_list is None: return [] return list(self.project.UnpackNestedPossibilityList( anthro_list.PossibilitiesOS, ICmAnthroItem, recursive)) @OperationsMethod def Create(self, name, abbreviation=None, anthro_code=None): """ Create a new anthropology item. Creates a new top-level ICmAnthroItem in the project's anthropology list. Use CreateSubitem() to create hierarchical items. Args: name (str): The name of the item (e.g., "Marriage Customs"). abbreviation (str, optional): Short abbreviation (e.g., "MAR"). If None, no abbreviation is set. Defaults to None. anthro_code (str, optional): OCM (Outline of Cultural Materials) code (e.g., "586" for marriage). If None, no code is set. Defaults to None. Returns: ICmAnthroItem: The newly created anthropology item object. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If name is None or empty. FP_ParameterError: If an item with this name already exists. Example: >>> # Create a simple item >>> item = project.Anthropology.Create("Marriage Customs") >>> print(project.Anthropology.GetName(item)) Marriage Customs >>> # Create with abbreviation and OCM code >>> item = project.Anthropology.Create( ... "Marriage Customs", ... abbreviation="MAR", ... anthro_code="586" ... ) >>> print(project.Anthropology.GetAnthroCode(item)) 586 >>> # Create multiple items >>> kinship = project.Anthropology.Create("Kinship", "KIN", "600") >>> religion = project.Anthropology.Create("Religion", "REL", "770") Notes: - Name must be unique within the project - Abbreviations don't need to be unique but should be distinct - OCM codes follow the Outline of Cultural Materials standard - The item is created in the default analysis writing system - Use CreateSubitem() for hierarchical items See Also: CreateSubitem, Delete, Exists, Find """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") name = name.strip() self._ValidateParam(name, "name") # Check if item already exists if self.Exists(name): raise FP_ParameterError(f"Anthropology item '{name}' already exists") # Ensure anthropology list exists. List creation/assignment is project-state # setup; resolve it before opening the per-item transaction so a missing # list never leaves an orphaned ICmAnthroItem. if self.project.lp.AnthroListOA is None: from SIL.LCModel import ICmPossibilityListFactory list_factory = self.project.project.ServiceLocator.GetService(ICmPossibilityListFactory) anthro_list = list_factory.Create() self.project.lp.AnthroListOA = anthro_list else: anthro_list = self.project.lp.AnthroListOA with self._TransactionCM('Create Anthropology Item'): # Get the writing system handle wsHandle = self.project.project.DefaultAnalWs # Create the new item using the factory factory = self.project.project.ServiceLocator.GetService(ICmAnthroItemFactory) new_item = factory.Create() # Add to the anthropology list (must be done before setting properties) anthro_list.PossibilitiesOS.Add(new_item) # Set name mkstr_name = TsStringUtils.MakeString(name, wsHandle) new_item.Name.set_String(wsHandle, mkstr_name) # Set abbreviation if provided if abbreviation: mkstr_abbr = TsStringUtils.MakeString(abbreviation, wsHandle) new_item.Abbreviation.set_String(wsHandle, mkstr_abbr) # Set OCM code if provided (note: AnthroCode may not exist on CmAnthroItem) if anthro_code and hasattr(new_item, "AnthroCode"): new_item.AnthroCode = anthro_code return new_item @OperationsMethod def CreateSubitem(self, parent_item, name, abbreviation=None, anthro_code=None): """ Create a new anthropology item as a child of an existing item. Creates a hierarchical relationship where the new item is a subcategory or more specific aspect of the parent item. Args: parent_item: The parent ICmAnthroItem object or HVO. name (str): The name of the subitem (e.g., "Wedding Ceremony"). abbreviation (str, optional): Short abbreviation. Defaults to None. anthro_code (str, optional): OCM code for the subitem. Defaults to None. Returns: ICmAnthroItem: The newly created subitem object. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If parent_item or name is None/empty. FP_ParameterError: If parent_item is invalid. Example: >>> # Create parent item >>> marriage = project.Anthropology.Create("Marriage Customs", "MAR", "586") >>> >>> # Create subitems >>> wedding = project.Anthropology.CreateSubitem( ... marriage, "Wedding Ceremony", "WED", "586.1" ... ) >>> gifts = project.Anthropology.CreateSubitem( ... marriage, "Marriage Gifts", "GIFT", "586.2" ... ) >>> >>> # Verify hierarchy >>> parent = project.Anthropology.GetParent(wedding) >>> print(project.Anthropology.GetName(parent)) Marriage Customs >>> >>> # Get all subitems >>> for sub in project.Anthropology.GetSubitems(marriage): ... print(project.Anthropology.GetName(sub)) Wedding Ceremony Marriage Gifts Notes: - Creates hierarchical organization of cultural data - Subitems inherit context from parent - OCM codes typically use decimal notation (e.g., 586.1, 586.2) - Multiple levels of nesting are supported - Parent item must exist before creating subitems See Also: Create, GetSubitems, GetParent, Delete """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") name = name.strip() self._ValidateParam(name, "name") parent = self.__GetItemObject(parent_item) # Get the writing system handle with self._TransactionCM('Create Anthropology Subitem'): wsHandle = self.project.project.DefaultAnalWs # Create the new item using the factory factory = self.project.project.ServiceLocator.GetService(ICmAnthroItemFactory) new_item = factory.Create() # Add to parent's subitems (must be done before setting properties) parent.SubPossibilitiesOS.Add(new_item) # Set name mkstr_name = TsStringUtils.MakeString(name, wsHandle) new_item.Name.set_String(wsHandle, mkstr_name) # Set abbreviation if provided if abbreviation: mkstr_abbr = TsStringUtils.MakeString(abbreviation, wsHandle) new_item.Abbreviation.set_String(wsHandle, mkstr_abbr) # Set OCM code if provided (note: AnthroCode may not exist on CmAnthroItem) if anthro_code and hasattr(new_item, "AnthroCode"): new_item.AnthroCode = anthro_code return new_item @OperationsMethod def Delete(self, item_or_hvo): """ Delete an anthropology item. Removes the item from the project. If the item has subitems, they will also be deleted. Removes all links to texts and researchers. Args: item_or_hvo: The ICmAnthroItem object or HVO to delete. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> # Delete a specific item >>> item = project.Anthropology.Find("Obsolete Item") >>> if item: ... project.Anthropology.Delete(item) >>> # Delete by HVO >>> project.Anthropology.Delete(item_hvo) Warning: - Deleting an item also deletes all its subitems - This operation cannot be undone (unless using UndoActionSequence) - Text and researcher links are removed, but the texts and researchers themselves are not deleted See Also: Create, CreateSubitem, Exists """ self._EnsureWriteEnabled() item = self.__GetItemObject(item_or_hvo) # Get the anthropology list anthro_list = self.project.lp.AnthroListOA if anthro_list is not None: # Check if it's a top-level item if anthro_list.PossibilitiesOS.Contains(item): anthro_list.PossibilitiesOS.Remove(item) else: # It's a subitem, remove from parent. item.Owner is # typed as ICmObject; cast to the concrete possibility # so SubPossibilitiesOS is reachable. The previous # implementation wrapped this in a broad `except: pass` # that swallowed real failures (including the no-op # case where the inner cast actually succeeded but # nothing happened because hasattr lied on a base # interface) -- let exceptions propagate so callers # see the failure. parent = self._GetTypedOwner(item) if parent is None: raise FP_ParameterError( "Anthropology subitem has no resolvable owner" ) if hasattr(parent, "SubPossibilitiesOS"): parent.SubPossibilitiesOS.Remove(item) @OperationsMethod def Exists(self, name): """ Check if an anthropology item with the given name exists. Performs a case-sensitive comparison of item names in the default analysis writing system. Args: name (str): The name of the item to check. Returns: bool: True if an item with the given name exists, False otherwise. Raises: FP_NullParameterError: If name is None or empty. Example: >>> if project.Anthropology.Exists("Marriage Customs"): ... print("Item already exists") ... else: ... item = project.Anthropology.Create("Marriage Customs") >>> # Case-sensitive check >>> print(project.Anthropology.Exists("marriage customs")) False Notes: - Search is case-sensitive - Searches through all items (including subitems) - Returns False for empty or whitespace-only names - More efficient than Find() when you only need existence check See Also: Find, FindByCode, Create """ self._ValidateParam(name, "name") name = name.strip() self._ValidateParam(name, "name") return self.Find(name) is not None @OperationsMethod def Find(self, name): """ Find an anthropology item by its name. Searches through all anthropology items (including subitems) and returns the first item with a matching name. Args: name (str): The name to search for. Returns: ICmAnthroItem or None: The item object if found, None otherwise. Raises: FP_NullParameterError: If name is None. Example: >>> # Find by name >>> item = project.Anthropology.Find("Marriage Customs") >>> if item: ... code = project.Anthropology.GetAnthroCode(item) ... print(f"Found item with code: {code}") Found item with code: 586 >>> # Handle not found >>> item = project.Anthropology.Find("Nonexistent Item") >>> if not item: ... print("Item not found") Item not found Notes: - Search is case-sensitive - Searches in default analysis writing system - Returns first match only - Returns None if not found (doesn't raise exception) - For OCM code search, use FindByCode() See Also: FindByCode, Exists, GetAll """ self._ValidateParam(name, "name") if not name or not name.strip(): return None name = name.strip() wsHandle = self.project.project.DefaultAnalWs # Search through all items target = normalize_match_key(name, casefold=False) for item in self.GetAll(): item_name = ITsString(item.Name.get_String(wsHandle)).Text if normalize_match_key(item_name, casefold=False) == target: return item return None @OperationsMethod def FindByCode(self, anthro_code): """ Find an anthropology item by its OCM code. Searches through all anthropology items and returns the first item with a matching AnthroCode (Outline of Cultural Materials code). Args: anthro_code (str): The OCM code to search for (e.g., "586"). Returns: ICmAnthroItem or None: The item object if found, None otherwise. Raises: FP_NullParameterError: If anthro_code is None. Example: >>> # Find by OCM code >>> item = project.Anthropology.FindByCode("586") >>> if item: ... name = project.Anthropology.GetName(item) ... print(f"Code 586 is: {name}") Code 586 is: Marriage Customs >>> # Find hierarchical codes >>> item = project.Anthropology.FindByCode("586.1") >>> if item: ... name = project.Anthropology.GetName(item) ... parent = project.Anthropology.GetParent(item) ... parent_name = project.Anthropology.GetName(parent) ... print(f"{name} is subcategory of {parent_name}") Wedding Ceremony is subcategory of Marriage Customs Notes: - Search is case-sensitive - Returns first match only - Returns None if not found - OCM codes are standard anthropological classification system - Common OCM code ranges: - 100-199: Orientation - 200-299: Food and Clothing - 300-399: Housing - 400-499: Economy - 500-599: Kinship and Family - 600-699: Political Behavior - 700-799: Religion - 800-899: Social Structure See Also: Find, GetAnthroCode, SetAnthroCode """ self._ValidateParam(anthro_code, "anthro_code") if not anthro_code or not anthro_code.strip(): return None anthro_code = anthro_code.strip() # Search through all items for item in self.GetAll(): if hasattr(item, "AnthroCode") and item.AnthroCode: if item.AnthroCode == anthro_code: return item return None @OperationsMethod def FindByCategory(self, category): """ Find all anthropology items in a specific category. Searches for items that have been assigned to a particular category possibility. Args: category: ICmPossibility object representing the category. Returns: list: List of ICmAnthroItem objects in that category. Raises: FP_NullParameterError: If category is None. Example: >>> # Get a category from the categories list >>> categories = project.lp.AnthroListOA >>> if categories and categories.PossibilitiesOS.Count > 0: ... category = categories.PossibilitiesOS[0] ... items = project.Anthropology.FindByCategory(category) ... for item in items: ... name = project.Anthropology.GetName(item) ... print(f"Item: {name}") Notes: - Returns empty list if no items found - Category must be a valid ICmPossibility - Items can belong to multiple categories - This searches the item's category reference See Also: Find, FindByCode, GetCategory, SetCategory """ self._ValidateParam(category, "category") results = [] # Search through all items for item in self.GetAll(): # Check if item has this category if hasattr(item, "CategoryRA") and item.CategoryRA: if item.CategoryRA == category: results.append(item) return results # --- Property Access Methods --- @OperationsMethod def GetName(self, item_or_hvo, wsHandle=None): """ Get the name of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The item name, or empty string if not set. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> name = project.Anthropology.GetName(item) >>> print(name) Marriage Customs >>> # Get name in a specific writing system >>> name_fr = project.Anthropology.GetName(item, ... project.WSHandle('fr')) >>> print(name_fr) Coutumes matrimoniales Notes: - Returns empty string if name not set in specified writing system - Names are typically set in multiple writing systems - Default writing system is the default analysis WS See Also: SetName, GetAbbreviation, GetDescription """ item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) name = ITsString(item.Name.get_String(wsHandle)).Text return name or "" @OperationsMethod def SetName(self, item_or_hvo, name, wsHandle=None): """ Set the name of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. name (str): The new name. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or name is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage") >>> project.Anthropology.SetName(item, "Marriage Customs") >>> print(project.Anthropology.GetName(item)) Marriage Customs >>> # Set in multiple writing systems >>> project.Anthropology.SetName(item, "Marriage Customs", "en") >>> project.Anthropology.SetName(item, "Coutumes matrimoniales", ... project.WSHandle('fr')) Notes: - Name is stored in the specified writing system - Can set different names for different writing systems - Empty string clears the name in that writing system See Also: GetName, SetAbbreviation, SetDescription """ self._EnsureWriteEnabled() self._ValidateParam(name, "name") item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(name, wsHandle) item.Name.set_String(wsHandle, mkstr) @OperationsMethod def GetAbbreviation(self, item_or_hvo, wsHandle=None): """ Get the abbreviation of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The abbreviation, or empty string if not set. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> abbr = project.Anthropology.GetAbbreviation(item) >>> print(abbr) MAR >>> # Use in display >>> for item in project.Anthropology.GetAll(): ... name = project.Anthropology.GetName(item) ... abbr = project.Anthropology.GetAbbreviation(item) ... if abbr: ... print(f"{abbr}: {name}") ... else: ... print(name) MAR: Marriage Customs WED: Wedding Ceremony KIN: Kinship Notes: - Returns empty string if abbreviation not set - Abbreviations are shorter identifiers for display - Typically 2-5 characters See Also: SetAbbreviation, GetName, GetAnthroCode """ item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) abbr = ITsString(item.Abbreviation.get_String(wsHandle)).Text return abbr or "" @OperationsMethod def SetAbbreviation(self, item_or_hvo, abbreviation, wsHandle=None): """ Set the abbreviation of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. abbreviation (str): The new abbreviation. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or abbreviation is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> project.Anthropology.SetAbbreviation(item, "MAR") >>> print(project.Anthropology.GetAbbreviation(item)) MAR >>> # Clear abbreviation >>> project.Anthropology.SetAbbreviation(item, "") Notes: - Abbreviation is stored in the specified writing system - Can set different abbreviations for different writing systems - Empty string clears the abbreviation See Also: GetAbbreviation, SetName, SetAnthroCode """ self._EnsureWriteEnabled() self._ValidateParam(abbreviation, "abbreviation") item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(abbreviation, wsHandle) item.Abbreviation.set_String(wsHandle, mkstr) @OperationsMethod def GetDescription(self, item_or_hvo, wsHandle=None): """ Get the description of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. wsHandle: Optional writing system handle. Defaults to analysis WS. Returns: str: The description, or empty string if not set. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> desc = project.Anthropology.GetDescription(item) >>> print(desc) Traditional marriage practices, including courtship, betrothal, wedding ceremonies, and gift exchanges. >>> # Display full item information >>> name = project.Anthropology.GetName(item) >>> code = project.Anthropology.GetAnthroCode(item) >>> desc = project.Anthropology.GetDescription(item) >>> print(f"{name} ({code})") >>> print(f"Description: {desc}") Notes: - Returns empty string if description not set - Descriptions can be multi-paragraph text - Use descriptions to document cultural context See Also: SetDescription, GetName, GetAnthroCode """ item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) desc = ITsString(item.Description.get_String(wsHandle)).Text return desc or "" @OperationsMethod def SetDescription(self, item_or_hvo, description, wsHandle=None): """ Set the description of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. description (str): The new description. wsHandle: Optional writing system handle. Defaults to analysis WS. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or description is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> project.Anthropology.SetDescription(item, ... "Traditional marriage practices, including courtship, " ... "betrothal, wedding ceremonies, and gift exchanges." ... ) >>> # Set in multiple languages >>> project.Anthropology.SetDescription(item, ... "Traditional marriage practices...", "en") >>> project.Anthropology.SetDescription(item, ... "Pratiques matrimoniales traditionnelles...", ... project.WSHandle('fr')) >>> # Clear description >>> project.Anthropology.SetDescription(item, "") Notes: - Description is stored in the specified writing system - Can set different descriptions for different writing systems - Empty string clears the description - Descriptions support multi-paragraph text See Also: GetDescription, SetName, SetAnthroCode """ self._EnsureWriteEnabled() self._ValidateParam(description, "description") item = self.__GetItemObject(item_or_hvo) wsHandle = self.__WSHandle(wsHandle) mkstr = TsStringUtils.MakeString(description, wsHandle) item.Description.set_String(wsHandle, mkstr) @OperationsMethod def GetAnthroCode(self, item_or_hvo): """ Get the OCM (Outline of Cultural Materials) code of an item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: str: The OCM code, or empty string if not set. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> code = project.Anthropology.GetAnthroCode(item) >>> print(code) 586 >>> # Display all items with their codes >>> for item in project.Anthropology.GetAll(): ... name = project.Anthropology.GetName(item) ... code = project.Anthropology.GetAnthroCode(item) ... if code: ... print(f"{code}: {name}") ... else: ... print(f"No code: {name}") 586: Marriage Customs 586.1: Wedding Ceremony 587: Divorce No code: Custom Category Notes: - OCM (Outline of Cultural Materials) is a standard classification - Returns empty string if no code is set - Codes typically use decimal notation for hierarchy - Common OCM code ranges documented in FindByCode() See Also: SetAnthroCode, FindByCode, GetName """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "AnthroCode") and item.AnthroCode: return item.AnthroCode return "" @OperationsMethod def SetAnthroCode(self, item_or_hvo, anthro_code): """ Set the OCM (Outline of Cultural Materials) code of an item. Args: item_or_hvo: The ICmAnthroItem object or HVO. anthro_code (str): The OCM code to set. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or anthro_code is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> project.Anthropology.SetAnthroCode(item, "586") >>> print(project.Anthropology.GetAnthroCode(item)) 586 >>> # Set hierarchical codes >>> subitem = project.Anthropology.Find("Wedding Ceremony") >>> project.Anthropology.SetAnthroCode(subitem, "586.1") >>> # Clear code >>> project.Anthropology.SetAnthroCode(item, "") Notes: - OCM codes follow standard anthropological classification - Empty string clears the code - Codes are stored as strings, not validated - Use decimal notation for hierarchical codes (e.g., 586.1) See Also: GetAnthroCode, FindByCode, SetName """ self._EnsureWriteEnabled() self._ValidateParam(anthro_code, "anthro_code") item = self.__GetItemObject(item_or_hvo) # Note: AnthroCode may not exist on CmAnthroItem if hasattr(item, "AnthroCode"): item.AnthroCode = anthro_code @OperationsMethod def GetCategory(self, item_or_hvo): """ Get the category of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: ICmPossibility or None: The category object if set, None otherwise. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> category = project.Anthropology.GetCategory(item) >>> if category: ... cat_name = category.Name.BestAnalysisAlternative.Text ... print(f"Category: {cat_name}") ... else: ... print("No category assigned") Notes: - Returns None if no category is assigned - Category is a reference to a possibility item - Categories help organize anthropology items See Also: SetCategory, FindByCategory """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "CategoryRA") and item.CategoryRA: return item.CategoryRA return None @OperationsMethod def SetCategory(self, item_or_hvo, category): """ Set the category of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. category: ICmPossibility object or None to clear. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item or category is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> >>> # Set category from a possibility list >>> # (assuming categories are defined in a list) >>> category = project.lp.AnthroListOA.PossibilitiesOS[0] >>> project.Anthropology.SetCategory(item, category) >>> >>> # Clear category >>> project.Anthropology.SetCategory(item, None) Notes: - Category must be a valid ICmPossibility object - Setting to None clears the category - Categories help organize and filter items See Also: GetCategory, FindByCategory """ self._EnsureWriteEnabled() item = self.__GetItemObject(item_or_hvo) if category is None: item.CategoryRA = None else: try: cat_poss = ICmPossibility(category) item.CategoryRA = cat_poss except Exception: raise FP_ParameterError("category must be a valid ICmPossibility object") # --- Hierarchy Operations --- @OperationsMethod def GetSubitems(self, item_or_hvo, recursive=True, **kwargs): """ Get the subitems of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. recursive (bool): If True (default), returns every descendant (depth-first, parents before children). If False, returns only direct children. Returns: list: List of ICmAnthroItem objects. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> marriage = project.Anthropology.Find("Marriage Customs") >>> for sub in project.Anthropology.GetSubitems(marriage): ... print(project.Anthropology.GetName(sub)) # All descendants >>> # Direct children only >>> for sub in project.Anthropology.GetSubitems(marriage, recursive=False): ... print(project.Anthropology.GetName(sub)) See Also: CreateSubitem, GetParent, GetAll """ self._RejectLegacyKwargs(kwargs, { "flat": ("recursive", "semantics inverted: flat=True is now recursive=True"), }) item = self.__GetItemObject(item_or_hvo) if not hasattr(item, "SubPossibilitiesOS"): return [] if not recursive: return list(item.SubPossibilitiesOS) result = [] def walk(collection): for child in collection: result.append(child) if hasattr(child, "SubPossibilitiesOS") and child.SubPossibilitiesOS.Count > 0: walk(child.SubPossibilitiesOS) walk(item.SubPossibilitiesOS) return result @OperationsMethod def GetParent(self, item_or_hvo): """ Get the parent item of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: ICmAnthroItem or None: The parent item if this is a subitem, None if this is a top-level item. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> # Get parent of a subitem >>> wedding = project.Anthropology.Find("Wedding Ceremony") >>> parent = project.Anthropology.GetParent(wedding) >>> if parent: ... parent_name = project.Anthropology.GetName(parent) ... print(f"Parent: {parent_name}") ... else: ... print("This is a top-level item") Parent: Marriage Customs >>> # Build breadcrumb trail >>> def get_path(item): ... path = [] ... current = item ... while current: ... name = project.Anthropology.GetName(current) ... path.insert(0, name) ... current = project.Anthropology.GetParent(current) ... return " > ".join(path) >>> print(get_path(wedding)) Marriage Customs > Wedding Ceremony Notes: - Returns None for top-level items - Parent is always an ICmAnthroItem or the AnthroList - Useful for navigation and breadcrumb displays See Also: GetSubitems, CreateSubitem, GetAll """ item = self.__GetItemObject(item_or_hvo) # .Owner is on every ICmObject; the truthiness check still # discriminates orphans / detached items. (issue #133 sweep) if item.Owner: try: # Check if owner is an ICmAnthroItem (not the list) parent = ICmAnthroItem(item.Owner) return parent except Exception: # Owner is the AnthroListOA, so this is a top-level item return None return None # --- Text Linking Operations --- @OperationsMethod def GetTexts(self, item_or_hvo): """ Get all texts linked to an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: list: List of IText objects linked to this item. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> texts = project.Anthropology.GetTexts(item) >>> print(f"Found {len(texts)} texts") Found 3 texts >>> >>> for text in texts: ... name = text.Name.BestAnalysisAlternative.Text ... print(f" - {name}") - Wedding Story 1 - Wedding Story 2 - Marriage Interview Notes: - Returns empty list if no texts are linked - Texts can be linked to multiple anthropology items - Use AddText() to create links - Use RemoveText() to remove links See Also: AddText, RemoveText, GetTextCount, GetItemsForText """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "TextsRC"): return list(item.TextsRC) return [] @OperationsMethod def AddText(self, item_or_hvo, text): """ Link a text to an anthropology item. Creates a bidirectional link between the item and the text. The same text can be linked to multiple anthropology items. Args: item_or_hvo: The ICmAnthroItem object or HVO. text: The IText object to link. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or text is None. FP_ParameterError: If the item or text is invalid, or if the text is already linked to this item. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> text = project.Texts.Find("Wedding Story 1") >>> >>> # Link text to item >>> project.Anthropology.AddText(item, text) >>> >>> # Verify link >>> texts = project.Anthropology.GetTexts(item) >>> print(f"Item now has {len(texts)} text(s)") Item now has 1 text(s) >>> # Link same text to multiple items >>> wedding = project.Anthropology.Find("Wedding Ceremony") >>> project.Anthropology.AddText(wedding, text) Notes: - Text can be linked to multiple items - Item can have multiple texts - Link is bidirectional (stored in both objects) - Raises error if text is already linked to this item - Use RemoveText() to remove the link See Also: RemoveText, GetTexts, GetItemsForText """ self._EnsureWriteEnabled() self._ValidateParam(text, "text") item = self.__GetItemObject(item_or_hvo) try: text_obj = IText(text) except Exception: raise FP_ParameterError("text must be a valid IText object") # Check if already linked if hasattr(item, "TextsRC") and item.TextsRC.Contains(text_obj): raise FP_ParameterError("Text is already linked to this item") # Add the text to the item's collection if hasattr(item, "TextsRC"): item.TextsRC.Add(text_obj) @OperationsMethod def RemoveText(self, item_or_hvo, text): """ Remove a text link from an anthropology item. Removes the bidirectional link between the item and the text. The text itself is not deleted, only the link. Args: item_or_hvo: The ICmAnthroItem object or HVO. text: The IText object to unlink. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or text is None. FP_ParameterError: If the item or text is invalid, or if the text is not linked to this item. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> text = project.Texts.Find("Wedding Story 1") >>> >>> # Remove text link >>> project.Anthropology.RemoveText(item, text) >>> >>> # Verify removal >>> texts = project.Anthropology.GetTexts(item) >>> print(f"Item now has {len(texts)} text(s)") Item now has 0 text(s) Notes: - Only removes the link, not the text itself - Text may still be linked to other items - Raises error if text is not linked to this item See Also: AddText, GetTexts, GetItemsForText """ self._EnsureWriteEnabled() self._ValidateParam(text, "text") item = self.__GetItemObject(item_or_hvo) try: text_obj = IText(text) except Exception: raise FP_ParameterError("text must be a valid IText object") # Check if linked if hasattr(item, "TextsRC") and not item.TextsRC.Contains(text_obj): raise FP_ParameterError("Text is not linked to this item") # Remove the text from the item's collection if hasattr(item, "TextsRC"): item.TextsRC.Remove(text_obj) @OperationsMethod def GetTextCount(self, item_or_hvo): """ Get the count of texts linked to an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: int: The number of texts linked to this item. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> count = project.Anthropology.GetTextCount(item) >>> print(f"Item has {count} linked text(s)") Item has 3 linked text(s) >>> # Find items with linked texts >>> for item in project.Anthropology.GetAll(): ... count = project.Anthropology.GetTextCount(item) ... if count > 0: ... name = project.Anthropology.GetName(item) ... print(f"{name}: {count} text(s)") Marriage Customs: 3 text(s) Kinship: 5 text(s) Notes: - Returns 0 if no texts are linked - More efficient than len(GetTexts()) for counting only See Also: GetTexts, AddText, RemoveText """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "TextsRC"): return item.TextsRC.Count return 0 @OperationsMethod def GetItemsForText(self, text): """ Get all anthropology items linked to a specific text. Searches through all anthropology items to find which ones are linked to the specified text. Args: text: The IText object to search for. Returns: list: List of ICmAnthroItem objects linked to this text. Raises: FP_NullParameterError: If text is None. Example: >>> text = project.Texts.Find("Wedding Story 1") >>> items = project.Anthropology.GetItemsForText(text) >>> print(f"Text is linked to {len(items)} item(s)") Text is linked to 2 item(s) >>> >>> for item in items: ... name = project.Anthropology.GetName(item) ... code = project.Anthropology.GetAnthroCode(item) ... print(f" {code}: {name}") 586: Marriage Customs 586.1: Wedding Ceremony >>> # Find texts not linked to any items >>> for text in project.Texts.GetAll(): ... items = project.Anthropology.GetItemsForText(text) ... if not items: ... name = text.Name.BestAnalysisAlternative.Text ... print(f"Unlinked text: {name}") Notes: - Returns empty list if text is not linked to any items - Text can be linked to multiple items - Useful for finding cultural context of a text See Also: GetTexts, AddText, RemoveText """ self._ValidateParam(text, "text") try: text_obj = IText(text) except Exception: raise FP_ParameterError("text must be a valid IText object") results = [] # Search through all items for item in self.GetAll(): if hasattr(item, "TextsRC") and item.TextsRC.Contains(text_obj): results.append(item) return results # --- Researcher/People Linking Operations --- @OperationsMethod def GetResearchers(self, item_or_hvo): """ Get all researchers (people) linked to an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: list: List of ICmPerson objects linked to this item. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> researchers = project.Anthropology.GetResearchers(item) >>> for person in researchers: ... name = person.Name.BestAnalysisAlternative.Text ... print(f"Researcher: {name}") Researcher: John Smith Researcher: Jane Doe Notes: - Returns empty list if no researchers are linked - Researchers are ICmPerson objects from the project - Use AddResearcher() to create links See Also: AddResearcher, RemoveResearcher """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "ResearchersRC"): return list(item.ResearchersRC) return [] @OperationsMethod def AddResearcher(self, item_or_hvo, person): """ Link a researcher (person) to an anthropology item. Creates a link between the item and a person object, typically used to track which researchers are responsible for documenting particular cultural items. Args: item_or_hvo: The ICmAnthroItem object or HVO. person: The ICmPerson object to link. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or person is None. FP_ParameterError: If the item or person is invalid, or if the person is already linked to this item. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> >>> # Get or create a person >>> # (assuming people are in project.lp.PeopleOA) >>> person = project.lp.PeopleOA.PossibilitiesOS[0] >>> >>> # Link person to item >>> project.Anthropology.AddResearcher(item, person) >>> >>> # Verify link >>> researchers = project.Anthropology.GetResearchers(item) >>> print(f"Item has {len(researchers)} researcher(s)") Item has 1 researcher(s) Notes: - Person must be a valid ICmPerson object - Same person can be linked to multiple items - Item can have multiple researchers - Raises error if person is already linked See Also: RemoveResearcher, GetResearchers """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") item = self.__GetItemObject(item_or_hvo) try: person_obj = ICmPerson(person) except Exception: raise FP_ParameterError("person must be a valid ICmPerson object") # Check if already linked if hasattr(item, "ResearchersRC") and item.ResearchersRC.Contains(person_obj): raise FP_ParameterError("Person is already linked to this item") # Add the person to the item's collection if hasattr(item, "ResearchersRC"): item.ResearchersRC.Add(person_obj) @OperationsMethod def RemoveResearcher(self, item_or_hvo, person): """ Remove a researcher link from an anthropology item. Removes the link between the item and the person. The person object itself is not deleted, only the link. Args: item_or_hvo: The ICmAnthroItem object or HVO. person: The ICmPerson object to unlink. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo or person is None. FP_ParameterError: If the item or person is invalid, or if the person is not linked to this item. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> person = project.lp.PeopleOA.PossibilitiesOS[0] >>> >>> # Remove person link >>> project.Anthropology.RemoveResearcher(item, person) >>> >>> # Verify removal >>> researchers = project.Anthropology.GetResearchers(item) >>> print(f"Item has {len(researchers)} researcher(s)") Item has 0 researcher(s) Notes: - Only removes the link, not the person object - Person may still be linked to other items - Raises error if person is not linked to this item See Also: AddResearcher, GetResearchers """ self._EnsureWriteEnabled() self._ValidateParam(person, "person") item = self.__GetItemObject(item_or_hvo) try: person_obj = ICmPerson(person) except Exception: raise FP_ParameterError("person must be a valid ICmPerson object") # Check if linked if hasattr(item, "ResearchersRC") and not item.ResearchersRC.Contains(person_obj): raise FP_ParameterError("Person is not linked to this item") # Remove the person from the item's collection if hasattr(item, "ResearchersRC"): item.ResearchersRC.Remove(person_obj) @OperationsMethod def Duplicate(self, item_or_hvo, insert_after=True, deep=True): """ Duplicate an anthropology item, creating a new copy with a new GUID. Args: item_or_hvo: The ICmAnthroItem object or HVO to duplicate. insert_after (bool): If True (default), insert after the source item. If False, insert at end of parent's subitems list. deep (bool): If True (default), also duplicate owned objects (subitems). If False, only copy simple properties and references. Returns: ICmAnthroItem: The newly created duplicate item with a new GUID. Raises: FP_ReadOnlyError: If the project is not opened with write enabled. FP_NullParameterError: If item_or_hvo is None. Example: >>> # Shallow duplicate (no subitems) >>> item = project.Anthropology.Find("Marriage Customs") >>> dup = project.Anthropology.Duplicate(item) >>> print(f"Original: {project.Anthropology.GetGuid(item)}") >>> print(f"Duplicate: {project.Anthropology.GetGuid(dup)}") Original: 12345678-1234-1234-1234-123456789abc Duplicate: 87654321-4321-4321-4321-cba987654321 >>> # Deep duplicate (includes all subitems) >>> deep_dup = project.Anthropology.Duplicate(item, deep=True) >>> print(f"Subitems: {len(project.Anthropology.GetSubitems(deep_dup))}") Notes: - Factory.Create() automatically generates a new GUID - insert_after=True preserves the original item's position - Simple properties copied: Name, Abbreviation, Description, AnthroCode - Reference properties copied: CategoryRA - Owned objects (deep=True): SubPossibilitiesOS (subitems) - TextsRC and ResearchersRC collections are NOT duplicated - DateCreated and DateModified are NOT copied (set to current time) See Also: Create, Delete, GetGuid, GetSubitems """ self._EnsureWriteEnabled() self._ValidateParam(item_or_hvo, "item_or_hvo") # Get source item and parent source = self.__GetItemObject(item_or_hvo) # Determine parent (owner) with self._TransactionCM('Duplicate Anthropology Item'): raw_owner = source.Owner # Create new item using factory (auto-generates new GUID) factory = self.project.project.ServiceLocator.GetService(ICmAnthroItemFactory) duplicate = factory.Create() # Determine insertion position and add to parent FIRST # SubPossibilitiesOS is declared on ICmAnthroItem; cast by ClassName so # pythonnet surfaces the typed collection accessor. The top-level branch # bypasses the owner entirely via lp.AnthroListOA — no cast needed there. if hasattr(raw_owner, "ClassName") and raw_owner.ClassName == "CmAnthroItem": owner = ICmAnthroItem(raw_owner) # Parent is another anthropology item if insert_after: source_index = owner.SubPossibilitiesOS.IndexOf(source) owner.SubPossibilitiesOS.Insert(source_index + 1, duplicate) else: owner.SubPossibilitiesOS.Add(duplicate) else: # Parent is the top-level list anthro_list = self.project.lp.AnthroListOA if anthro_list is not None: if insert_after: source_index = anthro_list.PossibilitiesOS.IndexOf(source) anthro_list.PossibilitiesOS.Insert(source_index + 1, duplicate) else: anthro_list.PossibilitiesOS.Add(duplicate) # Copy simple MultiString properties duplicate.Name.CopyAlternatives(source.Name) duplicate.Abbreviation.CopyAlternatives(source.Abbreviation) duplicate.Description.CopyAlternatives(source.Description) # Copy simple string property if hasattr(source, "AnthroCode") and source.AnthroCode: duplicate.AnthroCode = source.AnthroCode # Copy Reference Atomic (RA) properties if hasattr(source, "CategoryRA") and source.CategoryRA: duplicate.CategoryRA = source.CategoryRA # Handle owned objects if deep=True if deep: # Duplicate subitems into the NEW duplicate (not the original's parent) if hasattr(source, "SubPossibilitiesOS"): for subitem in source.SubPossibilitiesOS: self._DuplicateSubitemInto(subitem, duplicate, deep=True) return duplicate def _DuplicateSubitemInto(self, source_item, parent_dup, deep=True): """Duplicate an anthropology subitem into the specified parent's SubPossibilitiesOS.""" factory = self.project.project.ServiceLocator.GetService(ICmAnthroItemFactory) dup_item = factory.Create() parent_dup.SubPossibilitiesOS.Add(dup_item) # Copy properties dup_item.Name.CopyAlternatives(source_item.Name) dup_item.Abbreviation.CopyAlternatives(source_item.Abbreviation) dup_item.Description.CopyAlternatives(source_item.Description) if hasattr(source_item, "AnthroCode") and source_item.AnthroCode: dup_item.AnthroCode = source_item.AnthroCode if hasattr(source_item, "CategoryRA") and source_item.CategoryRA: dup_item.CategoryRA = source_item.CategoryRA # Recurse into nested subitems if deep and hasattr(source_item, "SubPossibilitiesOS"): for nested_item in source_item.SubPossibilitiesOS: self._DuplicateSubitemInto(nested_item, dup_item, deep=True) # ========== CATALOG IMPORT METHODS ========== # # Unlike POS / PhonFeatures / InflectionFeatures (which use the etic # CatalogEntry parser via CatalogBackedMixin), the anthropology # catalog ships as LCM-native XML at: # # <FWCodeDir>/Templates/OCM.xml # # i.e. <LangProject><AnthroList><CmPossibilityList>... with # <CmAnthroItem guid="..."> items. That shape is what # SIL.LCModel.Application.ApplicationServices.XmlList.ImportList # consumes natively, so we delegate to it directly rather than # reimplementing the parser. Items whose canonical GUIDs already # exist in the project are reused; new ones are created; localized # Name/Abbreviation/Description and nested SubPossibilities all come # from the catalog. # # Note: OCM-Frame.xml is a sibling catalog (also in Templates/) and # is NOT wrapped by this method -- this phase covers OCM.xml only. @OperationsMethod def ImportCatalog(self, progress=None, force=False): """ Import the full OCM anthropology catalog into this project. Loads SIL's standard Outline of Cultural Materials (OCM) hierarchy from ``<FWCodeDir>/Templates/OCM.xml`` via LCM's native ``XmlList.ImportList``. The catalog supplies localized Name / Abbreviation / Description and the full nested SubPossibilities hierarchy of anthropology items. **Idempotency guard**: LCM's underlying ``XmlList.ImportList`` APPENDS items into ``AnthroListOA.PossibilitiesOS`` without checking for canonical-GUID duplicates -- each call adds another full set of top-level anthropology items. To prevent accidental duplication, this method refuses by default when the list is already non-empty. Pass ``force=True`` to import anyway (e.g. to layer a second-language localization on top of an existing import). The caller then takes responsibility for any resulting duplicates. **Recovery from past duplication**: If a project already has duplicate top-level anthropology items from prior unguarded imports, this method does NOT clean them up automatically -- that would be destructive. Recovery requires either manual cleanup via the FieldWorks GUI, or deliberate ``bulk_delete``- style code the caller writes to remove duplicate GUID groups. Note: single-item import via ``CreateFromCatalog(source_id)`` is not yet supported for this catalog. The OCM catalog is hierarchical; the typical workflow is a full import, after which existing operations (``Find``, ``FindByCode``, ``GetAll``) work normally. File a request if you need single-item import. Args: progress: Optional ``SIL.LCModel.Utils.IProgress`` instance to report import progress. Pass ``None`` (the default) for silent import. force: If True, skip the non-empty list check and import anyway. Default False (refuse if list is non-empty). Returns: int: Number of top-level anthropology items in ``LangProject.AnthroListOA.PossibilitiesOS`` after import. Useful as a sanity check for callers. Raises: FP_ReadOnlyError: If the project is not write-enabled. FP_FileNotFoundError: If OCM.xml cannot be located under the FieldWorks install. FP_ParameterError: If ``AnthroListOA`` already has anthro items and ``force`` is False. """ # Body extracted to _LCMNativeCatalogImportMixin in Phase Q-3a; # this thin wrapper preserves the @OperationsMethod descriptor # on the class itself (tests introspect via __dict__). return self._import_lcm_native_catalog(progress=progress, force=force) @OperationsMethod def ImportFrameCatalog(self, progress=None, force=False): """ Import the OCM-Frame catalog into this project. ``OCM-Frame.xml`` is an alternative complete anthropology catalog that ships alongside ``OCM.xml`` under ``<FWCodeDir>/Templates/``. It targets the same ``LangProject.AnthroListOA`` list but carries a different set of items: 116 GUIDs present in OCM-Frame are absent from OCM, so the two catalogs are NOT additive layers -- use one or the other as the primary catalog for a given project. Same XmlList.ImportList plumbing, same non-empty-list guard, same caveats around appending without GUID deduplication. See ``ImportCatalog`` for the full contract; only the catalog file differs. (issue #29 item 1) Args: progress: Optional ``SIL.LCModel.Utils.IProgress`` instance. Pass ``None`` (default) for silent import. force: If True, skip the non-empty list check. Default False -- which means ``ImportFrameCatalog`` will refuse whenever ``AnthroListOA`` already has items (including the items ``ImportCatalog`` just added). Layered imports therefore need ``force=True`` after the main OCM import. Returns: int: Number of top-level items in ``AnthroListOA`` after import. Raises: FP_ReadOnlyError: If the project is not write-enabled. FP_FileNotFoundError: If OCM-Frame.xml cannot be located. FP_ParameterError: If ``AnthroListOA`` already has items and ``force`` is False. See Also: ``FLExProject.LocalizedLists.Import``: localisation packs match by canonical GUID. ``OCM-Frame.xml`` carries 116 GUIDs that ``OCM.xml`` does not. An OCM-only pack against an OCM-Frame project will leave the 116 frame-specific items untranslated; an OCM-Frame-aware pack against an OCM-only project will silently drop translations for the 116 GUIDs the project doesn't have. If you call ``LocalizedLists.Import``, do so **after** the relevant catalog import and verify counts. (issue #82) """ return self._import_lcm_native_catalog( progress=progress, force=force, catalog_file=self.FRAME_CATALOG_FILE ) # ========== SYNC INTEGRATION METHODS ========== @OperationsMethod def GetSyncableProperties(self, item): """Get syncable properties for cross-project synchronization.""" self._ValidateParam(item, "item") anthro_item = self.__ResolveObject(item) wsHandle = self.project.project.DefaultAnalWs props = {} props["Name"] = ITsString(anthro_item.Name.get_String(wsHandle)).Text or "" props["Abbreviation"] = ITsString(anthro_item.Abbreviation.get_String(wsHandle)).Text or "" props["Description"] = ITsString(anthro_item.Description.get_String(wsHandle)).Text or "" if hasattr(anthro_item, "AnthroCode") and anthro_item.AnthroCode: props["AnthroCode"] = anthro_item.AnthroCode else: props["AnthroCode"] = None if hasattr(anthro_item, "CategoryRA") and anthro_item.CategoryRA: props["Category"] = str(anthro_item.CategoryRA.Guid) else: props["Category"] = None return props @OperationsMethod def CompareTo(self, item1, item2, ops1=None, ops2=None): """Compare two anthropology items and return detailed differences.""" if ops1 is None: ops1 = self if ops2 is None: ops2 = self is_different = False differences = {"properties": {}} props1 = ops1.GetSyncableProperties(item1) props2 = ops2.GetSyncableProperties(item2) for key in set(props1.keys()) | set(props2.keys()): val1 = props1.get(key) val2 = props2.get(key) if val1 != val2: is_different = True differences["properties"][key] = {"source": val1, "target": val2, "type": "modified"} return is_different, differences # --- Metadata Operations --- @OperationsMethod def GetGuid(self, item_or_hvo): """ Get the GUID (Globally Unique Identifier) of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: System.Guid: The GUID of the item. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> guid = project.Anthropology.GetGuid(item) >>> print(f"GUID: {guid}") GUID: 12345678-1234-1234-1234-123456789abc >>> # Use GUID for stable cross-references >>> guid_str = str(guid) >>> # Store guid_str in external database... Notes: - GUID is unique across all FLEx databases - GUID is permanent and never changes - Use GUIDs for stable references in external systems - Returns System.Guid object (use str() to convert to string) See Also: GetDateCreated, GetDateModified """ item = self.__GetItemObject(item_or_hvo) return item.Guid @OperationsMethod def GetDateCreated(self, item_or_hvo): """ Get the creation date of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: System.DateTime or None: The creation date, or None if not available. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> date = project.Anthropology.GetDateCreated(item) >>> if date: ... print(f"Created: {date}") Created: 2024-01-15 10:30:00 >>> # Find recently created items >>> from System import DateTime, TimeSpan >>> week_ago = DateTime.Now - TimeSpan.FromDays(7) >>> recent = [] >>> for item in project.Anthropology.GetAll(): ... date = project.Anthropology.GetDateCreated(item) ... if date and date > week_ago: ... name = project.Anthropology.GetName(item) ... recent.append(name) >>> print(f"Items created in last week: {recent}") Notes: - Returns System.DateTime object - May return None if date is not set or available - Date is set automatically when item is created - Dates are in UTC See Also: GetDateModified, GetGuid """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "DateCreated"): return item.DateCreated return None @OperationsMethod def GetDateModified(self, item_or_hvo): """ Get the last modification date of an anthropology item. Args: item_or_hvo: The ICmAnthroItem object or HVO. Returns: System.DateTime or None: The modification date, or None if not available. Raises: FP_NullParameterError: If item_or_hvo is None. FP_ParameterError: If the item is invalid. Example: >>> item = project.Anthropology.Find("Marriage Customs") >>> date = project.Anthropology.GetDateModified(item) >>> if date: ... print(f"Last modified: {date}") Last modified: 2024-03-20 14:45:00 >>> # Find recently modified items >>> from System import DateTime, TimeSpan >>> day_ago = DateTime.Now - TimeSpan.FromDays(1) >>> modified = [] >>> for item in project.Anthropology.GetAll(): ... date = project.Anthropology.GetDateModified(item) ... if date and date > day_ago: ... name = project.Anthropology.GetName(item) ... modified.append(name) >>> print(f"Modified today: {modified}") Notes: - Returns System.DateTime object - May return None if date is not set or available - Date is updated automatically when item is modified - Dates are in UTC - Tracks all modifications (name, description, links, etc.) See Also: GetDateCreated, GetGuid """ item = self.__GetItemObject(item_or_hvo) if hasattr(item, "DateModified"): return item.DateModified return None