Skip to content

Commit 639f0dd

Browse files
author
Saurabh Badenkal
committed
Expose primary_name_attribute in TableInfo, fix prodev example E2E
SDK changes: - _get_entity_by_table_schema_name now selects PrimaryNameAttribute and PrimaryIdAttribute from EntityDefinitions - _create_table returns primary_name_attribute in its result dict - TableInfo model includes primary_name_attribute and primary_id_attribute fields with legacy key access - Fixes #148: tables.create() now exposes the primary column logical name Example changes: - prodev_quick_start.py uses result.primary_name_attribute from tables.create() to construct correct record payloads (fixes the demo_*_name column error) - Both examples verified E2E against live Dataverse
1 parent 860d10b commit 639f0dd

5 files changed

Lines changed: 76 additions & 19 deletions

File tree

examples/advanced/datascience_risk_assessment.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@
2020
4) Write risk assessments back to Dataverse
2121
5) Produce a summary report
2222
23+
Note: This example reads from existing Dataverse tables (account,
24+
incident, opportunity) and does not create or delete any tables.
25+
Step 4 (write-back) is disabled by default -- uncomment it in
26+
run_risk_pipeline() to write risk scores back to account records.
27+
2328
Prerequisites (required -- included in SDK dependencies):
2429
pip install PowerPlatform-Dataverse-Client
2530
pip install azure-identity

examples/advanced/prodev_quick_start.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
5) Query and join data across tables
1717
6) Clean up (delete tables)
1818
19+
Note: The last step (cleanup) automatically deletes all demo tables.
20+
Comment out the cleanup() call in run_demo() if you want to keep the
21+
tables in your environment for inspection.
22+
1923
Why pandas DataFrames?
2024
This example uses client.dataframe (pandas) instead of raw dict/list CRUD
2125
because DataFrames provide significant advantages for multi-record operations:
@@ -100,19 +104,19 @@ def run_demo(client):
100104
print(f"[INFO] Output folder: {OUTPUT_DIR.resolve()}")
101105

102106
# -- Step 1: Create 4 tables --
103-
step1_create_tables(client)
107+
primary_name_col = step1_create_tables(client)
104108

105109
# -- Step 2: Create relationships --
106110
step2_create_relationships(client)
107111

108112
# -- Step 3: Populate with sample data --
109-
customer_ids, project_ids, task_ids = step3_populate_data(client)
113+
customer_ids, project_ids, task_ids = step3_populate_data(client, primary_name_col)
110114

111115
# -- Step 4: Query and analyze --
112-
step4_query_and_analyze(client, customer_ids)
116+
step4_query_and_analyze(client, customer_ids, primary_name_col)
113117

114118
# -- Step 5: Update and delete --
115-
step5_update_and_delete(client, task_ids)
119+
step5_update_and_delete(client, task_ids, primary_name_col)
116120

117121
# -- Step 6: Cleanup --
118122
cleanup(client)
@@ -134,15 +138,18 @@ def step1_create_tables(client):
134138
print("-" * 60)
135139

