From 17719515fabfb841998fd16eb73ee34e84dd6f97 Mon Sep 17 00:00:00 2001 From: "Jeff Anderson (Fargo)" Date: Wed, 17 Sep 2025 14:50:40 -0500 Subject: [PATCH 1/3] Added ability to create relationships either in initial table definition or as a separate operation --- examples/inline_lookup_example.py | 128 +++++++++++++++++ examples/relationship_example.py | 123 +++++++++++++++++ src/dataverse_sdk/client.py | 77 ++++++++++- src/dataverse_sdk/odata.py | 221 ++++++++++++++++++++++++++++-- 4 files changed, 537 insertions(+), 12 deletions(-) create mode 100644 examples/inline_lookup_example.py create mode 100644 examples/relationship_example.py diff --git a/examples/inline_lookup_example.py b/examples/inline_lookup_example.py new file mode 100644 index 0000000..8b46dd0 --- /dev/null +++ b/examples/inline_lookup_example.py @@ -0,0 +1,128 @@ +""" +Example demonstrating how to create tables with lookup fields inline. + +This example: +1. Creates a Project table with basic fields +2. Creates a Task table with a lookup field to Project in a single step +3. Creates records in both tables and demonstrates the relationship +""" + +from dataverse_sdk import DataverseClient +import os +from datetime import datetime + +# Get credentials from environment variables +BASE_URL = os.environ.get("DATAVERSE_URL") + +# Initialize client +client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default + +def main(): + # 1. Create the Project table + project_schema = { + "name": "string", + "description": "string", + "start_date": "datetime", + "end_date": "datetime", + "budget": "decimal" + } + + print("Creating Project table...") + project_info = client.create_table("Project", project_schema) + project_entity = project_info["entity_logical_name"] + project_entity_set = project_info["entity_set_name"] + print(f"Created Project table: {project_entity} (Set: {project_entity_set})") + + # 2. Create the Task table with an inline lookup field to Project + task_schema = { + "title": "string", + "description": "string", + "status": "string", + "due_date": "datetime", + "estimated_hours": "decimal", + # Define a lookup field inline + "project": { + "lookup": project_entity, # Reference the logical name of the target table + "display_name": "Project", + "description": "The project this task belongs to", + "required_level": "Recommended", + "cascade_delete": "Cascade" # Delete tasks when project is deleted + } + } + + print("Creating Task table with project lookup...") + task_info = client.create_table("Task", task_schema) + task_entity = task_info["entity_logical_name"] + task_entity_set = task_info["entity_set_name"] + print(f"Created Task table: {task_entity} (Set: {task_entity_set})") + print(f"Columns created: {task_info['columns_created']}") + + # Find the created lookup field name + lookup_field = None + for column in task_info["columns_created"]: + if "project" in column.lower(): + lookup_field = column + break + + if not lookup_field: + print("Could not find project lookup field!") + return + + print(f"Created lookup field: {lookup_field}") + + # 3. Create a project record + project_data = { + "new_name": "Website Redesign", + "new_description": "Complete overhaul of company website", + "new_start_date": datetime.now().isoformat(), + "new_end_date": datetime(2023, 12, 31).isoformat(), + "new_budget": 25000.00 + } + + print("Creating project record...") + project_record = client.create(project_entity_set, project_data) + project_id = project_record["new_projectid"] + print(f"Created project with ID: {project_id}") + + # 4. Create a task linked to the project + # The lookup field name follows the pattern: new_project_id + lookup_field_name = lookup_field.lower() + "id" + + task_data = { + "new_title": "Design homepage mockup", + "new_description": "Create initial design mockups for homepage", + "new_status": "Not Started", + "new_due_date": datetime(2023, 10, 15).isoformat(), + "new_estimated_hours": 16.5, + # Add the lookup reference + lookup_field_name: project_id + } + + print("Creating task record with project reference...") + task_record = client.create(task_entity_set, task_data) + task_id = task_record["new_taskid"] + print(f"Created task with ID: {task_id}") + + # 5. Fetch the task with project reference + print("Fetching task with project information...") + expand_field = lookup_field.lower() + "_expand" + + # Use the OData $expand syntax to retrieve the related project + odata_client = client._get_odata() + task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field]) + + # Display the relationship information + print("\nTask details:") + print(f"Title: {task_with_project['new_title']}") + print(f"Status: {task_with_project['new_status']}") + + project_ref = task_with_project.get(expand_field) + if project_ref: + print("\nLinked Project:") + print(f"Name: {project_ref['new_name']}") + print(f"Budget: ${project_ref['new_budget']}") + + print("\nInline lookup field creation successfully demonstrated!") + +if __name__ == "__main__": + main() diff --git a/examples/relationship_example.py b/examples/relationship_example.py new file mode 100644 index 0000000..66303b0 --- /dev/null +++ b/examples/relationship_example.py @@ -0,0 +1,123 @@ +""" +Example demonstrating how to create lookup fields (n:1 relationships) between tables. + +This example: +1. Creates two tables: 'Project' and 'Task' +2. Creates a lookup field in Task that references Project +3. Creates a record in Project +4. Creates a Task record linked to the Project +5. Queries both records showing the relationship +""" + +from dataverse_sdk import DataverseClient +import os +from datetime import datetime + +# Get credentials from environment variables +BASE_URL = os.environ.get("DATAVERSE_URL") + +# Initialize client +client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default + +def main(): + # 1. Create the Project table + project_schema = { + "name": "string", + "description": "string", + "start_date": "datetime", + "end_date": "datetime", + "budget": "decimal" + } + + print("Creating Project table...") + project_info = client.create_table("Project", project_schema) + project_entity = project_info["entity_logical_name"] + project_entity_set = project_info["entity_set_name"] + print(f"Created Project table: {project_entity} (Set: {project_entity_set})") + + # 2. Create the Task table + task_schema = { + "title": "string", + "description": "string", + "status": "string", + "due_date": "datetime", + "estimated_hours": "decimal", + } + + print("Creating Task table...") + task_info = client.create_table("Task", task_schema) + task_entity = task_info["entity_logical_name"] + task_entity_set = task_info["entity_set_name"] + print(f"Created Task table: {task_entity} (Set: {task_entity_set})") + + # 3. Create a lookup field from Task to Project + print("Creating lookup relationship...") + relationship_info = client.create_lookup_field( + table_name=task_entity, + field_name="project", + target_table=project_entity, + display_name="Project", + description="The project this task belongs to", + required_level="Recommended", # Recommended but not required + cascade_delete="Cascade" # Delete tasks when project is deleted + ) + + print(f"Created relationship: {relationship_info['relationship_name']}") + print(f"Lookup field created: {relationship_info['lookup_field']}") + + # 4. Create a project record + project_data = { + "new_name": "Website Redesign", + "new_description": "Complete overhaul of company website", + "new_start_date": datetime.now().isoformat(), + "new_end_date": datetime(2023, 12, 31).isoformat(), + "new_budget": 25000.00 + } + + print("Creating project record...") + project_record = client.create(project_entity_set, project_data) + project_id = project_record["new_projectid"] + print(f"Created project with ID: {project_id}") + + # 5. Create a task linked to the project + # The lookup field name follows the pattern: new_project_id + lookup_field_name = relationship_info["lookup_field"].lower() + "id" + + task_data = { + "new_title": "Design homepage mockup", + "new_description": "Create initial design mockups for homepage", + "new_status": "Not Started", + "new_due_date": datetime(2023, 10, 15).isoformat(), + "new_estimated_hours": 16.5, + # Add the lookup reference + lookup_field_name: project_id + } + + print("Creating task record with project reference...") + task_record = client.create(task_entity_set, task_data) + task_id = task_record["new_taskid"] + print(f"Created task with ID: {task_id}") + + # 6. Fetch the task with project reference + print("Fetching task with project information...") + expand_field = relationship_info["lookup_field"].lower() + "_expand" + + # Use the OData $expand syntax to retrieve the related project + odata_client = client._get_odata() + task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field]) + + # Display the relationship information + print("\nTask details:") + print(f"Title: {task_with_project['new_title']}") + print(f"Status: {task_with_project['new_status']}") + + project_ref = task_with_project.get(expand_field) + if project_ref: + print("\nLinked Project:") + print(f"Name: {project_ref['new_name']}") + print(f"Budget: ${project_ref['new_budget']}") + + print("\nRelationship successfully created and verified!") + +if __name__ == "__main__": + main() diff --git a/src/dataverse_sdk/client.py b/src/dataverse_sdk/client.py index 5adf09b..cdfd519 100644 --- a/src/dataverse_sdk/client.py +++ b/src/dataverse_sdk/client.py @@ -198,16 +198,32 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]: """ return self._get_odata().get_table_info(tablename) - def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]: + def create_table(self, tablename: str, schema: Dict[str, Union[str, Dict[str, Any]]]) -> Dict[str, Any]: """Create a simple custom table. Parameters ---------- tablename : str Friendly name (``"SampleItem"``) or a full schema name (``"new_SampleItem"``). - schema : dict[str, str] - Column definitions mapping logical names (without prefix) to types. - Supported: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``. + schema : dict[str, str | dict] + Column definitions mapping logical names to types or lookup configurations. + + For standard columns, use string type names: + ``"name": "string", "count": "int", "price": "decimal"`` + + Supported types: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``. + + For lookup fields, use a dictionary with configuration options: + ``"project": {"lookup": "new_project", "display_name": "Project", "cascade_delete": "Cascade"}`` + + Lookup field options: + - ``lookup``: Target table (required) + - ``display_name``: Display name for the field (optional) + - ``description``: Description for the field (optional) + - ``required_level``: "None", "Recommended", or "ApplicationRequired" (default: "None") + - ``relationship_name``: Custom name for the relationship (optional) + - ``relationship_behavior``: "UseLabel", "UseCollectionName", "DoNotDisplay" (default: "UseLabel") + - ``cascade_delete``: "Cascade", "RemoveLink", "Restrict" (default: "RemoveLink") Returns ------- @@ -217,6 +233,59 @@ def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any] """ return self._get_odata().create_table(tablename, schema) + def create_lookup_field( + self, + table_name: str, + field_name: str, + target_table: str, + display_name: Optional[str] = None, + description: Optional[str] = None, + required_level: str = "None", + relationship_name: Optional[str] = None, + relationship_behavior: str = "UseLabel", + cascade_delete: str = "RemoveLink", + ) -> Dict[str, Any]: + """Create a lookup field (n:1 relationship) between two tables. + + Parameters + ---------- + table_name : str + The table where the lookup field will be created. + field_name : str + The name of the lookup field to create. + target_table : str + The table the lookup will reference. + display_name : str, optional + The display name for the lookup field. If not provided, will use target table name. + description : str, optional + The description for the lookup field. + required_level : str, optional + The requirement level: "None", "Recommended", or "ApplicationRequired". + relationship_name : str, optional + The name of the relationship. If not provided, one will be generated. + relationship_behavior : str, optional + The relationship menu behavior: "UseLabel", "UseCollectionName", "DoNotDisplay". + cascade_delete : str, optional + The cascade behavior on delete: "Cascade", "RemoveLink", "Restrict". + + Returns + ------- + dict + Details about the created relationship including relationship_id, relationship_name, + lookup_field, referenced_entity, and referencing_entity. + """ + return self._get_odata().create_lookup_field( + table_name, + field_name, + target_table, + display_name, + description, + required_level, + relationship_name, + relationship_behavior, + cascade_delete + ) + def delete_table(self, tablename: str) -> None: """Delete a custom table by name. diff --git a/src/dataverse_sdk/odata.py b/src/dataverse_sdk/odata.py index 788193b..1369a27 100644 --- a/src/dataverse_sdk/odata.py +++ b/src/dataverse_sdk/odata.py @@ -102,11 +102,9 @@ def _logical_from_entity_set(self, entity_set: str) -> str: if cached: return cached url = f"{self.api}/EntityDefinitions" - # Escape single quotes in entity set name - es_escaped = self._escape_odata_quotes(es) params = { "$select": "LogicalName,EntitySetName", - "$filter": f"EntitySetName eq '{es_escaped}'", + "$filter": f"EntitySetName eq '{es}'", } r = self._request("get", url, headers=self._headers(), params=params) r.raise_for_status() @@ -374,11 +372,9 @@ def _to_pascal(self, name: str) -> str: def _get_entity_by_schema(self, schema_name: str) -> Optional[Dict[str, Any]]: url = f"{self.api}/EntityDefinitions" - # Escape single quotes in schema name - schema_escaped = self._escape_odata_quotes(schema_name) params = { "$select": "MetadataId,LogicalName,SchemaName,EntitySetName", - "$filter": f"SchemaName eq '{schema_escaped}'", + "$filter": f"SchemaName eq '{schema_name}'", } r = self._request("get", url, headers=self._headers(), params=params) r.raise_for_status() @@ -540,7 +536,176 @@ def delete_table(self, tablename: str) -> None: r = self._request("delete", url, headers=headers) r.raise_for_status() - def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]: + def create_lookup_field( + self, + table_name: str, + field_name: str, + target_table: str, + display_name: str = None, + description: str = None, + required_level: str = "None", + relationship_name: str = None, + relationship_behavior: str = "UseLabel", + cascade_delete: str = "RemoveLink", + ) -> Dict[str, Any]: + """ + Create a lookup field (n:1 relationship) between two tables. + + Parameters + ---------- + table_name : str + The logical name of the table where the lookup field will be created (referencing entity). + field_name : str + The name of the lookup field to create (without _id suffix). + target_table : str + The logical name of the table the lookup will reference (referenced entity). + display_name : str, optional + The display name for the lookup field. + description : str, optional + The description for the lookup field. + required_level : str, optional + The requirement level: "None", "Recommended", or "ApplicationRequired". + relationship_name : str, optional + The name of the relationship. If not provided, one will be generated. + relationship_behavior : str, optional + The relationship menu behavior: "UseLabel", "UseCollectionName", "DoNotDisplay". + cascade_delete : str, optional + The cascade behavior on delete: "Cascade", "RemoveLink", "Restrict". + + Returns + ------- + dict + Details about the created relationship. + """ + # Get information about both tables + referencing_entity = self._get_entity_by_schema(table_name) + referenced_entity = self._get_entity_by_schema(target_table) + + if not referencing_entity: + raise ValueError(f"Table '{table_name}' not found.") + if not referenced_entity: + raise ValueError(f"Target table '{target_table}' not found.") + + referencing_logical_name = referencing_entity.get("LogicalName") + referenced_logical_name = referenced_entity.get("LogicalName") + + if not referencing_logical_name or not referenced_logical_name: + raise ValueError("Could not determine logical names for the tables.") + + # If no relationship name provided, generate one + if not relationship_name: + relationship_name = f"{referenced_logical_name}_{referencing_logical_name}" + + # If no display name provided, use the target table name + if not display_name: + display_name = self._to_pascal(referenced_logical_name) + + # Prepare relationship metadata + one_to_many_relationship = { + "@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata", + "SchemaName": relationship_name, + "ReferencedEntity": referenced_logical_name, + "ReferencingEntity": referencing_logical_name, + "ReferencedAttribute": f"{referenced_logical_name}id", # Usually the primary key attribute + "AssociatedMenuConfiguration": { + "Behavior": relationship_behavior, + "Group": "Details", + "Label": { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", + "Label": display_name or referenced_logical_name, + "LanguageCode": int(self.config.language_code), + } + ], + "UserLocalizedLabel": { + "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", + "Label": display_name or referenced_logical_name, + "LanguageCode": int(self.config.language_code), + } + }, + "Order": 10000 + }, + "CascadeConfiguration": { + "Assign": "NoCascade", + "Delete": cascade_delete, + "Merge": "NoCascade", + "Reparent": "NoCascade", + "Share": "NoCascade", + "Unshare": "NoCascade" + } + } + + # Prepare lookup attribute metadata + lookup_field_schema_name = f"{field_name}" + if not lookup_field_schema_name.lower().startswith(f"{referencing_logical_name.split('_')[0]}_"): + lookup_field_schema_name = f"{referencing_logical_name.split('_')[0]}_{field_name}" + + lookup_attribute = { + "@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata", + "SchemaName": lookup_field_schema_name, + "DisplayName": { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", + "Label": display_name or field_name, + "LanguageCode": int(self.config.language_code), + } + ] + } + } + + if description: + lookup_attribute["Description"] = { + "@odata.type": "Microsoft.Dynamics.CRM.Label", + "LocalizedLabels": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel", + "Label": description, + "LanguageCode": int(self.config.language_code), + } + ] + } + + lookup_attribute["RequiredLevel"] = { + "Value": required_level, + "CanBeChanged": True, + "ManagedPropertyLogicalName": "canmodifyrequirementlevelsettings" + } + + # Create the relationship + url = f"{self.api}/RelationshipDefinitions" + headers = self._headers().copy() + + # Add the lookup attribute to the relationship definition + one_to_many_relationship["Lookup"] = lookup_attribute + + # POST the relationship definition + r = self._request("post", url, headers=headers, json=one_to_many_relationship) + r.raise_for_status() + + # Get the relationship ID from the OData-EntityId header + relationship_id = None + if "OData-EntityId" in r.headers: + entity_id_url = r.headers["OData-EntityId"] + # Extract GUID from the URL + import re + match = re.search(r'RelationshipDefinitions\((.*?)\)', entity_id_url) + if match: + relationship_id = match.group(1) + + # Return relationship info + return { + "relationship_id": relationship_id, + "relationship_name": relationship_name, + "lookup_field": lookup_field_schema_name, + "referenced_entity": referenced_logical_name, + "referencing_entity": referencing_logical_name + } + + def create_table(self, tablename: str, schema: Dict[str, Union[str, Dict[str, Any]]]) -> Dict[str, Any]: # Accept a friendly name and construct a default schema under 'new_'. # If a full SchemaName is passed (contains '_'), use as-is. entity_schema = tablename if "_" in tablename else f"new_{self._to_pascal(tablename)}" @@ -553,9 +718,30 @@ def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any] primary_attr_schema = "new_Name" if "_" not in entity_schema else f"{entity_schema.split('_',1)[0]}_Name" attributes: List[Dict[str, Any]] = [] attributes.append(self._attribute_payload(primary_attr_schema, "string", is_primary_name=True)) - for col_name, dtype in schema.items(): + + # Track lookups to create after table creation + lookup_fields = [] + + for col_name, col_info in schema.items(): # Use same publisher prefix segment as entity_schema if present; else default to 'new_'. publisher = entity_schema.split("_", 1)[0] if "_" in entity_schema else "new" + + # Handle lookup fields (dictionary values in schema) + if isinstance(col_info, dict) and "lookup" in col_info: + lookup_fields.append({ + "field_name": col_name, + "target_table": col_info["lookup"], + "display_name": col_info.get("display_name"), + "description": col_info.get("description"), + "required_level": col_info.get("required_level", "None"), + "relationship_name": col_info.get("relationship_name"), + "relationship_behavior": col_info.get("relationship_behavior", "UseLabel"), + "cascade_delete": col_info.get("cascade_delete", "RemoveLink"), + }) + continue + + # Handle regular fields (string type values) + dtype = col_info if isinstance(col_info, str) else "string" if col_name.lower().startswith(f"{publisher}_"): attr_schema = col_name else: @@ -569,6 +755,25 @@ def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any] metadata_id = self._create_entity(entity_schema, tablename, attributes) ent2: Dict[str, Any] = self._wait_for_entity_ready(entity_schema) or {} logical_name = ent2.get("LogicalName") + + # Create lookup fields after table is created + for lookup in lookup_fields: + try: + lookup_result = self.create_lookup_field( + table_name=logical_name, + field_name=lookup["field_name"], + target_table=lookup["target_table"], + display_name=lookup["display_name"], + description=lookup["description"], + required_level=lookup["required_level"], + relationship_name=lookup["relationship_name"], + relationship_behavior=lookup["relationship_behavior"], + cascade_delete=lookup["cascade_delete"] + ) + created_cols.append(lookup_result["lookup_field"]) + except Exception as e: + # Continue creating other lookup fields even if one fails + print(f"Warning: Could not create lookup field '{lookup['field_name']}': {str(e)}") return { "entity_schema": entity_schema, From cfea0741adcb2e77d65469a793809deb1615500f Mon Sep 17 00:00:00 2001 From: "Jeff Anderson (Fargo)" Date: Wed, 17 Sep 2025 14:53:09 -0500 Subject: [PATCH 2/3] Removed unexpected files --- examples/inline_lookup_example.py | 128 ------------------------------ examples/relationship_example.py | 123 ---------------------------- 2 files changed, 251 deletions(-) delete mode 100644 examples/inline_lookup_example.py delete mode 100644 examples/relationship_example.py diff --git a/examples/inline_lookup_example.py b/examples/inline_lookup_example.py deleted file mode 100644 index 8b46dd0..0000000 --- a/examples/inline_lookup_example.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -Example demonstrating how to create tables with lookup fields inline. - -This example: -1. Creates a Project table with basic fields -2. Creates a Task table with a lookup field to Project in a single step -3. Creates records in both tables and demonstrates the relationship -""" - -from dataverse_sdk import DataverseClient -import os -from datetime import datetime - -# Get credentials from environment variables -BASE_URL = os.environ.get("DATAVERSE_URL") - -# Initialize client -client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default - -def main(): - # 1. Create the Project table - project_schema = { - "name": "string", - "description": "string", - "start_date": "datetime", - "end_date": "datetime", - "budget": "decimal" - } - - print("Creating Project table...") - project_info = client.create_table("Project", project_schema) - project_entity = project_info["entity_logical_name"] - project_entity_set = project_info["entity_set_name"] - print(f"Created Project table: {project_entity} (Set: {project_entity_set})") - - # 2. Create the Task table with an inline lookup field to Project - task_schema = { - "title": "string", - "description": "string", - "status": "string", - "due_date": "datetime", - "estimated_hours": "decimal", - # Define a lookup field inline - "project": { - "lookup": project_entity, # Reference the logical name of the target table - "display_name": "Project", - "description": "The project this task belongs to", - "required_level": "Recommended", - "cascade_delete": "Cascade" # Delete tasks when project is deleted - } - } - - print("Creating Task table with project lookup...") - task_info = client.create_table("Task", task_schema) - task_entity = task_info["entity_logical_name"] - task_entity_set = task_info["entity_set_name"] - print(f"Created Task table: {task_entity} (Set: {task_entity_set})") - print(f"Columns created: {task_info['columns_created']}") - - # Find the created lookup field name - lookup_field = None - for column in task_info["columns_created"]: - if "project" in column.lower(): - lookup_field = column - break - - if not lookup_field: - print("Could not find project lookup field!") - return - - print(f"Created lookup field: {lookup_field}") - - # 3. Create a project record - project_data = { - "new_name": "Website Redesign", - "new_description": "Complete overhaul of company website", - "new_start_date": datetime.now().isoformat(), - "new_end_date": datetime(2023, 12, 31).isoformat(), - "new_budget": 25000.00 - } - - print("Creating project record...") - project_record = client.create(project_entity_set, project_data) - project_id = project_record["new_projectid"] - print(f"Created project with ID: {project_id}") - - # 4. Create a task linked to the project - # The lookup field name follows the pattern: new_project_id - lookup_field_name = lookup_field.lower() + "id" - - task_data = { - "new_title": "Design homepage mockup", - "new_description": "Create initial design mockups for homepage", - "new_status": "Not Started", - "new_due_date": datetime(2023, 10, 15).isoformat(), - "new_estimated_hours": 16.5, - # Add the lookup reference - lookup_field_name: project_id - } - - print("Creating task record with project reference...") - task_record = client.create(task_entity_set, task_data) - task_id = task_record["new_taskid"] - print(f"Created task with ID: {task_id}") - - # 5. Fetch the task with project reference - print("Fetching task with project information...") - expand_field = lookup_field.lower() + "_expand" - - # Use the OData $expand syntax to retrieve the related project - odata_client = client._get_odata() - task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field]) - - # Display the relationship information - print("\nTask details:") - print(f"Title: {task_with_project['new_title']}") - print(f"Status: {task_with_project['new_status']}") - - project_ref = task_with_project.get(expand_field) - if project_ref: - print("\nLinked Project:") - print(f"Name: {project_ref['new_name']}") - print(f"Budget: ${project_ref['new_budget']}") - - print("\nInline lookup field creation successfully demonstrated!") - -if __name__ == "__main__": - main() diff --git a/examples/relationship_example.py b/examples/relationship_example.py deleted file mode 100644 index 66303b0..0000000 --- a/examples/relationship_example.py +++ /dev/null @@ -1,123 +0,0 @@ -""" -Example demonstrating how to create lookup fields (n:1 relationships) between tables. - -This example: -1. Creates two tables: 'Project' and 'Task' -2. Creates a lookup field in Task that references Project -3. Creates a record in Project -4. Creates a Task record linked to the Project -5. Queries both records showing the relationship -""" - -from dataverse_sdk import DataverseClient -import os -from datetime import datetime - -# Get credentials from environment variables -BASE_URL = os.environ.get("DATAVERSE_URL") - -# Initialize client -client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default - -def main(): - # 1. Create the Project table - project_schema = { - "name": "string", - "description": "string", - "start_date": "datetime", - "end_date": "datetime", - "budget": "decimal" - } - - print("Creating Project table...") - project_info = client.create_table("Project", project_schema) - project_entity = project_info["entity_logical_name"] - project_entity_set = project_info["entity_set_name"] - print(f"Created Project table: {project_entity} (Set: {project_entity_set})") - - # 2. Create the Task table - task_schema = { - "title": "string", - "description": "string", - "status": "string", - "due_date": "datetime", - "estimated_hours": "decimal", - } - - print("Creating Task table...") - task_info = client.create_table("Task", task_schema) - task_entity = task_info["entity_logical_name"] - task_entity_set = task_info["entity_set_name"] - print(f"Created Task table: {task_entity} (Set: {task_entity_set})") - - # 3. Create a lookup field from Task to Project - print("Creating lookup relationship...") - relationship_info = client.create_lookup_field( - table_name=task_entity, - field_name="project", - target_table=project_entity, - display_name="Project", - description="The project this task belongs to", - required_level="Recommended", # Recommended but not required - cascade_delete="Cascade" # Delete tasks when project is deleted - ) - - print(f"Created relationship: {relationship_info['relationship_name']}") - print(f"Lookup field created: {relationship_info['lookup_field']}") - - # 4. Create a project record - project_data = { - "new_name": "Website Redesign", - "new_description": "Complete overhaul of company website", - "new_start_date": datetime.now().isoformat(), - "new_end_date": datetime(2023, 12, 31).isoformat(), - "new_budget": 25000.00 - } - - print("Creating project record...") - project_record = client.create(project_entity_set, project_data) - project_id = project_record["new_projectid"] - print(f"Created project with ID: {project_id}") - - # 5. Create a task linked to the project - # The lookup field name follows the pattern: new_project_id - lookup_field_name = relationship_info["lookup_field"].lower() + "id" - - task_data = { - "new_title": "Design homepage mockup", - "new_description": "Create initial design mockups for homepage", - "new_status": "Not Started", - "new_due_date": datetime(2023, 10, 15).isoformat(), - "new_estimated_hours": 16.5, - # Add the lookup reference - lookup_field_name: project_id - } - - print("Creating task record with project reference...") - task_record = client.create(task_entity_set, task_data) - task_id = task_record["new_taskid"] - print(f"Created task with ID: {task_id}") - - # 6. Fetch the task with project reference - print("Fetching task with project information...") - expand_field = relationship_info["lookup_field"].lower() + "_expand" - - # Use the OData $expand syntax to retrieve the related project - odata_client = client._get_odata() - task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field]) - - # Display the relationship information - print("\nTask details:") - print(f"Title: {task_with_project['new_title']}") - print(f"Status: {task_with_project['new_status']}") - - project_ref = task_with_project.get(expand_field) - if project_ref: - print("\nLinked Project:") - print(f"Name: {project_ref['new_name']}") - print(f"Budget: ${project_ref['new_budget']}") - - print("\nRelationship successfully created and verified!") - -if __name__ == "__main__": - main() From f62c262122d7e108927b68c4da9098d3bc292f8d Mon Sep 17 00:00:00 2001 From: "Jeff Anderson (Fargo)" Date: Thu, 18 Sep 2025 15:05:30 -0500 Subject: [PATCH 3/3] Removed explicit add_lookup method per review feedbackg --- src/dataverse_sdk/client.py | 53 ------------------------------------- 1 file changed, 53 deletions(-) diff --git a/src/dataverse_sdk/client.py b/src/dataverse_sdk/client.py index cdfd519..0aafcb8 100644 --- a/src/dataverse_sdk/client.py +++ b/src/dataverse_sdk/client.py @@ -233,59 +233,6 @@ def create_table(self, tablename: str, schema: Dict[str, Union[str, Dict[str, An """ return self._get_odata().create_table(tablename, schema) - def create_lookup_field( - self, - table_name: str, - field_name: str, - target_table: str, - display_name: Optional[str] = None, - description: Optional[str] = None, - required_level: str = "None", - relationship_name: Optional[str] = None, - relationship_behavior: str = "UseLabel", - cascade_delete: str = "RemoveLink", - ) -> Dict[str, Any]: - """Create a lookup field (n:1 relationship) between two tables. - - Parameters - ---------- - table_name : str - The table where the lookup field will be created. - field_name : str - The name of the lookup field to create. - target_table : str - The table the lookup will reference. - display_name : str, optional - The display name for the lookup field. If not provided, will use target table name. - description : str, optional - The description for the lookup field. - required_level : str, optional - The requirement level: "None", "Recommended", or "ApplicationRequired". - relationship_name : str, optional - The name of the relationship. If not provided, one will be generated. - relationship_behavior : str, optional - The relationship menu behavior: "UseLabel", "UseCollectionName", "DoNotDisplay". - cascade_delete : str, optional - The cascade behavior on delete: "Cascade", "RemoveLink", "Restrict". - - Returns - ------- - dict - Details about the created relationship including relationship_id, relationship_name, - lookup_field, referenced_entity, and referencing_entity. - """ - return self._get_odata().create_lookup_field( - table_name, - field_name, - target_table, - display_name, - description, - required_level, - relationship_name, - relationship_behavior, - cascade_delete - ) - def delete_table(self, tablename: str) -> None: """Delete a custom table by name.