Skip to content

Commit 10ed519

Browse files
committed
fix: address Copilot review - reject filter+typed, document typed/dict-spec in docstrings + README
1 parent cf388d1 commit 10ed519

8 files changed

Lines changed: 86 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ df = results.to_dataframe() # same rows as a pandas DataFrame
111111

112112
### Define and evolve schema
113113

114-
`client.tables` creates and inspects tables, columns, relationships, and alternate keys. Column types are simple strings (`"string"`, `"int"`, `"decimal"`, `"money"`, `"datetime"`, `"bool"`, `"memo"`); pass an `IntEnum` subclass to create a choice column. `tables.get` returns `None` when the table does not exist, which makes schema setup idempotent. Every table gets a primary name column automatically — `<prefix>_Name` unless you pass `primary_column` — so do not list it in `columns`.
114+
`client.tables` creates and inspects tables, columns, relationships, and alternate keys. Column types are simple strings (`"string"`, `"int"`, `"decimal"`, `"money"`, `"datetime"`, `"bool"`, `"memo"`); pass an `IntEnum` subclass to create a choice column, or a dict spec (e.g. `{"type": "memo", "max_length": 2000}`) to set constraints like length, range, format, required level, or display name. `tables.get` returns `None` when the table does not exist, which makes schema setup idempotent. Every table gets a primary name column automatically — `<prefix>_Name` unless you pass `primary_column` — so do not list it in `columns`.
115115

116116
```python
117117
# Create a custom table with typed columns
@@ -136,6 +136,30 @@ client.tables.create_lookup_field(
136136
)
137137
```
138138

