Skip to content

Commit 1771951

Browse files
committed
Added ability to create relationships either in initial table definition or as a separate operation
1 parent e93fa6e commit 1771951

File tree

4 files changed

+537
-12
lines changed

4 files changed

+537
-12
lines changed

examples/inline_lookup_example.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Example demonstrating how to create tables with lookup fields inline.
3+
4+
This example:
5+
1. Creates a Project table with basic fields
6+
2. Creates a Task table with a lookup field to Project in a single step
7+
3. Creates records in both tables and demonstrates the relationship
8+
"""
9+
10+
from dataverse_sdk import DataverseClient
11+
import os
12+
from datetime import datetime
13+
14+
# Get credentials from environment variables
15+
BASE_URL = os.environ.get("DATAVERSE_URL")
16+
17+
# Initialize client
18+
client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default
19+
20+
def main():
21+
# 1. Create the Project table
22+
project_schema = {
23+
"name": "string",
24+
"description": "string",
25+
"start_date": "datetime",
26+
"end_date": "datetime",
27+
"budget": "decimal"
28+
}
29+
30+
print("Creating Project table...")
31+
project_info = client.create_table("Project", project_schema)
32+
project_entity = project_info["entity_logical_name"]
33+
project_entity_set = project_info["entity_set_name"]
34+
print(f"Created Project table: {project_entity} (Set: {project_entity_set})")
35+
36+
# 2. Create the Task table with an inline lookup field to Project
37+
task_schema = {
38+
"title": "string",
39+
"description": "string",
40+
"status": "string",
41+
"due_date": "datetime",
42+
"estimated_hours": "decimal",
43+
# Define a lookup field inline
44+
"project": {
45+
"lookup": project_entity, # Reference the logical name of the target table
46+
"display_name": "Project",
47+
"description": "The project this task belongs to",
48+
"required_level": "Recommended",
49+
"cascade_delete": "Cascade" # Delete tasks when project is deleted
50+
}
51+
}
52+
53+
print("Creating Task table with project lookup...")
54+
task_info = client.create_table("Task", task_schema)
55+
task_entity = task_info["entity_logical_name"]
56+
task_entity_set = task_info["entity_set_name"]
57+
print(f"Created Task table: {task_entity} (Set: {task_entity_set})")
58+
print(f"Columns created: {task_info['columns_created']}")
59+
60+
# Find the created lookup field name
61+
lookup_field = None
62+
for column in task_info["columns_created"]:
63+
if "project" in column.lower():
64+
lookup_field = column
65+
break
66+
67+
if not lookup_field:
68+
print("Could not find project lookup field!")
69+
return
70+
71+
print(f"Created lookup field: {lookup_field}")
72+
73+
# 3. Create a project record
74+
project_data = {
75+
"new_name": "Website Redesign",
76+
"new_description": "Complete overhaul of company website",
77+
"new_start_date": datetime.now().isoformat(),
78+
"new_end_date": datetime(2023, 12, 31).isoformat(),
79+
"new_budget": 25000.00
80+
}
81+
82+
print("Creating project record...")
83+
project_record = client.create(project_entity_set, project_data)
84+
project_id = project_record["new_projectid"]
85+
print(f"Created project with ID: {project_id}")
86+
87+
# 4. Create a task linked to the project
88+
# The lookup field name follows the pattern: new_project_id
89+
lookup_field_name = lookup_field.lower() + "id"
90+
91+
task_data = {
92+
"new_title": "Design homepage mockup",
93+
"new_description": "Create initial design mockups for homepage",
94+
"new_status": "Not Started",
95+
"new_due_date": datetime(2023, 10, 15).isoformat(),
96+
"new_estimated_hours": 16.5,
97+
# Add the lookup reference
98+
lookup_field_name: project_id
99+
}
100+
101+
print("Creating task record with project reference...")
102+
task_record = client.create(task_entity_set, task_data)
103+
task_id = task_record["new_taskid"]
104+
print(f"Created task with ID: {task_id}")
105+
106+
# 5. Fetch the task with project reference
107+
print("Fetching task with project information...")
108+
expand_field = lookup_field.lower() + "_expand"
109+
110+
# Use the OData $expand syntax to retrieve the related project
111+
odata_client = client._get_odata()
112+
task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field])
113+
114+
# Display the relationship information
115+
print("\nTask details:")
116+
print(f"Title: {task_with_project['new_title']}")
117+
print(f"Status: {task_with_project['new_status']}")
118+
119+
project_ref = task_with_project.get(expand_field)
120+
if project_ref:
121+
print("\nLinked Project:")
122+
print(f"Name: {project_ref['new_name']}")
123+
print(f"Budget: ${project_ref['new_budget']}")
124+
125+
print("\nInline lookup field creation successfully demonstrated!")
126+
127+
if __name__ == "__main__":
128+
main()

examples/relationship_example.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Example demonstrating how to create lookup fields (n:1 relationships) between tables.
3+
4+
This example:
5+
1. Creates two tables: 'Project' and 'Task'
6+
2. Creates a lookup field in Task that references Project
7+
3. Creates a record in Project
8+
4. Creates a Task record linked to the Project
9+
5. Queries both records showing the relationship
10+
"""
11+
12+
from dataverse_sdk import DataverseClient
13+
import os
14+
from datetime import datetime
15+
16+
# Get credentials from environment variables
17+
BASE_URL = os.environ.get("DATAVERSE_URL")
18+
19+
# Initialize client
20+
client = DataverseClient(BASE_URL) # Uses DefaultAzureCredential by default
21+
22+
def main():
23+
# 1. Create the Project table
24+
project_schema = {
25+
"name": "string",
26+
"description": "string",
27+
"start_date": "datetime",
28+
"end_date": "datetime",
29+
"budget": "decimal"
30+
}
31+
32+
print("Creating Project table...")
33+
project_info = client.create_table("Project", project_schema)
34+
project_entity = project_info["entity_logical_name"]
35+
project_entity_set = project_info["entity_set_name"]
36+
print(f"Created Project table: {project_entity} (Set: {project_entity_set})")
37+
38+
# 2. Create the Task table
39+
task_schema = {
40+
"title": "string",
41+
"description": "string",
42+
"status": "string",
43+
"due_date": "datetime",
44+
"estimated_hours": "decimal",
45+
}
46+
47+
print("Creating Task table...")
48+
task_info = client.create_table("Task", task_schema)
49+
task_entity = task_info["entity_logical_name"]
50+
task_entity_set = task_info["entity_set_name"]
51+
print(f"Created Task table: {task_entity} (Set: {task_entity_set})")
52+
53+
# 3. Create a lookup field from Task to Project
54+
print("Creating lookup relationship...")
55+
relationship_info = client.create_lookup_field(
56+
table_name=task_entity,
57+
field_name="project",
58+
target_table=project_entity,
59+
display_name="Project",
60+
description="The project this task belongs to",
61+
required_level="Recommended", # Recommended but not required
62+
cascade_delete="Cascade" # Delete tasks when project is deleted
63+
)
64+
65+
print(f"Created relationship: {relationship_info['relationship_name']}")
66+
print(f"Lookup field created: {relationship_info['lookup_field']}")
67+
68+
# 4. Create a project record
69+
project_data = {
70+
"new_name": "Website Redesign",
71+
"new_description": "Complete overhaul of company website",
72+
"new_start_date": datetime.now().isoformat(),
73+
"new_end_date": datetime(2023, 12, 31).isoformat(),
74+
"new_budget": 25000.00
75+
}
76+
77+
print("Creating project record...")
78+
project_record = client.create(project_entity_set, project_data)
79+
project_id = project_record["new_projectid"]
80+
print(f"Created project with ID: {project_id}")
81+
82+
# 5. Create a task linked to the project
83+
# The lookup field name follows the pattern: new_project_id
84+
lookup_field_name = relationship_info["lookup_field"].lower() + "id"
85+
86+
task_data = {
87+
"new_title": "Design homepage mockup",
88+
"new_description": "Create initial design mockups for homepage",
89+
"new_status": "Not Started",
90+
"new_due_date": datetime(2023, 10, 15).isoformat(),
91+
"new_estimated_hours": 16.5,
92+
# Add the lookup reference
93+
lookup_field_name: project_id
94+
}
95+
96+
print("Creating task record with project reference...")
97+
task_record = client.create(task_entity_set, task_data)
98+
task_id = task_record["new_taskid"]
99+
print(f"Created task with ID: {task_id}")
100+
101+
# 6. Fetch the task with project reference
102+
print("Fetching task with project information...")
103+
expand_field = relationship_info["lookup_field"].lower() + "_expand"
104+
105+
# Use the OData $expand syntax to retrieve the related project
106+
odata_client = client._get_odata()
107+
task_with_project = odata_client.get(task_entity_set, task_id, expand=[expand_field])
108+
109+
# Display the relationship information
110+
print("\nTask details:")
111+
print(f"Title: {task_with_project['new_title']}")
112+
print(f"Status: {task_with_project['new_status']}")
113+
114+
project_ref = task_with_project.get(expand_field)
115+
if project_ref:
116+
print("\nLinked Project:")
117+
print(f"Name: {project_ref['new_name']}")
118+
print(f"Budget: ${project_ref['new_budget']}")
119+
120+
print("\nRelationship successfully created and verified!")
121+
122+
if __name__ == "__main__":
123+
main()