136140
# Customer table
137-
client.tables.create(
141+
result = client.tables.create(
138142
TABLE_CUSTOMER,
139143
{
140144
f"{TABLE_CUSTOMER}_Email": "string",
141145
f"{TABLE_CUSTOMER}_Industry": "string",
142146
f"{TABLE_CUSTOMER}_Revenue": "money",
143147
},
144148
)
145-
print(f"[OK] Created table: {TABLE_CUSTOMER}")
149+
# The primary name column logical name is returned by tables.create()
150+
# so we know exactly what key to use in create payloads.
151+
primary_name_col = result.primary_name_attribute
152+
print(f"[OK] Created table: {TABLE_CUSTOMER} (primary column: {primary_name_col})")
146153

147154
# Project table
148155
client.tables.create(
@@ -177,6 +184,9 @@ def step1_create_tables(client):
177184
)
178185
print(f"[OK] Created table: {TABLE_TIMEENTRY}")
179186
print(f"[OK] All 4 tables created with prefix '{PREFIX}'")
187+
print(f"[INFO] Primary name column: '{primary_name_col}'")
188+
189+
return primary_name_col
180190

181191

182192
# ================================================================
@@ -225,7 +235,7 @@ def step2_create_relationships(client):
225235
# ================================================================
226236

227237

228-
def step3_populate_data(client):
238+
def step3_populate_data(client, primary_name_col):
229239
"""Create sample records using client.dataframe.create().
230240
231241
Why DataFrames here instead of client.records.create()?
@@ -252,10 +262,8 @@ def step3_populate_data(client):
252262
print("-" * 60)
253263

254264
# -- Customers --
255-
# Note: The primary column logical name is {prefix}_name (lowercase),
256-
# and custom column logical names follow the same lowercase convention.
257-
# The SDK lowercases keys automatically, so we use the schema names here.
258-
name_col = f"{PREFIX}_Name"
265+
# Use the primary name column returned by tables.create()
266+
name_col = primary_name_col
259267
customers_df = pd.DataFrame(
260268
[
261269
{
@@ -364,15 +372,15 @@ def step3_populate_data(client):
364372
# ================================================================
365373

366374

367-
def step4_query_and_analyze(client, customer_ids):
375+
def step4_query_and_analyze(client, customer_ids, primary_name_col):
368376
"""Query data and demonstrate DataFrame analysis."""
369377
print("\n" + "-" * 60)
370378
print("STEP 4: Query and analyze data")
371379
print("-" * 60)
372380

373381
# Query all projects as a DataFrame
374382
# Note: select uses logical names (lowercase). The SDK lowercases automatically.
375-
name_attr = f"{PREFIX}_name" # primary column logical name
383+
name_attr = primary_name_col
376384
projects = client.dataframe.get(
377385
TABLE_PROJECT,
378386
select=[
@@ -437,7 +445,7 @@ def step4_query_and_analyze(client, customer_ids):
437445
# ================================================================
438446

439447

440-
def step5_update_and_delete(client, task_ids):
448+
def step5_update_and_delete(client, task_ids, primary_name_col):
441449
"""Demonstrate update and delete with DataFrames."""
442450
print("\n" + "-" * 60)
443451
print("STEP 5: Update and delete records")
@@ -461,10 +469,9 @@ def step5_update_and_delete(client, task_ids):
461469
print(f"[OK] Deleted 1 task")
462470

463471
# Verify
464-
name_attr = f"{PREFIX}_name"
465472
remaining = client.dataframe.get(
466473
TABLE_TASK,
467-
select=[name_attr, status_col],
474+
select=[primary_name_col, status_col],
468475
)
469476
print(f"\n Remaining tasks ({len(remaining)}):")
470477
print(f"{remaining.to_string(index=False)}")

src/PowerPlatform/Dataverse/data/_odata.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -960,7 +960,7 @@ def _get_entity_by_table_schema_name(
960960
logical_lower = table_schema_name.lower()
961961
logical_escaped = self._escape_odata_quotes(logical_lower)
962962
params = {
963-
"$select": "MetadataId,LogicalName,SchemaName,EntitySetName",
963+
"$select": "MetadataId,LogicalName,SchemaName,EntitySetName,PrimaryNameAttribute,PrimaryIdAttribute",
964964
"$filter": f"LogicalName eq '{logical_escaped}'",
965965
}
966966
r = self._request("get", url, params=params, headers=headers)
@@ -1445,6 +1445,8 @@ def _get_table_info(self, table_schema_name: str) -> Optional[Dict[str, Any]]:
14451445
"table_logical_name": ent.get("LogicalName"),
14461446
"entity_set_name": ent.get("EntitySetName"),
14471447
"metadata_id": ent.get("MetadataId"),
1448+
"primary_name_attribute": ent.get("PrimaryNameAttribute"),
1449+
"primary_id_attribute": ent.get("PrimaryIdAttribute"),
14481450
"columns_created": [],
14491451
}
14501452

@@ -1689,6 +1691,8 @@ def _create_table(
16891691
"table_logical_name": metadata.get("LogicalName"),
16901692
"entity_set_name": metadata.get("EntitySetName"),
16911693
"metadata_id": metadata.get("MetadataId"),
1694+
"primary_name_attribute": metadata.get("PrimaryNameAttribute"),
1695+
"primary_id_attribute": metadata.get("PrimaryIdAttribute"),
16921696
"columns_created": created_cols,
16931697
}
16941698

src/PowerPlatform/Dataverse/models/table_info.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ class TableInfo:
112112
logical_name: str = ""
113113
entity_set_name: str = ""
114114
metadata_id: str = ""
115+
primary_name_attribute: Optional[str] = None
116+
primary_id_attribute: Optional[str] = None
115117
display_name: Optional[str] = None
116118
description: Optional[str] = None
117119
columns: Optional[List[ColumnInfo]] = field(default=None, repr=False)
@@ -123,6 +125,8 @@ class TableInfo:
123125
"table_logical_name": "logical_name",
124126
"entity_set_name": "entity_set_name",
125127
"metadata_id": "metadata_id",
128+
"primary_name_attribute": "primary_name_attribute",
129+
"primary_id_attribute": "primary_id_attribute",
126130
"columns_created": "columns_created",
127131
}
128132

@@ -187,6 +191,8 @@ def from_dict(cls, data: Dict[str, Any]) -> TableInfo:
187191
logical_name=data.get("table_logical_name", ""),
188192
entity_set_name=data.get("entity_set_name", ""),
189193
metadata_id=data.get("metadata_id", ""),
194+
primary_name_attribute=data.get("primary_name_attribute"),
195+
primary_id_attribute=data.get("primary_id_attribute"),
190196
columns_created=data.get("columns_created"),
191197
)
192198

@@ -213,6 +219,8 @@ def from_api_response(cls, response_data: Dict[str, Any]) -> TableInfo:
213219
logical_name=response_data.get("LogicalName", ""),
214220
entity_set_name=response_data.get("EntitySetName", ""),
215221
metadata_id=response_data.get("MetadataId", ""),
222+
primary_name_attribute=response_data.get("PrimaryNameAttribute"),
223+
primary_id_attribute=response_data.get("PrimaryIdAttribute"),
216224
display_name=display_name,
217225
description=description,
218226
)

tests/unit/models/test_table_info.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,19 @@ def test_legacy_key_iteration(self):
4949
keys = list(self.info)
5050
self.assertEqual(
5151
keys,
52-
["table_schema_name", "table_logical_name", "entity_set_name", "metadata_id", "columns_created"],
52+
[
53+
"table_schema_name",
54+
"table_logical_name",
55+
"entity_set_name",
56+
"metadata_id",
57+
"primary_name_attribute",
58+
"primary_id_attribute",
59+
"columns_created",
60+
],
5361
)
5462

5563
def test_len(self):
56-
self.assertEqual(len(self.info), 5)
64+
self.assertEqual(len(self.info), 7)
5765

5866
def test_keys_values_items(self):
5967
self.assertEqual(list(self.info.keys()), list(self.info._LEGACY_KEY_MAP.keys()))
@@ -76,20 +84,41 @@ def test_from_dict(self):
7684
"table_logical_name": "new_product",
7785
"entity_set_name": "new_products",
7886
"metadata_id": "meta-guid-1",
87+
"primary_name_attribute": "new_name",
88+
"primary_id_attribute": "new_productid",
7989
"columns_created": ["new_Price"],
8090
}
8191
info = TableInfo.from_dict(data)
8292
self.assertEqual(info.schema_name, "new_Product")
8393
self.assertEqual(info.logical_name, "new_product")
8494
self.assertEqual(info.entity_set_name, "new_products")
8595
self.assertEqual(info.metadata_id, "meta-guid-1")
96+
self.assertEqual(info.primary_name_attribute, "new_name")
97+
self.assertEqual(info.primary_id_attribute, "new_productid")
8698
self.assertEqual(info.columns_created, ["new_Price"])
8799

88100
def test_from_dict_missing_keys(self):
89101
info = TableInfo.from_dict({})
90102
self.assertEqual(info.schema_name, "")
103+
self.assertIsNone(info.primary_name_attribute)
104+
self.assertIsNone(info.primary_id_attribute)
91105
self.assertIsNone(info.columns_created)
92106

107+
def test_from_dict_legacy_access_primary_fields(self):
108+
"""Primary fields are accessible via legacy dict-key access."""
109+
data = {
110+
"table_schema_name": "new_Product",
111+
"table_logical_name": "new_product",
112+
"entity_set_name": "new_products",
113+
"metadata_id": "meta-guid-1",
114+
"primary_name_attribute": "new_name",
115+
"primary_id_attribute": "new_productid",
116+
"columns_created": [],
117+
}
118+
info = TableInfo.from_dict(data)
119+
self.assertEqual(info["primary_name_attribute"], "new_name")
120+
self.assertEqual(info["primary_id_attribute"], "new_productid")
121+
93122

94123
class TestTableInfoFromApiResponse(unittest.TestCase):
95124
"""Tests for TableInfo.from_api_response factory (PascalCase keys)."""
@@ -100,6 +129,8 @@ def test_from_api_response(self):
100129
"LogicalName": "account",
101130
"EntitySetName": "accounts",
102131
"MetadataId": "meta-guid-2",
132+
"PrimaryNameAttribute": "name",
133+
"PrimaryIdAttribute": "accountid",
103134
"DisplayName": {"UserLocalizedLabel": {"Label": "Account", "LanguageCode": 1033}},
104135
"Description": {"UserLocalizedLabel": {"Label": "Business account", "LanguageCode": 1033}},
105136
}
@@ -108,6 +139,8 @@ def test_from_api_response(self):
108139
self.assertEqual(info.logical_name, "account")
109140
self.assertEqual(info.entity_set_name, "accounts")
110141
self.assertEqual(info.metadata_id, "meta-guid-2")
142+
self.assertEqual(info.primary_name_attribute, "name")
143+
self.assertEqual(info.primary_id_attribute, "accountid")
111144
self.assertEqual(info.display_name, "Account")
112145
self.assertEqual(info.description, "Business account")
113146

0 commit comments

Comments
 (0)