139+
Columns can carry constraints, be updated in place, and be read back with their
140+
type-specific metadata in a single request:
141+
142+
```python
143+
# Create columns with constraints -- pass a dict spec instead of a bare type.
144+
# Keys: max_length, min_value, max_value, precision, format, required, display_name.
145+
client.tables.create("new_Feedback", {
146+
"new_Rating": {"type": "int", "min_value": 1, "max_value": 5},
147+
"new_Comment": {"type": "memo", "max_length": 2000, "display_name": "Comment"},
148+
})
149+
150+
# Update an existing column's constraints (same override keys as create).
151+
client.tables.update_column("new_Feedback", "new_Comment", {"max_length": 4000})
152+
client.tables.update_columns("new_Feedback", {
153+
"new_Rating": {"max_value": 10},
154+
"new_Comment": {"display_name": "Customer Comment"},
155+
})
156+
157+
# Read a column (or all columns) with typed fields in one request (typed=True).
158+
col = client.tables.get_column("new_Feedback", "new_Comment", typed=True)
159+
print(col["MaxLength"]) # -> 4000
160+
cols = client.tables.list_columns("new_Feedback", typed=True)
161+
```
162+
139163
Relationship methods take logical names, which are always the schema name in lowercase. For choice columns, many-to-many relationships, and alternate keys, see [Customize tables and columns](https://learn.microsoft.com/power-apps/developer/data-platform/sdk-python/metadata) and [Manage table relationships](https://learn.microsoft.com/power-apps/developer/data-platform/sdk-python/relationships).
140164

141165
### Work in bulk

‎src/PowerPlatform/Dataverse/aio/data/_async_odata.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -945,6 +945,12 @@ async def _list_columns(
945945
:raises HttpError: If the Web API request fails.
946946
"""
947947
if typed:
948+
if filter:
949+
raise ValueError(
950+
"The 'filter' argument is not supported with typed=True: RetrieveMetadataChanges "
951+
"projects typed fields but does not accept an OData $filter. Use typed=False to "
952+
"filter, or drop filter when typed=True."
953+
)
948954
return await self._retrieve_metadata_changes(table_schema_name, properties=select)
949955
ent = await self._get_entity_by_table_schema_name(table_schema_name)
950956
if not ent or not ent.get("MetadataId"):

‎src/PowerPlatform/Dataverse/aio/operations/async_tables.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,9 +721,15 @@ async def list_columns(
721721
:param select: Optional list of property names to project via
722722
``$select``. Values are passed as-is (PascalCase).
723723
:type select: list[str] or None
724-
:param filter: Optional OData ``$filter`` expression. For example,
725-
``"AttributeType eq 'String'"`` returns only string columns.
724+
:param filter: Optional OData ``$filter`` expression (only when
725+
``typed=False``). For example, ``"AttributeType eq 'String'"``
726+
returns only string columns.
726727
:type filter: :class:`str` or None
728+
:param typed: When ``True``, read via ``RetrieveMetadataChanges`` so
729+
type-specific fields (``MaxLength``, ``MinValue``/``MaxValue``, ...)
730+
come back in one request without the ``@odata.type`` URL cast.
731+
``filter`` is not supported in this mode.
732+
:type typed: :class:`bool`
727733
728734
:return: List of raw attribute metadata dictionaries.
729735
:rtype: list[dict[str, typing.Any]]
@@ -732,6 +738,8 @@ async def list_columns(
732738
If the table is not found.
733739
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
734740
If the Web API request fails.
741+
:raises ValueError:
742+
If ``filter`` is combined with ``typed=True``.
735743
736744
Example::
737745

‎src/PowerPlatform/Dataverse/data/_odata.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,12 @@ def _list_columns(
924924
:raises HttpError: If the Web API request fails.
925925
"""
926926
if typed:
927+
if filter:
928+
raise ValueError(
929+
"The 'filter' argument is not supported with typed=True: RetrieveMetadataChanges "
930+
"projects typed fields but does not accept an OData $filter. Use typed=False to "
931+
"filter, or drop filter when typed=True."
932+
)
927933
return self._retrieve_metadata_changes(table_schema_name, properties=select)
928934
ent = self._get_entity_by_table_schema_name(table_schema_name)
929935
if not ent or not ent.get("MetadataId"):

‎src/PowerPlatform/Dataverse/operations/batch.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,10 @@ def create(
527527
528528
:param table: Schema name of the new table (e.g. ``"new_Product"``).
529529
:type table: :class:`str`
530-
:param columns: Mapping of column schema names to type strings or Enum subclasses.
530+
:param columns: Mapping of column schema name to a type string, an
531+
:class:`~enum.Enum` subclass, or a dict constraint spec (e.g.
532+
``{"type": "memo", "max_length": 2000}``) -- the same shape accepted
533+
by :meth:`PowerPlatform.Dataverse.operations.tables.TableOperations.create`.
531534
:type columns: dict[str, typing.Any]
532535
:param solution: Optional solution unique name.
533536
:type solution: str or None
@@ -601,7 +604,10 @@ def add_columns(self, table: str, columns: Dict[str, Any]) -> None:
601604
602605
:param table: Schema name of the target table.
603606
:type table: :class:`str`
604-
:param columns: Mapping of column schema names to type strings or Enum subclasses.
607+
:param columns: Mapping of column schema name to a type string, an
608+
:class:`~enum.Enum` subclass, or a dict constraint spec (e.g.
609+
``{"type": "memo", "max_length": 2000}``) -- the same shape accepted
610+
by :meth:`PowerPlatform.Dataverse.operations.tables.TableOperations.add_columns`.
605611
:type columns: dict[str, typing.Any]
606612
"""
607613
self._batch._items.append(_TableAddColumns(table=table, columns=columns))

‎src/PowerPlatform/Dataverse/operations/tables.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -722,9 +722,15 @@ def list_columns(
722722
:param select: Optional list of property names to project via
723723
``$select``. Values are passed as-is (PascalCase).
724724
:type select: list[str] or None
725-
:param filter: Optional OData ``$filter`` expression. For example,
726-
``"AttributeType eq 'String'"`` returns only string columns.
725+
:param filter: Optional OData ``$filter`` expression (only when
726+
``typed=False``). For example, ``"AttributeType eq 'String'"``
727+
returns only string columns.
727728
:type filter: :class:`str` or None
729+
:param typed: When ``True``, read via ``RetrieveMetadataChanges`` so
730+
type-specific fields (``MaxLength``, ``MinValue``/``MaxValue``, ...)
731+
come back in one request without the ``@odata.type`` URL cast.
732+
``filter`` is not supported in this mode.
733+
:type typed: :class:`bool`
728734
729735
:return: List of raw attribute metadata dictionaries.
730736
:rtype: list[dict[str, typing.Any]]
@@ -733,6 +739,8 @@ def list_columns(
733739
If the table is not found.
734740
:raises ~PowerPlatform.Dataverse.core.errors.HttpError:
735741
If the Web API request fails.
742+
:raises ValueError:
743+
If ``filter`` is combined with ``typed=True``.
736744
737745
Example::
738746

‎tests/unit/aio/data/test_async_odata_internal.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,6 +1946,12 @@ async def test_get_column_typed_empty_returns_none(self):
19461946
client._retrieve_metadata_changes = AsyncMock(return_value=[])
19471947
assert await client._get_column("account", "ghost", typed=True) is None
19481948

1949+
async def test_list_columns_typed_with_filter_raises(self):
1950+
"""filter + typed=True is rejected (not silently dropped)."""
1951+
client = _make_client()
1952+
with pytest.raises(ValueError):
1953+
await client._list_columns("account", typed=True, filter="AttributeType eq 'String'")
1954+
19491955

19501956
class TestAsyncGetColumn:
19511957
"""_AsyncODataClient._get_column non-typed paths (#203)."""

‎tests/unit/data/test_odata_internal.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3305,6 +3305,21 @@ def test_get_column_typed_returns_single_attr(self):
33053305
self.assertEqual(col["LogicalName"], "emailaddress1")
33063306
self.assertEqual(col["MaxLength"], 100)
33073307

3308+
def test_list_columns_typed_with_filter_raises(self):
3309+
"""filter + typed=True is rejected (not silently dropped)."""
3310+
with self.assertRaises(ValueError):
3311+
self.od._list_columns("account", typed=True, filter="AttributeType eq 'String'")
3312+
3313+
3314+
class TestBuildCreateColumnDictSpec(unittest.TestCase):
3315+
"""Batch create/add-columns reaches dict constraint specs via the shared builder."""
3316+
3317+
def test_build_create_column_accepts_dict_spec(self):
3318+
od = _make_odata_client()
3319+
req = od._build_create_column("meta-1", "new_Comment", {"type": "memo", "max_length": 2000})
3320+
self.assertEqual(req.method, "POST")
3321+
self.assertEqual(json.loads(req.body)["MaxLength"], 2000)
3322+
33083323

33093324
if __name__ == "__main__":
33103325
unittest.main()

0 commit comments

Comments
 (0)