src/dataverse_sdk/client.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,32 @@ def get_table_info(self, tablename: str) -> Optional[Dict[str, Any]]:
198198
"""
199199
return self._get_odata().get_table_info(tablename)
200200

201-
def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]:
201+
def create_table(self, tablename: str, schema: Dict[str, Union[str, Dict[str, Any]]]) -> Dict[str, Any]:
202202
"""Create a simple custom table.
203203
204204
Parameters
205205
----------
206206
tablename : str
207207
Friendly name (``"SampleItem"``) or a full schema name (``"new_SampleItem"``).
208-
schema : dict[str, str]
209-
Column definitions mapping logical names (without prefix) to types.
210-
Supported: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
208+
schema : dict[str, str | dict]
209+
Column definitions mapping logical names to types or lookup configurations.
210+
211+
For standard columns, use string type names:
212+
``"name": "string", "count": "int", "price": "decimal"``
213+
214+
Supported types: ``string``, ``int``, ``decimal``, ``float``, ``datetime``, ``bool``.
215+
216+
For lookup fields, use a dictionary with configuration options:
217+
``"project": {"lookup": "new_project", "display_name": "Project", "cascade_delete": "Cascade"}``
218+
219+
Lookup field options:
220+
- ``lookup``: Target table (required)
221+
- ``display_name``: Display name for the field (optional)
222+
- ``description``: Description for the field (optional)
223+
- ``required_level``: "None", "Recommended", or "ApplicationRequired" (default: "None")
224+
- ``relationship_name``: Custom name for the relationship (optional)
225+
- ``relationship_behavior``: "UseLabel", "UseCollectionName", "DoNotDisplay" (default: "UseLabel")
226+
- ``cascade_delete``: "Cascade", "RemoveLink", "Restrict" (default: "RemoveLink")
211227
212228
Returns
213229
-------
@@ -217,6 +233,59 @@ def create_table(self, tablename: str, schema: Dict[str, str]) -> Dict[str, Any]
217233
"""
218234
return self._get_odata().create_table(tablename, schema)
219235

236+
def create_lookup_field(
237+
self,
238+
table_name: str,
239+
field_name: str,
240+
target_table: str,
241+
display_name: Optional[str] = None,
242+
description: Optional[str] = None,
243+
required_level: str = "None",
244+
relationship_name: Optional[str] = None,
245+
relationship_behavior: str = "UseLabel",
246+
cascade_delete: str = "RemoveLink",
247+
) -> Dict[str, Any]:
248+
"""Create a lookup field (n:1 relationship) between two tables.
249+
250+
Parameters
251+
----------
252+
table_name : str
253+
The table where the lookup field will be created.
254+
field_name : str
255+
The name of the lookup field to create.
256+
target_table : str
257+
The table the lookup will reference.
258+
display_name : str, optional
259+
The display name for the lookup field. If not provided, will use target table name.
260+
description : str, optional
261+
The description for the lookup field.
262+
required_level : str, optional
263+
The requirement level: "None", "Recommended", or "ApplicationRequired".
264+
relationship_name : str, optional
265+
The name of the relationship. If not provided, one will be generated.
266+
relationship_behavior : str, optional
267+
The relationship menu behavior: "UseLabel", "UseCollectionName", "DoNotDisplay".
268+
cascade_delete : str, optional
269+
The cascade behavior on delete: "Cascade", "RemoveLink", "Restrict".
270+
271+
Returns
272+
-------
273+
dict
274+
Details about the created relationship including relationship_id, relationship_name,
275+
lookup_field, referenced_entity, and referencing_entity.
276+
"""
277+
return self._get_odata().create_lookup_field(
278+
table_name,
279+
field_name,
280+
target_table,
281+
display_name,
282+
description,
283+
required_level,
284+
relationship_name,
285+
relationship_behavior,
286+
cascade_delete
287+
)
288+
220289
def delete_table(self, tablename: str) -> None:
221290
"""Delete a custom table by name.
222291

0 commit comments

Comments
 (0)