diff --git a/examples/basic/functional_testing.py b/examples/basic/functional_testing.py index ddebd362..e482f4a1 100644 --- a/examples/basic/functional_testing.py +++ b/examples/basic/functional_testing.py @@ -27,6 +27,7 @@ import sys import time +from enum import Enum from typing import Optional, Dict, Any from datetime import datetime @@ -909,7 +910,12 @@ def test_batch_all_operations(client: DataverseClient, table_info: Dict[str, Any pass -def cleanup_test_data(client: DataverseClient, table_info: Dict[str, Any], record_id: str) -> None: +def cleanup_test_data( + client: DataverseClient, + table_info: Dict[str, Any], + record_id: str, + picklist_table_schema_name: Optional[str] = None, +) -> None: """Clean up test data.""" print("\n-> Cleanup") print("=" * 50) @@ -979,6 +985,46 @@ def cleanup_test_data(client: DataverseClient, table_info: Dict[str, Any], recor else: print("Test table kept for future testing") + # --- Picklist test table cleanup --- + if picklist_table_schema_name: + picklist_cleanup = ( + input(f"Do you want to delete the picklist test table '{picklist_table_schema_name}'? (y/N): ") + .strip() + .lower() + ) + if picklist_cleanup in ["y", "yes"]: + for attempt in range(1, retries + 1): + try: + client.tables.delete(picklist_table_schema_name) + print(f"[OK] Picklist test table '{picklist_table_schema_name}' deleted successfully") + break + except HttpError as err: + status = getattr(err, "status_code", None) + if status == 404: + if _table_still_exists(client, picklist_table_schema_name): + if attempt < retries: + print( + f" Picklist table delete retry {attempt}/{retries} after metadata 404 ({err}). Waiting {delay_seconds}s..." + ) + time.sleep(delay_seconds) + continue + print(f"[WARN] Failed to delete picklist test table due to metadata delay: {err}") + break + print("[OK] Picklist test table deleted successfully (404 reported).") + break + if attempt < retries: + print( + f" Picklist table delete retry {attempt}/{retries} after error ({err}). Waiting {delay_seconds}s..." + ) + time.sleep(delay_seconds) + continue + print(f"[WARN] Failed to delete picklist test table: {err}") + except Exception as e: + print(f"[WARN] Failed to delete picklist test table: {e}") + break + else: + print("Picklist test table kept for future testing") + def backoff(op, *, delays=(0, 2, 5, 10, 20, 20)): """Retry helper with exponential backoff for metadata propagation delays.""" @@ -1260,6 +1306,118 @@ def _get_or_create(schema, columns, label): print(f" [WARN] Could not delete {tbl}: {e}") +def test_picklist_table(client: DataverseClient) -> str: + """Create a table with a local picklist column and write/read records. + + Demonstrates: + - Defining a local OptionSet via an ``Enum`` subclass passed as the column ``dtype``. + - Optional multi-language labels via the ``__labels__`` class attribute. + - Writing records using either the enum member's integer value OR its label. + - Reading the integer value back, and the formatted label via + ``include_annotations="OData.Community.Display.V1.FormattedValue"``. + + Returns the schema name of the table so the caller can clean it up later. + """ + print("\n-> Picklist Column Test") + print("=" * 50) + + table_schema_name = "test_PicklistAttribute" + + # Define a local option set as an Enum. Optional __labels__ provides + # display labels per language code (1033 = English, 1036 = French). + class TaskStatus(Enum): + NotStarted = 1 + InProgress = 2 + Completed = 3 + Cancelled = 4 + + __labels__ = { + 1033: { + "NotStarted": "Not Started", + "InProgress": "In Progress", + "Completed": "Completed", + "Cancelled": "Cancelled", + }, + 1036: { + "NotStarted": "Non commencé", + "InProgress": "En cours", + "Completed": "Terminé", + "Cancelled": "Annulé", + }, + } + + record_id: Optional[str] = None + try: + # Drop any leftover table from a prior failed run so this example is idempotent. + try: + existing = client.tables.get(table_schema_name) + if existing: + print(f" Removing leftover '{table_schema_name}' from a previous run...") + client.tables.delete(table_schema_name) + except Exception: + pass + + print(f"Creating table '{table_schema_name}' with a picklist column 'test_status'...") + + client.tables.create( + table_schema_name, + primary_column="test_name", + columns={ + "test_status": TaskStatus, # Enum subclass => local picklist + "test_notes": "string", + }, + ) + + table_info = wait_for_table_metadata(client, table_schema_name) + print(f"[OK] Picklist table ready: entity_set='{table_info.get('entity_set_name')}'") + + # --- Insert one record using the enum's integer value --- + rec_by_int = { + "test_name": f"Picklist Int {datetime.now().strftime('%H:%M:%S')}", + "test_status": TaskStatus.InProgress.value, # integer 2 + "test_notes": "Created using TaskStatus.InProgress.value", + } + record_id = client.records.create(table_schema_name, rec_by_int) + print(f"[OK] Created record by int value: {record_id} (status={TaskStatus.InProgress.value})") + + # --- Insert another record using the picklist label (SDK resolves label -> int) --- + rec_by_label = { + "test_name": f"Picklist Label {datetime.now().strftime('%H:%M:%S')}", + "test_status": "Completed", # resolved via label cache to int 3 + "test_notes": "Created using label string 'Completed'", + } + record_id_2 = client.records.create(table_schema_name, rec_by_label) + print(f"[OK] Created record by label: {record_id_2} (status='Completed' -> 3)") + + # --- Read back including the FormattedValue annotation --- + annotation = "OData.Community.Display.V1.FormattedValue" + retrieved = client.records.retrieve( + table_schema_name, + record_id, + select=["test_name", "test_status"], + include_annotations=annotation, + ) + status_int = retrieved.get("test_status") + status_label = retrieved.get(f"test_status@{annotation}") + print(f" Retrieved: test_status={status_int}, formatted='{status_label}'") + assert status_int == TaskStatus.InProgress.value, f"expected {TaskStatus.InProgress.value}, got {status_int}" + + # --- List records, filtering by the picklist column --- + completed = client.records.list( + table_schema_name, + select=["test_name", "test_status"], + filter=f"test_status eq {TaskStatus.Completed.value}", + include_annotations=annotation, + ) + print(f"[OK] Query by picklist value found {len(completed)} 'Completed' record(s).") + + except HttpError as e: + print(f"[ERR] HTTP error during picklist test: {e}") + raise + + return table_schema_name + + def _table_still_exists(client: DataverseClient, table_schema_name: Optional[str]) -> bool: if not table_schema_name: return False @@ -1304,6 +1462,9 @@ def main(): # Test querying test_query_records(client, table_info) + # Test picklist (local OptionSet) column creation, write, read + picklist_table_info = test_picklist_table(client) + # Test relationships test_relationships(client) @@ -1321,13 +1482,14 @@ def main(): print("[OK] Record Creation: Success") print("[OK] Record Reading: Success") print("[OK] Record Querying: Success") + print("[OK] Picklist Column: Success") print("[OK] Relationship Operations: Success") print("[OK] SQL Encoding: Success") print("[OK] Batch Operations: Success") print("\nYour PowerPlatform Dataverse Client SDK is fully functional!") # Cleanup - cleanup_test_data(client, table_info, record_id) + cleanup_test_data(client, table_info, record_id, picklist_table_info) except KeyboardInterrupt: print("\n\n[WARN] Test interrupted by user") diff --git a/src/PowerPlatform/Dataverse/data/_odata.py b/src/PowerPlatform/Dataverse/data/_odata.py index 2264ccf9..bc92b973 100644 --- a/src/PowerPlatform/Dataverse/data/_odata.py +++ b/src/PowerPlatform/Dataverse/data/_odata.py @@ -816,18 +816,22 @@ def _create_entity( attributes: List[Dict[str, Any]], solution_unique_name: Optional[str] = None, ) -> Dict[str, Any]: - url = f"{self.api}/EntityDefinitions" + url = f"{self.api}/CreateEntities" payload = { - "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata", - "SchemaName": table_schema_name, - "DisplayName": self._label(display_name), - "DisplayCollectionName": self._label(display_name + "s"), - "Description": self._label(f"Custom entity for {display_name}"), - "OwnershipType": "UserOwned", - "HasActivities": False, - "HasNotes": True, - "IsActivity": False, - "Attributes": attributes, + "Entities": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.ComplexEntityMetadata", + "SchemaName": table_schema_name, + "DisplayName": self._label(display_name), + "DisplayCollectionName": self._label(display_name + "s"), + "Description": self._label(f"Custom entity for {display_name}"), + "OwnershipType": "UserOwned", + "HasActivities": False, + "HasNotes": True, + "IsActivity": False, + "Attributes": attributes, + } + ] } params = None if solution_unique_name: @@ -1298,9 +1302,9 @@ def _create_table( ) attributes: List[Dict[str, Any]] = [] - attributes.append(self._attribute_payload(primary_attr_schema, "string", is_primary_name=True)) + attributes.append(self._attribute_payload(primary_attr_schema, "string", is_primary_name=True, complex=True)) for col_name, dtype in schema.items(): - payload = self._attribute_payload(col_name, dtype) + payload = self._attribute_payload(col_name, dtype, complex=True) if not payload: raise ValueError(f"Unsupported column type '{dtype}' for '{col_name}'.") attributes.append(payload) diff --git a/src/PowerPlatform/Dataverse/data/_odata_base.py b/src/PowerPlatform/Dataverse/data/_odata_base.py index 34ff7c55..700c5a42 100644 --- a/src/PowerPlatform/Dataverse/data/_odata_base.py +++ b/src/PowerPlatform/Dataverse/data/_odata_base.py @@ -330,7 +330,12 @@ def _build_localizedlabels_payload(self, translations: Dict[int, str]) -> Dict[s } def _enum_optionset_payload( - self, column_schema_name: str, enum_cls: type[Enum], is_primary_name: bool = False + self, + column_schema_name: str, + enum_cls: type[Enum], + is_primary_name: bool = False, + *, + complex: bool = False, ) -> Dict[str, Any]: """Create local (IsGlobal=False) PicklistAttributeMetadata from an Enum subclass. @@ -341,7 +346,14 @@ def _enum_optionset_payload( Keys inside per-language dict may be either enum member objects or their names. If a language lacks a label for a member, member.name is used as fallback. The client's configured language code is always ensured to exist. + + :param complex: When ``True``, emit ``ComplexPicklistAttributeMetadata`` / + ``ComplexOptionSetMetadata`` types required by the ``CreateEntities`` + action. When ``False`` (default), emit the standard types used by the + ``EntityDefinitions/{id}/Attributes`` endpoint. """ + prefix = "Microsoft.Dynamics.CRM.Complex" if complex else "Microsoft.Dynamics.CRM." + all_member_items = list(enum_cls.__members__.items()) if not all_member_items: raise ValueError(f"Enum {enum_cls.__name__} has no members") @@ -402,6 +414,8 @@ def _enum_optionset_payload( per_lang[lang] = label_text options.append( { + # OptionMetadata has no "Complex" variant in the EDM model - + # only the outer PicklistAttributeMetadata/OptionSetMetadata do. "@odata.type": "Microsoft.Dynamics.CRM.OptionMetadata", "Value": m.value, "Label": self._build_localizedlabels_payload(per_lang), @@ -410,33 +424,48 @@ def _enum_optionset_payload( attr_label = column_schema_name.split("_")[-1] return { - "@odata.type": "Microsoft.Dynamics.CRM.PicklistAttributeMetadata", + "@odata.type": f"{prefix}PicklistAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(attr_label), "RequiredLevel": {"Value": "None"}, "IsPrimaryName": bool(is_primary_name), "OptionSet": { - "@odata.type": "Microsoft.Dynamics.CRM.OptionSetMetadata", + "@odata.type": f"{prefix}OptionSetMetadata", "IsGlobal": False, "Options": options, }, } def _attribute_payload( - self, column_schema_name: str, dtype: Any, *, is_primary_name: bool = False + self, + column_schema_name: str, + dtype: Any, + *, + is_primary_name: bool = False, + complex: bool = False, ) -> Optional[Dict[str, Any]]: + """Build attribute metadata payload for a column. + + :param complex: When ``True``, emit ``Complex*AttributeMetadata`` types + required by the ``CreateEntities`` action. When ``False`` (default), + emit the standard ``*AttributeMetadata`` types used by the + ``EntityDefinitions/{id}/Attributes`` endpoint. + """ # Enum-based local option set support if isinstance(dtype, type) and issubclass(dtype, Enum): - return self._enum_optionset_payload(column_schema_name, dtype, is_primary_name=is_primary_name) + return self._enum_optionset_payload( + column_schema_name, dtype, is_primary_name=is_primary_name, complex=complex + ) if not isinstance(dtype, str): raise ValueError( f"Unsupported column spec type for '{column_schema_name}': {type(dtype)} (expected str or Enum subclass)" ) dtype_l = dtype.lower().strip() label = column_schema_name.split("_")[-1] + prefix = "Microsoft.Dynamics.CRM.Complex" if complex else "Microsoft.Dynamics.CRM." if dtype_l in ("string", "text"): return { - "@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata", + "@odata.type": f"{prefix}StringAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -446,7 +475,7 @@ def _attribute_payload( } if dtype_l in ("memo", "multiline"): return { - "@odata.type": "Microsoft.Dynamics.CRM.MemoAttributeMetadata", + "@odata.type": f"{prefix}MemoAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -456,7 +485,7 @@ def _attribute_payload( } if dtype_l in ("int", "integer"): return { - "@odata.type": "Microsoft.Dynamics.CRM.IntegerAttributeMetadata", + "@odata.type": f"{prefix}IntegerAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -466,7 +495,7 @@ def _attribute_payload( } if dtype_l in ("decimal", "money"): return { - "@odata.type": "Microsoft.Dynamics.CRM.DecimalAttributeMetadata", + "@odata.type": f"{prefix}DecimalAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -476,7 +505,7 @@ def _attribute_payload( } if dtype_l in ("float", "double"): return { - "@odata.type": "Microsoft.Dynamics.CRM.DoubleAttributeMetadata", + "@odata.type": f"{prefix}DoubleAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -486,7 +515,7 @@ def _attribute_payload( } if dtype_l in ("datetime", "date"): return { - "@odata.type": "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata", + "@odata.type": f"{prefix}DateTimeAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -495,12 +524,12 @@ def _attribute_payload( } if dtype_l in ("bool", "boolean"): return { - "@odata.type": "Microsoft.Dynamics.CRM.BooleanAttributeMetadata", + "@odata.type": f"{prefix}BooleanAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, "OptionSet": { - "@odata.type": "Microsoft.Dynamics.CRM.BooleanOptionSetMetadata", + "@odata.type": f"{prefix}BooleanOptionSetMetadata", "TrueOption": { "Value": 1, "Label": self._label("True"), @@ -514,7 +543,7 @@ def _attribute_payload( } if dtype_l == "file": return { - "@odata.type": "Microsoft.Dynamics.CRM.FileAttributeMetadata", + "@odata.type": f"{prefix}FileAttributeMetadata", "SchemaName": column_schema_name, "DisplayName": self._label(label), "RequiredLevel": {"Value": "None"}, @@ -533,14 +562,14 @@ def _build_create_entity( primary_column: Optional[str] = None, display_name: Optional[str] = None, ) -> _RawRequest: - """Build an EntityDefinitions POST request without sending it.""" + """Build an CreateEntities POST request without sending it.""" if primary_column: primary_attr = primary_column else: primary_attr = f"{table.split('_', 1)[0]}_Name" if "_" in table else "new_Name" - attributes = [self._attribute_payload(primary_attr, "string", is_primary_name=True)] + attributes = [self._attribute_payload(primary_attr, "string", is_primary_name=True, complex=True)] for col_name, dtype in columns.items(): - attr = self._attribute_payload(col_name, dtype) + attr = self._attribute_payload(col_name, dtype, complex=True) if not attr: raise ValidationError( f"Unsupported column type '{dtype}' for column '{col_name}'.", @@ -552,18 +581,22 @@ def _build_create_entity( raise TypeError("display_name must be a non-empty string when provided") label = display_name if display_name is not None else table body = { - "@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata", - "SchemaName": table, - "DisplayName": self._label(label), - "DisplayCollectionName": self._label(label + "s"), - "Description": self._label(f"Custom entity for {label}"), - "OwnershipType": "UserOwned", - "HasActivities": False, - "HasNotes": True, - "IsActivity": False, - "Attributes": attributes, + "Entities": [ + { + "@odata.type": "Microsoft.Dynamics.CRM.ComplexEntityMetadata", + "SchemaName": table, + "DisplayName": self._label(label), + "DisplayCollectionName": self._label(label + "s"), + "Description": self._label(f"Custom entity for {label}"), + "OwnershipType": "UserOwned", + "HasActivities": False, + "HasNotes": True, + "IsActivity": False, + "Attributes": attributes, + } + ] } - url = f"{self.api}/EntityDefinitions" + url = f"{self.api}/CreateEntities" if solution: url += f"?SolutionUniqueName={solution}" return _RawRequest( diff --git a/tests/unit/data/test_odata_internal.py b/tests/unit/data/test_odata_internal.py index f392ce20..ff738528 100644 --- a/tests/unit/data/test_odata_internal.py +++ b/tests/unit/data/test_odata_internal.py @@ -1518,6 +1518,11 @@ def test_int_dtype(self): result = self.od._attribute_payload("new_Count", "int") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.IntegerAttributeMetadata") + def test_complex_int_dtype(self): + """'int' with complex=True produces ComplexIntegerAttributeMetadata.""" + result = self.od._attribute_payload("new_Count", "int", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexIntegerAttributeMetadata") + def test_integer_dtype_alias(self): """'integer' is an alias for 'int'.""" result = self.od._attribute_payload("new_Count", "integer") @@ -1528,6 +1533,11 @@ def test_decimal_dtype(self): result = self.od._attribute_payload("new_Price", "decimal") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.DecimalAttributeMetadata") + def test_complex_decimal_dtype(self): + """'decimal' with complex=True produces ComplexDecimalAttributeMetadata.""" + result = self.od._attribute_payload("new_Price", "decimal", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexDecimalAttributeMetadata") + def test_money_dtype_alias(self): """'money' is an alias for 'decimal'.""" result = self.od._attribute_payload("new_Revenue", "money") @@ -1538,6 +1548,11 @@ def test_float_dtype(self): result = self.od._attribute_payload("new_Score", "float") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.DoubleAttributeMetadata") + def test_complex_float_dtype(self): + """'float' with complex=True produces ComplexDoubleAttributeMetadata.""" + result = self.od._attribute_payload("new_Score", "float", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexDoubleAttributeMetadata") + def test_double_dtype_alias(self): """'double' is an alias for 'float'.""" result = self.od._attribute_payload("new_Score", "double") @@ -1548,6 +1563,11 @@ def test_datetime_dtype(self): result = self.od._attribute_payload("new_CreatedDate", "datetime") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata") + def test_complex_datetime_dtype(self): + """'datetime' with complex=True produces ComplexDateTimeAttributeMetadata.""" + result = self.od._attribute_payload("new_CreatedDate", "datetime", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexDateTimeAttributeMetadata") + def test_date_dtype_alias(self): """'date' is an alias for 'datetime'.""" result = self.od._attribute_payload("new_BirthDate", "date") @@ -1558,6 +1578,11 @@ def test_bool_dtype(self): result = self.od._attribute_payload("new_IsActive", "bool") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.BooleanAttributeMetadata") + def test_complex_bool_dtype(self): + """'bool' with complex=True produces ComplexBooleanAttributeMetadata.""" + result = self.od._attribute_payload("new_IsActive", "bool", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexBooleanAttributeMetadata") + def test_boolean_dtype_alias(self): """'boolean' is an alias for 'bool'.""" result = self.od._attribute_payload("new_IsActive", "boolean") @@ -1568,6 +1593,11 @@ def test_file_dtype(self): result = self.od._attribute_payload("new_Attachment", "file") self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.FileAttributeMetadata") + def test_complex_file_dtype(self): + """'file' with complex=True produces ComplexFileAttributeMetadata.""" + result = self.od._attribute_payload("new_Attachment", "file", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexFileAttributeMetadata") + def test_non_string_dtype_raises_value_error(self): """Non-string dtype raises ValueError.""" with self.assertRaises(ValueError): @@ -1582,6 +1612,15 @@ def test_memo_type(self): self.assertEqual(result["FormatName"], {"Value": "Text"}) self.assertNotIn("IsPrimaryName", result) + def test_complex_memo_type(self): + """'memo' with complex=True produces ComplexMemoAttributeMetadata with MaxLength 4000.""" + result = self.od._attribute_payload("new_Notes", "memo", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexMemoAttributeMetadata") + self.assertEqual(result["SchemaName"], "new_Notes") + self.assertEqual(result["MaxLength"], 4000) + self.assertEqual(result["FormatName"], {"Value": "Text"}) + self.assertNotIn("IsPrimaryName", result) + def test_multiline_alias(self): """'multiline' produces identical payload to 'memo'.""" memo_result = self.od._attribute_payload("new_Description", "memo") @@ -1595,6 +1634,13 @@ def test_string_type_max_length(self): self.assertEqual(result["MaxLength"], 200) self.assertEqual(result["FormatName"], {"Value": "Text"}) + def test_complex_string_type_max_length(self): + """'string' with complex=True produces ComplexStringAttributeMetadata with MaxLength 200.""" + result = self.od._attribute_payload("new_Title", "string", complex=True) + self.assertEqual(result["@odata.type"], "Microsoft.Dynamics.CRM.ComplexStringAttributeMetadata") + self.assertEqual(result["MaxLength"], 200) + self.assertEqual(result["FormatName"], {"Value": "Text"}) + def test_unsupported_type_returns_none(self): """An unknown type string should return None.""" result = self.od._attribute_payload("new_Col", "unknown_type") @@ -1819,7 +1865,7 @@ def test_primary_column_schema_name_used_when_provided(self): self._setup_for_create() self.od._create_table("new_TestTable", {}, primary_column_schema_name="new_CustomName") post_json = self.od._request.call_args.kwargs["json"] - attrs = post_json["Attributes"] + attrs = post_json["Entities"][0]["Attributes"] primary_attr = next((a for a in attrs if a.get("IsPrimaryName")), None) self.assertIsNotNone(primary_attr) self.assertEqual(primary_attr["SchemaName"], "new_CustomName") @@ -1829,7 +1875,7 @@ def test_display_name_used_in_payload_when_provided(self): self._setup_for_create() self.od._create_table("new_TestTable", {}, display_name="My Test Table") post_json = self.od._request.call_args.kwargs["json"] - label_value = post_json["DisplayName"]["LocalizedLabels"][0]["Label"] + label_value = post_json["Entities"][0]["DisplayName"]["LocalizedLabels"][0]["Label"] self.assertEqual(label_value, "My Test Table") def test_display_name_defaults_to_schema_name(self): @@ -1837,7 +1883,7 @@ def test_display_name_defaults_to_schema_name(self): self._setup_for_create() self.od._create_table("new_TestTable", {}) post_json = self.od._request.call_args.kwargs["json"] - label_value = post_json["DisplayName"]["LocalizedLabels"][0]["Label"] + label_value = post_json["Entities"][0]["DisplayName"]["LocalizedLabels"][0]["Label"] self.assertEqual(label_value, "new_TestTable") def test_display_name_empty_string_raises(self): @@ -2855,7 +2901,7 @@ def setUp(self): def _body(self, **kwargs): req = self.od._build_create_entity("new_TestTable", {}, **kwargs) - return json.loads(req.body) + return json.loads(req.body)["Entities"][0] def test_display_name_used_in_payload_when_provided(self): """_build_create_entity uses the provided display_name in DisplayName.""" @@ -2894,10 +2940,10 @@ def test_returns_post_request(self): req = self.od._build_create_entity("new_TestTable", {}) self.assertEqual(req.method, "POST") - def test_url_targets_entity_definitions(self): - """_build_create_entity URL ends with /EntityDefinitions.""" + def test_url_targets_create_entities(self): + """_build_create_entity URL ends with /CreateEntities.""" req = self.od._build_create_entity("new_TestTable", {}) - self.assertTrue(req.url.endswith("/EntityDefinitions")) + self.assertTrue(req.url.endswith("/CreateEntities")) def test_solution_appended_to_url(self): """_build_create_entity appends SolutionUniqueName to URL when solution is given.""" @@ -2942,7 +2988,7 @@ def test_primary_column_derived_from_table_prefix(self): def test_primary_column_explicit(self): """_build_create_entity uses explicit primary_column when provided.""" req = self.od._build_create_entity("new_TestTable", {}, primary_column="new_CustomName") - body = json.loads(req.body) + body = json.loads(req.body)["Entities"][0] attrs = body["Attributes"] primary = next(a for a in attrs if a.get("IsPrimaryName")) self.assertEqual(primary["SchemaName"], "new_CustomName") @@ -2950,7 +2996,7 @@ def test_primary_column_explicit(self): def test_primary_column_derived_no_prefix(self): """Primary column defaults to 'new_Name' when table has no underscore.""" req = self.od._build_create_entity("TestTable", {}) - body = json.loads(req.body) + body = json.loads(req.body)["Entities"][0] primary = next(a for a in body["Attributes"] if a.get("IsPrimaryName")) self.assertEqual(primary["SchemaName"], "new_Name") @@ -2961,7 +3007,7 @@ def test_columns_included_in_attributes(self): body = ( self._body.__func__(self, **{}) if False - else json.loads(self.od._build_create_entity("new_TestTable", {"new_Price": "decimal"}).body) + else json.loads(self.od._build_create_entity("new_TestTable", {"new_Price": "decimal"}).body)["Entities"][0] ) schemas = [a["SchemaName"] for a in body["Attributes"]] self.assertIn("new_Price", schemas)