From b8e56d7e24ae506fffff2c564dba782ed056c363 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Tue, 24 Feb 2026 15:55:39 +0000 Subject: [PATCH 01/10] feat: add support for update report definition endpoint --- smartsheet/models/__init__.py | 7 + smartsheet/models/enums/__init__.py | 4 + .../models/enums/report_aggregation_type.py | 27 ++ .../models/enums/report_boolean_operator.py | 22 ++ .../models/enums/report_filter_operator.py | 56 +++ .../models/enums/report_system_column_type.py | 26 ++ .../models/report_aggregation_criterion.py | 80 ++++ smartsheet/models/report_column_identifier.py | 101 ++++++ smartsheet/models/report_definition.py | 98 +++++ smartsheet/models/report_filter_criterion.py | 81 +++++ smartsheet/models/report_filter_expression.py | 101 ++++++ .../models/report_grouping_criterion.py | 89 +++++ smartsheet/models/report_sorting_criterion.py | 80 ++++ smartsheet/reports.py | 34 +- .../reports/test_update_report_definition.py | 343 ++++++++++++++++++ 15 files changed, 1148 insertions(+), 1 deletion(-) create mode 100644 smartsheet/models/enums/report_aggregation_type.py create mode 100644 smartsheet/models/enums/report_boolean_operator.py create mode 100644 smartsheet/models/enums/report_filter_operator.py create mode 100644 smartsheet/models/enums/report_system_column_type.py create mode 100644 smartsheet/models/report_aggregation_criterion.py create mode 100644 smartsheet/models/report_column_identifier.py create mode 100644 smartsheet/models/report_definition.py create mode 100644 smartsheet/models/report_filter_criterion.py create mode 100644 smartsheet/models/report_filter_expression.py create mode 100644 smartsheet/models/report_grouping_criterion.py create mode 100644 smartsheet/models/report_sorting_criterion.py create mode 100644 tests/mock_api/reports/test_update_report_definition.py diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index 2df18ec2..db7c672d 100644 --- a/smartsheet/models/__init__.py +++ b/smartsheet/models/__init__.py @@ -80,10 +80,17 @@ from .project_settings import ProjectSettings from .recipient import Recipient from .report import Report +from .report_aggregation_criterion import ReportAggregationCriterion from .report_cell import ReportCell from .report_column import ReportColumn +from .report_column_identifier import ReportColumnIdentifier +from .report_definition import ReportDefinition +from .report_filter_criterion import ReportFilterCriterion +from .report_filter_expression import ReportFilterExpression +from .report_grouping_criterion import ReportGroupingCriterion from .report_publish import ReportPublish from .report_row import ReportRow +from .report_sorting_criterion import ReportSortingCriterion from .result import Result from .row import Row from .row_email import RowEmail diff --git a/smartsheet/models/enums/__init__.py b/smartsheet/models/enums/__init__.py index ec5e4b92..b842c2ec 100644 --- a/smartsheet/models/enums/__init__.py +++ b/smartsheet/models/enums/__init__.py @@ -41,6 +41,10 @@ from .paper_type import PaperType from .predecessor_type import PredecessorType from .publish_accessible_by import PublishAccessibleBy +from .report_aggregation_type import ReportAggregationType +from .report_boolean_operator import ReportBooleanOperator +from .report_filter_operator import ReportFilterOperator +from .report_system_column_type import ReportSystemColumnType from .schedule_type import ScheduleType from .share_scope import ShareScope from .share_type import ShareType diff --git a/smartsheet/models/enums/report_aggregation_type.py b/smartsheet/models/enums/report_aggregation_type.py new file mode 100644 index 00000000..1f2d350d --- /dev/null +++ b/smartsheet/models/enums/report_aggregation_type.py @@ -0,0 +1,27 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2018 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from enum import Enum + + +class ReportAggregationType(Enum): + SUM = 1 + AVG = 2 + MIN = 3 + MAX = 4 + COUNT = 5 + FIRST = 6 + LAST = 7 diff --git a/smartsheet/models/enums/report_boolean_operator.py b/smartsheet/models/enums/report_boolean_operator.py new file mode 100644 index 00000000..18957eae --- /dev/null +++ b/smartsheet/models/enums/report_boolean_operator.py @@ -0,0 +1,22 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2018 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from enum import Enum + + +class ReportBooleanOperator(Enum): + AND = 1 + OR = 2 diff --git a/smartsheet/models/enums/report_filter_operator.py b/smartsheet/models/enums/report_filter_operator.py new file mode 100644 index 00000000..e028e016 --- /dev/null +++ b/smartsheet/models/enums/report_filter_operator.py @@ -0,0 +1,56 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2018 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from enum import Enum + + +class ReportFilterOperator(Enum): + EQUAL = 1 + NOT_EQUAL = 2 + GREATER_THAN = 3 + LESS_THAN = 4 + CONTAINS = 5 + BETWEEN = 6 + TODAY = 7 + PAST = 8 + FUTURE = 9 + LAST_N_DAYS = 10 + NEXT_N_DAYS = 11 + IS_BLANK = 12 + IS_NOT_BLANK = 13 + IS_NUMBER = 14 + IS_NOT_NUMBER = 15 + IS_DATE = 16 + IS_NOT_DATE = 17 + IS_CHECKED = 18 + IS_UNCHECKED = 19 + IS_ONE_OF = 20 + IS_NOT_ONE_OF = 21 + LESS_THAN_OR_EQUAL = 22 + GREATER_THAN_OR_EQUAL = 23 + DOES_NOT_CONTAIN = 24 + NOT_BETWEEN = 25 + NOT_TODAY = 26 + NOT_PAST = 27 + NOT_FUTURE = 28 + NOT_LAST_N_DAYS = 29 + NOT_NEXT_N_DAYS = 30 + HAS_ANY_OF = 31 + HAS_NONE_OF = 32 + HAS_ALL_OF = 33 + NOT_ALL_OF = 34 + MULTI_IS_EQUAL = 35 + MULTI_IS_NOT_EQUAL = 36 diff --git a/smartsheet/models/enums/report_system_column_type.py b/smartsheet/models/enums/report_system_column_type.py new file mode 100644 index 00000000..a163c174 --- /dev/null +++ b/smartsheet/models/enums/report_system_column_type.py @@ -0,0 +1,26 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2018 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from enum import Enum + + +class ReportSystemColumnType(Enum): + AUTO_NUMBER = 1 + MODIFIED_DATE = 2 + MODIFIED_BY = 3 + CREATED_DATE = 4 + CREATED_BY = 5 + SHEET_NAME = 6 # Valid for reports only diff --git a/smartsheet/models/report_aggregation_criterion.py b/smartsheet/models/report_aggregation_criterion.py new file mode 100644 index 00000000..f3ec1c4e --- /dev/null +++ b/smartsheet/models/report_aggregation_criterion.py @@ -0,0 +1,80 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import Optional, Union + +from ..types import EnumeratedValue, TypedObject, Boolean, json +from ..util import deserialize, serialize +from .enums import ReportAggregationType +from .report_column_identifier import ReportColumnIdentifier + + +class ReportAggregationCriterion: + """Smartsheet ReportAggregationCriterion data model. + + Report aggregation criterion. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportAggregationCriterion model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._column = TypedObject(ReportColumnIdentifier) + self._aggregation_type = EnumeratedValue(ReportAggregationType) + self._is_expanded = Boolean() + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def column(self) -> Optional[ReportColumnIdentifier]: + return self._column.value + + @column.setter + def column(self, value: Union[ReportColumnIdentifier, dict]) -> None: + self._column.value = value + + @property + def aggregation_type(self) -> EnumeratedValue: + return self._aggregation_type + + @aggregation_type.setter + def aggregation_type(self, value: Union[ReportAggregationType, str]) -> None: + self._aggregation_type.set(value) + + @property + def is_expanded(self) -> Optional[bool]: + return self._is_expanded.value + + @is_expanded.setter + def is_expanded(self, value: bool) -> None: + self._is_expanded.value = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_column_identifier.py b/smartsheet/models/report_column_identifier.py new file mode 100644 index 00000000..a5e64c9d --- /dev/null +++ b/smartsheet/models/report_column_identifier.py @@ -0,0 +1,101 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import Optional, Union + +from ..types import EnumeratedValue, String, Boolean, json +from ..util import deserialize, serialize +from .enums import ColumnType, ReportSystemColumnType + + +class ReportColumnIdentifier: + """Smartsheet ReportColumnIdentifier data model. + + Object used to match a sheet column for a report. One of [type, systemColumnType] + or [primary=true] is required. + + systemColumnType should be specified if you want to match a system column. + Use primary=true to match primary columns. When matching primary columns, title + can be used to customize primary column name in the rendered report. + + Note: Columns in the report are matched by the combination of title and type + (and systemColumnType if specified). + + Note: symbol is not used for matching and as a result CHECKBOX or PICKLIST + columns with different symbols (from different sheets) can be combined into the + same column in the report. You cannot combine CHECKBOX with PICKLIST into the + same column in the report because they are different types. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportColumnIdentifier model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._title = String() + self._type = EnumeratedValue(ColumnType) + self._system_column_type = EnumeratedValue(ReportSystemColumnType) + self._primary = Boolean() + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def title(self) -> Optional[str]: + return self._title.value + + @title.setter + def title(self, value: str) -> None: + self._title.value = value + + @property + def type(self) -> EnumeratedValue: + return self._type + + @type.setter + def type(self, value: Union[ColumnType, str]) -> None: + self._type.set(value) + + @property + def system_column_type(self) -> EnumeratedValue: + return self._system_column_type + + @system_column_type.setter + def system_column_type(self, value: Union[ReportSystemColumnType, str]) -> None: + self._system_column_type.set(value) + + @property + def primary(self) -> Optional[bool]: + return self._primary.value + + @primary.setter + def primary(self, value: bool) -> None: + self._primary.value = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_definition.py b/smartsheet/models/report_definition.py new file mode 100644 index 00000000..65be3a66 --- /dev/null +++ b/smartsheet/models/report_definition.py @@ -0,0 +1,98 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from ..types import TypedList, TypedObject, json +from ..util import deserialize, serialize +from .report_aggregation_criterion import ReportAggregationCriterion +from .report_filter_expression import ReportFilterExpression +from .report_grouping_criterion import ReportGroupingCriterion +from .report_sorting_criterion import ReportSortingCriterion + + +class ReportDefinition: + """Smartsheet ReportDefinition data model. + + The report definition contains filters, grouping and sorting properties of the report. + + Note: When groupingCriteria is defined the primary column of the report will move + to index 0 when it is first rendered by the app. + + Supports partial updates on root level properties such as: + - filters + - groupingCriteria + - aggregationCriteria + - sortingCriteria + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportDefinition model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._filters = TypedObject(ReportFilterExpression) + self._grouping_criteria = TypedList(ReportGroupingCriterion) + self._aggregation_criteria = TypedList(ReportAggregationCriterion) + self._sorting_criteria = TypedList(ReportSortingCriterion) + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def filters(self): + return self._filters.value + + @filters.setter + def filters(self, value): + self._filters.value = value + + @property + def grouping_criteria(self): + return self._grouping_criteria + + @grouping_criteria.setter + def grouping_criteria(self, value): + self._grouping_criteria.load(value) + + @property + def aggregation_criteria(self): + return self._aggregation_criteria + + @aggregation_criteria.setter + def aggregation_criteria(self, value): + self._aggregation_criteria.load(value) + + @property + def sorting_criteria(self): + return self._sorting_criteria + + @sorting_criteria.setter + def sorting_criteria(self, value): + self._sorting_criteria.load(value) + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_filter_criterion.py b/smartsheet/models/report_filter_criterion.py new file mode 100644 index 00000000..0cb8989d --- /dev/null +++ b/smartsheet/models/report_filter_criterion.py @@ -0,0 +1,81 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import List, Optional, Union + +from ..types import EnumeratedValue, TypedObject, json +from ..util import deserialize, serialize +from .enums import ReportFilterOperator +from .report_column_identifier import ReportColumnIdentifier + + +class ReportFilterCriterion: + """Smartsheet ReportFilterCriterion data model. + + Criteria object specifying custom criteria against which to match cell values. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportFilterCriterion model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._column = TypedObject(ReportColumnIdentifier) + self._operator = EnumeratedValue(ReportFilterOperator) + self._values = None + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def column(self) -> Optional[ReportColumnIdentifier]: + return self._column.value + + @column.setter + def column(self, value: Union[ReportColumnIdentifier, dict]) -> None: + self._column.value = value + + @property + def operator(self) -> EnumeratedValue: + return self._operator + + @operator.setter + def operator(self, value: Union[ReportFilterOperator, str]) -> None: + self._operator.set(value) + + @property + def values(self) -> Optional[List[str]]: + return self._values + + @values.setter + def values(self, value: List[str]) -> None: + if isinstance(value, list): + self._values = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_filter_expression.py b/smartsheet/models/report_filter_expression.py new file mode 100644 index 00000000..5d6813ae --- /dev/null +++ b/smartsheet/models/report_filter_expression.py @@ -0,0 +1,101 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import Union + +from ..types import EnumeratedValue, TypedList, json +from ..util import deserialize, serialize +from .enums import ReportBooleanOperator +from .report_filter_criterion import ReportFilterCriterion + + +class ReportFilterExpression: + """Smartsheet ReportFilterExpression data model. + + Report filter expression. It is a recursive object that allows at most 3 levels. + + At least one of criteria or nestedCriteria has to be provided in addition to operator. + + Example: + { + "operator": "OR", + "nestedCriteria": [ + { + "operator": "AND", + "nestedCriteria": [], + "criteria": [ + { + "column": { "title": "Price", "type": "TEXT_NUMBER" }, + "operator": "GREATER_THAN", + "values": ["11"] + } + ] + } + ], + "criteria": [] + } + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportFilterExpression model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._operator = EnumeratedValue(ReportBooleanOperator) + self._nested_criteria = TypedList(ReportFilterExpression) + self._criteria = TypedList(ReportFilterCriterion) + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def operator(self) -> EnumeratedValue: + return self._operator + + @operator.setter + def operator(self, value: Union[ReportBooleanOperator, str]) -> None: + self._operator.set(value) + + @property + def nested_criteria(self) -> TypedList: + return self._nested_criteria + + @nested_criteria.setter + def nested_criteria(self, value: list) -> None: + self._nested_criteria.load(value) + + @property + def criteria(self) -> TypedList: + return self._criteria + + @criteria.setter + def criteria(self, value: list) -> None: + self._criteria.load(value) + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_grouping_criterion.py b/smartsheet/models/report_grouping_criterion.py new file mode 100644 index 00000000..a7e79586 --- /dev/null +++ b/smartsheet/models/report_grouping_criterion.py @@ -0,0 +1,89 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import Optional, Union + +from ..types import EnumeratedValue, String, TypedObject, Boolean, json +from ..util import deserialize, serialize +from .enums import SortDirection +from .report_column_identifier import ReportColumnIdentifier + + +class ReportGroupingCriterion: + """Smartsheet ReportGroupingCriterion data model. + + Report grouping criterion. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportGroupingCriterion model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._id = String() + self._column = TypedObject(ReportColumnIdentifier) + self._sorting_direction = EnumeratedValue(SortDirection) + self._is_expanded = Boolean() + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def id(self) -> Optional[str]: + return self._id.value + + @id.setter + def id(self, value: str) -> None: + self._id.value = value + + @property + def column(self) -> Optional[ReportColumnIdentifier]: + return self._column.value + + @column.setter + def column(self, value: Union[ReportColumnIdentifier, dict]) -> None: + self._column.value = value + + @property + def sorting_direction(self) -> EnumeratedValue: + return self._sorting_direction + + @sorting_direction.setter + def sorting_direction(self, value: Union[SortDirection, str]) -> None: + self._sorting_direction.set(value) + + @property + def is_expanded(self) -> Optional[bool]: + return self._is_expanded.value + + @is_expanded.setter + def is_expanded(self, value: bool) -> None: + self._is_expanded.value = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/models/report_sorting_criterion.py b/smartsheet/models/report_sorting_criterion.py new file mode 100644 index 00000000..eff04f9e --- /dev/null +++ b/smartsheet/models/report_sorting_criterion.py @@ -0,0 +1,80 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2016 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from typing import Optional, Union + +from ..types import EnumeratedValue, TypedObject, Boolean, json +from ..util import deserialize, serialize +from .enums import SortDirection +from .report_column_identifier import ReportColumnIdentifier + + +class ReportSortingCriterion: + """Smartsheet ReportSortingCriterion data model. + + Report sorting criterion. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportSortingCriterion model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._column = TypedObject(ReportColumnIdentifier) + self._sorting_direction = EnumeratedValue(SortDirection) + self._force_nulls_to_bottom = Boolean() + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def column(self) -> Optional[ReportColumnIdentifier]: + return self._column.value + + @column.setter + def column(self, value: Union[ReportColumnIdentifier, dict]) -> None: + self._column.value = value + + @property + def sorting_direction(self) -> EnumeratedValue: + return self._sorting_direction + + @sorting_direction.setter + def sorting_direction(self, value: Union[SortDirection, str]) -> None: + self._sorting_direction.set(value) + + @property + def force_nulls_to_bottom(self) -> Optional[bool]: + return self._force_nulls_to_bottom.value + + @force_nulls_to_bottom.setter + def force_nulls_to_bottom(self, value: bool) -> None: + self._force_nulls_to_bottom.value = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 09513d66..48b0ee9c 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -24,7 +24,7 @@ from datetime import datetime from .util import fresh_operation -from .models import Error, DownloadedFile, IndexResult, Report, ReportPublish, Result, Share +from .models import Error, DownloadedFile, IndexResult, Report, ReportDefinition, ReportPublish, Result, Share class Reports: @@ -383,3 +383,35 @@ def set_publish_status(self, report_id, report_publish_obj) -> Union[Result[Repo response = self._base.request(prepped_request, expected, _op) return response + + def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition) -> Union[Result[None], Error]: + """Updates a report's definition. + + Update a Report's definition based on the specified ID. + + Note: This endpoint supports partial updates only on root level + properties of the report definition, such as filters, groupingCriteria + and aggregationCriteria. For example, you can update the report's + filters without affecting its grouping criteria. However, nested + properties within these objects, such as a specific filter or grouping + criterion, cannot be updated individually and require a full replacement + of the respective section. + + Args: + report_id (int): Report ID + report_definition_obj (ReportDefinition): ReportDefinition object. + + Returns: + Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. + """ + _op = fresh_operation("update_report_definition") + _op["method"] = "PATCH" + _op["path"] = "/reports/" + str(report_id) + "/definition" + _op["json"] = report_definition_obj + + expected = ["Result", None] + + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response diff --git a/tests/mock_api/reports/test_update_report_definition.py b/tests/mock_api/reports/test_update_report_definition.py new file mode 100644 index 00000000..c855f429 --- /dev/null +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -0,0 +1,343 @@ +import json +import uuid +from urllib.parse import urlparse + +from smartsheet.models import ( + Error, + ReportDefinition, + ReportFilterExpression, + ReportGroupingCriterion, + ReportAggregationCriterion, + ReportSortingCriterion, +) +from tests.mock_api.mock_api_test_helper import ( + get_mock_api_client, + get_wiremock_request, +) + + +# Test constants +TEST_REPORT_ID = 4583173393803140 +TEST_SUCCESS_MESSAGE = "SUCCESS" +TEST_RESULT_CODE = 0 + + +def test_update_report_definition_generated_url_is_correct(): + """Test that the URL is correctly generated for PATCH /reports/{id}/definition.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + report_definition = ReportDefinition({ + "filters": { + "operator": "AND", + "criteria": [] + } + }) + + client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + wiremock_request = get_wiremock_request(request_id) + url = urlparse(wiremock_request["absoluteUrl"]) + assert url.path == f'/2.0/reports/{TEST_REPORT_ID}/definition' + assert wiremock_request["method"] == "PATCH" + + +def test_update_report_definition_request_body_with_nested_filters(): + """Test that request body is correctly serialized with nested filter criteria.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + # Create a complex filter expression with nested criteria to test circular marshalling + filter_expression = ReportFilterExpression({ + "operator": "OR", + "nestedCriteria": [ + { + "operator": "AND", + "nestedCriteria": [], + "criteria": [ + { + "column": {"title": "Price", "type": "TEXT_NUMBER"}, + "operator": "GREATER_THAN", + "values": ["11"] + }, + { + "column": {"primary": True}, + "operator": "CONTAINS", + "values": ["PROJ-1"] + } + ] + }, + { + "operator": "AND", + "nestedCriteria": [], + "criteria": [ + { + "column": {"title": "Quantity", "type": "TEXT_NUMBER"}, + "operator": "LESS_THAN", + "values": ["12"] + }, + { + "column": {"title": "Sold Out", "type": "CHECKBOX"}, + "operator": "IS_CHECKED", + "values": [] + } + ] + } + ], + "criteria": [] + }) + + grouping_criterion = ReportGroupingCriterion({ + "column": {"title": "Status", "type": "PICKLIST"}, + "sortingDirection": "ASCENDING", + "isExpanded": True + }) + + aggregation_criterion = ReportAggregationCriterion({ + "column": {"title": "Price", "type": "TEXT_NUMBER"}, + "aggregationType": "SUM", + "isExpanded": True + }) + + sorting_criterion = ReportSortingCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "sortingDirection": "DESCENDING", + "forceNullsToBottom": True + }) + + report_definition = ReportDefinition() + report_definition.filters = filter_expression + report_definition.grouping_criteria = [grouping_criterion] + report_definition.aggregation_criteria = [aggregation_criterion] + report_definition.sorting_criteria = [sorting_criterion] + + client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + + # Verify filter structure with nested criteria + assert body["filters"]["operator"] == "OR" + assert len(body["filters"]["nestedCriteria"]) == 2 + assert body["filters"]["nestedCriteria"][0]["operator"] == "AND" + assert len(body["filters"]["nestedCriteria"][0]["criteria"]) == 2 + assert body["filters"]["nestedCriteria"][0]["criteria"][0]["column"]["title"] == "Price" + assert body["filters"]["nestedCriteria"][0]["criteria"][0]["operator"] == "GREATER_THAN" + assert body["filters"]["nestedCriteria"][0]["criteria"][1]["column"]["primary"] is True + assert body["filters"]["nestedCriteria"][0]["criteria"][1]["operator"] == "CONTAINS" + + # Verify grouping criteria + assert len(body["groupingCriteria"]) == 1 + assert body["groupingCriteria"][0]["column"]["title"] == "Status" + assert body["groupingCriteria"][0]["sortingDirection"] == "ASCENDING" + assert body["groupingCriteria"][0]["isExpanded"] is True + + # Verify aggregation criteria + assert len(body["aggregationCriteria"]) == 1 + assert body["aggregationCriteria"][0]["column"]["title"] == "Price" + assert body["aggregationCriteria"][0]["aggregationType"] == "SUM" + assert body["aggregationCriteria"][0]["isExpanded"] is True + + # Verify sorting criteria + assert len(body["sortingCriteria"]) == 1 + assert body["sortingCriteria"][0]["column"]["title"] == "Date" + assert body["sortingCriteria"][0]["sortingDirection"] == "DESCENDING" + assert body["sortingCriteria"][0]["forceNullsToBottom"] is True + + +def test_update_report_definition_partial_update_filters_only(): + """Test partial update with only filters property.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + filter_expression = ReportFilterExpression({ + "operator": "AND", + "criteria": [ + { + "column": {"title": "Status", "type": "PICKLIST"}, + "operator": "EQUAL", + "values": ["Complete"] + } + ], + "nestedCriteria": [] + }) + + report_definition = ReportDefinition() + report_definition.filters = filter_expression + + client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + + # Verify only filters are in the request body + assert "filters" in body + assert body["filters"]["operator"] == "AND" + assert len(body["filters"]["criteria"]) == 1 + + +def test_update_report_definition_with_system_column(): + """Test filter with system column type (e.g., SHEET_NAME for reports).""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + filter_expression = ReportFilterExpression({ + "operator": "AND", + "criteria": [ + { + "column": {"systemColumnType": "SHEET_NAME"}, + "operator": "CONTAINS", + "values": ["Project"] + }, + { + "column": {"systemColumnType": "MODIFIED_DATE"}, + "operator": "LAST_N_DAYS", + "values": ["7"] + } + ], + "nestedCriteria": [] + }) + + report_definition = ReportDefinition() + report_definition.filters = filter_expression + + client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + + # Verify system column types are correctly serialized + assert body["filters"]["criteria"][0]["column"]["systemColumnType"] == "SHEET_NAME" + assert body["filters"]["criteria"][1]["column"]["systemColumnType"] == "MODIFIED_DATE" + + +def test_update_report_definition_all_response_properties(): + """Test that all response properties are correctly deserialized.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + report_definition = ReportDefinition({ + "filters": { + "operator": "AND", + "criteria": [] + } + }) + + response = client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + assert response.message == TEST_SUCCESS_MESSAGE + assert response.result_code == TEST_RESULT_CODE + + +def test_update_report_definition_error_4xx(): + """Test 4xx error response handling.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/400-response", request_id + ) + + report_definition = ReportDefinition({ + "filters": { + "operator": "AND", + "criteria": [] + } + }) + + response = client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + assert isinstance(response, Error) + + +def test_update_report_definition_error_5xx(): + """Test 5xx error response handling.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/500-response", request_id + ) + + report_definition = ReportDefinition({ + "filters": { + "operator": "AND", + "criteria": [] + } + }) + + response = client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + assert isinstance(response, Error) + + +def test_update_report_definition_multiple_aggregation_types(): + """Test with multiple aggregation criteria using different aggregation types.""" + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/update-report-definition/all-response-body-properties", request_id + ) + + aggregation_criteria = [ + ReportAggregationCriterion({ + "column": {"title": "Price", "type": "TEXT_NUMBER"}, + "aggregationType": "SUM" + }), + ReportAggregationCriterion({ + "column": {"title": "Quantity", "type": "TEXT_NUMBER"}, + "aggregationType": "AVG" + }), + ReportAggregationCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "aggregationType": "MIN" + }), + ReportAggregationCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "aggregationType": "MAX" + }), + ] + + report_definition = ReportDefinition() + report_definition.aggregation_criteria = aggregation_criteria + + client.Reports.update_report_definition( + report_id=TEST_REPORT_ID, + report_definition_obj=report_definition, + ) + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + + # Verify all aggregation types are correctly serialized + assert len(body["aggregationCriteria"]) == 4 + assert body["aggregationCriteria"][0]["aggregationType"] == "SUM" + assert body["aggregationCriteria"][1]["aggregationType"] == "AVG" + assert body["aggregationCriteria"][2]["aggregationType"] == "MIN" + assert body["aggregationCriteria"][3]["aggregationType"] == "MAX" From 45cab93bb56b4c6305d15ae7d31451ed663c78ad Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Tue, 24 Feb 2026 16:13:43 +0000 Subject: [PATCH 02/10] feat: add support for update report definition endpoint update docs-source with the new models, and enums --- docs-source/smartsheet_enums.rst | 32 ++++++++++++++++++ docs-source/smartsheet_models.rst | 56 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/docs-source/smartsheet_enums.rst b/docs-source/smartsheet_enums.rst index 20cb4d15..1771cc96 100644 --- a/docs-source/smartsheet_enums.rst +++ b/docs-source/smartsheet_enums.rst @@ -169,6 +169,38 @@ PaperType :undoc-members: :show-inheritance: +ReportAggregationType +----------------------------------------------------- + +.. automodule:: smartsheet.models.enums.report_aggregation_type + :members: + :undoc-members: + :show-inheritance: + +ReportBooleanOperator +----------------------------------------------------- + +.. automodule:: smartsheet.models.enums.report_boolean_operator + :members: + :undoc-members: + :show-inheritance: + +ReportFilterOperator +---------------------------------------------------- + +.. automodule:: smartsheet.models.enums.report_filter_operator + :members: + :undoc-members: + :show-inheritance: + +ReportSystemColumnType +------------------------------------------------------ + +.. automodule:: smartsheet.models.enums.report_system_column_type + :members: + :undoc-members: + :show-inheritance: + PredecessorType ------------------------------------------------ diff --git a/docs-source/smartsheet_models.rst b/docs-source/smartsheet_models.rst index 30de3cc7..3c57d00d 100644 --- a/docs-source/smartsheet_models.rst +++ b/docs-source/smartsheet_models.rst @@ -553,6 +553,14 @@ Report :undoc-members: :show-inheritance: +ReportAggregationCriterion +-------------------------- + +.. automodule:: smartsheet.models.report_aggregation_criterion + :members: + :undoc-members: + :show-inheritance: + ReportCell ---------- @@ -569,6 +577,46 @@ ReportColumn :undoc-members: :show-inheritance: +ReportColumnIdentifier +---------------------- + +.. automodule:: smartsheet.models.report_column_identifier + :members: + :undoc-members: + :show-inheritance: + +ReportDefinition +---------------- + +.. automodule:: smartsheet.models.report_definition + :members: + :undoc-members: + :show-inheritance: + +ReportFilterCriterion +--------------------- + +.. automodule:: smartsheet.models.report_filter_criterion + :members: + :undoc-members: + :show-inheritance: + +ReportFilterExpression +---------------------- + +.. automodule:: smartsheet.models.report_filter_expression + :members: + :undoc-members: + :show-inheritance: + +ReportGroupingCriterion +----------------------- + +.. automodule:: smartsheet.models.report_grouping_criterion + :members: + :undoc-members: + :show-inheritance: + ReportPublish ------------- @@ -585,6 +633,14 @@ ReportRow :undoc-members: :show-inheritance: +ReportSortingCriterion +---------------------- + +.. automodule:: smartsheet.models.report_sorting_criterion + :members: + :undoc-members: + :show-inheritance: + ReportWidgetContent ------------------- From 8d438b4684668920f8e1f6fc78226e19c2415b02 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Tue, 24 Feb 2026 16:33:35 +0000 Subject: [PATCH 03/10] feat: add support for update report definition endpoint Fix alphabetical ordering in smartsheet_enums.rst --- docs-source/smartsheet_enums.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs-source/smartsheet_enums.rst b/docs-source/smartsheet_enums.rst index 1771cc96..0bc3b707 100644 --- a/docs-source/smartsheet_enums.rst +++ b/docs-source/smartsheet_enums.rst @@ -169,6 +169,22 @@ PaperType :undoc-members: :show-inheritance: +PredecessorType +------------------------------------------------ + +.. automodule:: smartsheet.models.enums.predecessor_type + :members: + :undoc-members: + :show-inheritance: + +PublishAccessibleBy +------------------------------------------------------ + +.. automodule:: smartsheet.models.enums.publish_accessible_by + :members: + :undoc-members: + :show-inheritance: + ReportAggregationType ----------------------------------------------------- @@ -201,22 +217,6 @@ ReportSystemColumnType :undoc-members: :show-inheritance: -PredecessorType ------------------------------------------------- - -.. automodule:: smartsheet.models.enums.predecessor_type - :members: - :undoc-members: - :show-inheritance: - -PublishAccessibleBy ------------------------------------------------------- - -.. automodule:: smartsheet.models.enums.publish_accessible_by - :members: - :undoc-members: - :show-inheritance: - ScheduleType --------------------------------------------- From 5f6ace872a7fd580572ae6d89ca6874bbdd8b764 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Tue, 24 Feb 2026 16:52:17 +0000 Subject: [PATCH 04/10] chore: update changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b8c9c8..80b3cc44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [x.x.x] - Unreleased +### Added + +- Support for PATCH /reports/{reportId}/definition endpoint +- New `update_report_definition()` method in Reports class to update report definitions +- New `ReportDefinition` model with support for filters, grouping, aggregation, and sorting criteria +- New `ReportFilterExpression` model with recursive structure for complex filter logic +- New `ReportFilterCriterion` model for individual filter conditions +- New `ReportColumnIdentifier` model for identifying columns in report criteria +- New `ReportGroupingCriterion` model for report grouping configuration +- New `ReportAggregationCriterion` model for report aggregation configuration +- New `ReportSortingCriterion` model for report sorting configuration +- New `ReportAggregationType` enum (SUM, AVG, MIN, MAX, COUNT, FIRST, LAST) +- New `ReportBooleanOperator` enum (AND, OR) for filter expressions +- New `ReportFilterOperator` enum with 36 operators for filter criteria +- New `ReportSystemColumnType` enum with report-specific system columns including SHEET_NAME +- Type hints for all new report models and methods +- WireMock integration tests for report definition update endpoint including nested filter validation + ## [3.7.2] - 2026-01-29 ### Added From a77fa0bb352de909cc2d9fd962f6da4b53399a37 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Tue, 10 Mar 2026 14:21:45 +0000 Subject: [PATCH 05/10] feat: add support for add report columns remove invalid options --- smartsheet/models/enums/report_system_column_type.py | 11 +++++------ smartsheet/models/report_sorting_criterion.py | 9 --------- .../mock_api/reports/test_update_report_definition.py | 2 -- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/smartsheet/models/enums/report_system_column_type.py b/smartsheet/models/enums/report_system_column_type.py index a163c174..b74e02ce 100644 --- a/smartsheet/models/enums/report_system_column_type.py +++ b/smartsheet/models/enums/report_system_column_type.py @@ -18,9 +18,8 @@ class ReportSystemColumnType(Enum): - AUTO_NUMBER = 1 - MODIFIED_DATE = 2 - MODIFIED_BY = 3 - CREATED_DATE = 4 - CREATED_BY = 5 - SHEET_NAME = 6 # Valid for reports only + MODIFIED_DATE = 1 + MODIFIED_BY = 2 + CREATED_DATE = 3 + CREATED_BY = 4 + SHEET_NAME = 5 # Valid for reports only diff --git a/smartsheet/models/report_sorting_criterion.py b/smartsheet/models/report_sorting_criterion.py index eff04f9e..7dd4898c 100644 --- a/smartsheet/models/report_sorting_criterion.py +++ b/smartsheet/models/report_sorting_criterion.py @@ -39,7 +39,6 @@ def __init__(self, props=None, base_obj=None): self._column = TypedObject(ReportColumnIdentifier) self._sorting_direction = EnumeratedValue(SortDirection) - self._force_nulls_to_bottom = Boolean() if props: deserialize(self, props) @@ -62,14 +61,6 @@ def sorting_direction(self) -> EnumeratedValue: def sorting_direction(self, value: Union[SortDirection, str]) -> None: self._sorting_direction.set(value) - @property - def force_nulls_to_bottom(self) -> Optional[bool]: - return self._force_nulls_to_bottom.value - - @force_nulls_to_bottom.setter - def force_nulls_to_bottom(self, value: bool) -> None: - self._force_nulls_to_bottom.value = value - def to_dict(self): return serialize(self) diff --git a/tests/mock_api/reports/test_update_report_definition.py b/tests/mock_api/reports/test_update_report_definition.py index c855f429..4104097e 100644 --- a/tests/mock_api/reports/test_update_report_definition.py +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -109,7 +109,6 @@ def test_update_report_definition_request_body_with_nested_filters(): sorting_criterion = ReportSortingCriterion({ "column": {"title": "Date", "type": "DATE"}, "sortingDirection": "DESCENDING", - "forceNullsToBottom": True }) report_definition = ReportDefinition() @@ -152,7 +151,6 @@ def test_update_report_definition_request_body_with_nested_filters(): assert len(body["sortingCriteria"]) == 1 assert body["sortingCriteria"][0]["column"]["title"] == "Date" assert body["sortingCriteria"][0]["sortingDirection"] == "DESCENDING" - assert body["sortingCriteria"][0]["forceNullsToBottom"] is True def test_update_report_definition_partial_update_filters_only(): From d2ac9f8b5727cbe2d1610d10a0b63fb6a659302c Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Thu, 12 Mar 2026 13:06:30 +0000 Subject: [PATCH 06/10] feat: add support for add report columns add update filters query param support --- smartsheet/reports.py | 6 +++--- tests/mock_api/reports/test_update_report_definition.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 48b0ee9c..65bb101f 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -384,7 +384,7 @@ def set_publish_status(self, report_id, report_publish_obj) -> Union[Result[Repo return response - def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition) -> Union[Result[None], Error]: + def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition, updateFilters: bool = False) -> Union[Result[None], Error]: """Updates a report's definition. Update a Report's definition based on the specified ID. @@ -400,13 +400,13 @@ def update_report_definition(self, report_id: int, report_definition_obj: Report Args: report_id (int): Report ID report_definition_obj (ReportDefinition): ReportDefinition object. - + updateFilters (bool): Whether the `filters` property should be updated. Defaults to False. Returns: Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. """ _op = fresh_operation("update_report_definition") _op["method"] = "PATCH" - _op["path"] = "/reports/" + str(report_id) + "/definition" + _op["path"] = "/reports/" + str(report_id) + "/definition?updateFilters=" + str(updateFilters).lower() _op["json"] = report_definition_obj expected = ["Result", None] diff --git a/tests/mock_api/reports/test_update_report_definition.py b/tests/mock_api/reports/test_update_report_definition.py index 4104097e..4a1ca6c1 100644 --- a/tests/mock_api/reports/test_update_report_definition.py +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -44,6 +44,7 @@ def test_update_report_definition_generated_url_is_correct(): wiremock_request = get_wiremock_request(request_id) url = urlparse(wiremock_request["absoluteUrl"]) assert url.path == f'/2.0/reports/{TEST_REPORT_ID}/definition' + assert url.query == "updateFilters=false" assert wiremock_request["method"] == "PATCH" From 58b103240f8a165a1db982d87e025bb8f5810b0b Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Thu, 12 Mar 2026 14:43:46 +0000 Subject: [PATCH 07/10] feat: add support for add report columns fix linting issue --- smartsheet/reports.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 65bb101f..735438b7 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -384,7 +384,7 @@ def set_publish_status(self, report_id, report_publish_obj) -> Union[Result[Repo return response - def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition, updateFilters: bool = False) -> Union[Result[None], Error]: + def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition, update_filters: bool = False) -> Union[Result[None], Error]: """Updates a report's definition. Update a Report's definition based on the specified ID. @@ -400,13 +400,13 @@ def update_report_definition(self, report_id: int, report_definition_obj: Report Args: report_id (int): Report ID report_definition_obj (ReportDefinition): ReportDefinition object. - updateFilters (bool): Whether the `filters` property should be updated. Defaults to False. + update_filters (bool): Whether the `filters` property should be updated. Defaults to False. Returns: Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. """ _op = fresh_operation("update_report_definition") _op["method"] = "PATCH" - _op["path"] = "/reports/" + str(report_id) + "/definition?updateFilters=" + str(updateFilters).lower() + _op["path"] = "/reports/" + str(report_id) + "/definition?updateFilters=" + str(update_filters).lower() _op["json"] = report_definition_obj expected = ["Result", None] From 5d11d019d40a9307d6dbb2b9619a8d5a2688963c Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Thu, 12 Mar 2026 15:05:03 +0000 Subject: [PATCH 08/10] feat: add support for add report columns change for consistency --- smartsheet/reports.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 735438b7..7b929218 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -406,7 +406,8 @@ def update_report_definition(self, report_id: int, report_definition_obj: Report """ _op = fresh_operation("update_report_definition") _op["method"] = "PATCH" - _op["path"] = "/reports/" + str(report_id) + "/definition?updateFilters=" + str(update_filters).lower() + _op["path"] = "/reports/" + str(report_id) + "/definition" + _op["query_params"]["updateFilters"] = str(update_filters).lower() _op["json"] = report_definition_obj expected = ["Result", None] From e66d073c596e27c21ee83a7435acbfa3747a63e2 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Mon, 16 Mar 2026 11:24:43 +0000 Subject: [PATCH 09/10] feat: update report definition response body update change aggregationCriteria to summarizingCriteria to match spec --- CHANGELOG.md | 4 +- docs-source/smartsheet_models.rst | 4 +- smartsheet/models/__init__.py | 2 +- smartsheet/models/report_definition.py | 16 ++++---- ...ion.py => report_summarizing_criterion.py} | 8 ++-- smartsheet/reports.py | 2 +- .../reports/test_update_report_definition.py | 40 +++++++++---------- 7 files changed, 38 insertions(+), 38 deletions(-) rename smartsheet/models/{report_aggregation_criterion.py => report_summarizing_criterion.py} (92%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80b3cc44..f9aa114a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Support for PATCH /reports/{reportId}/definition endpoint - New `update_report_definition()` method in Reports class to update report definitions -- New `ReportDefinition` model with support for filters, grouping, aggregation, and sorting criteria +- New `ReportDefinition` model with support for filters, grouping, summarizing, and sorting criteria - New `ReportFilterExpression` model with recursive structure for complex filter logic - New `ReportFilterCriterion` model for individual filter conditions - New `ReportColumnIdentifier` model for identifying columns in report criteria - New `ReportGroupingCriterion` model for report grouping configuration -- New `ReportAggregationCriterion` model for report aggregation configuration +- New `ReportSummarizingCriterion` model for report summarizing configuration - New `ReportSortingCriterion` model for report sorting configuration - New `ReportAggregationType` enum (SUM, AVG, MIN, MAX, COUNT, FIRST, LAST) - New `ReportBooleanOperator` enum (AND, OR) for filter expressions diff --git a/docs-source/smartsheet_models.rst b/docs-source/smartsheet_models.rst index 3c57d00d..4835b2d3 100644 --- a/docs-source/smartsheet_models.rst +++ b/docs-source/smartsheet_models.rst @@ -553,10 +553,10 @@ Report :undoc-members: :show-inheritance: -ReportAggregationCriterion +ReportSummarizingCriterion -------------------------- -.. automodule:: smartsheet.models.report_aggregation_criterion +.. automodule:: smartsheet.models.report_summarizing_criterion :members: :undoc-members: :show-inheritance: diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index db7c672d..579c9558 100644 --- a/smartsheet/models/__init__.py +++ b/smartsheet/models/__init__.py @@ -80,7 +80,7 @@ from .project_settings import ProjectSettings from .recipient import Recipient from .report import Report -from .report_aggregation_criterion import ReportAggregationCriterion +from .report_summarizing_criterion import ReportSummarizingCriterion from .report_cell import ReportCell from .report_column import ReportColumn from .report_column_identifier import ReportColumnIdentifier diff --git a/smartsheet/models/report_definition.py b/smartsheet/models/report_definition.py index 65be3a66..3c9d4dfd 100644 --- a/smartsheet/models/report_definition.py +++ b/smartsheet/models/report_definition.py @@ -19,7 +19,7 @@ from ..types import TypedList, TypedObject, json from ..util import deserialize, serialize -from .report_aggregation_criterion import ReportAggregationCriterion +from .report_summarizing_criterion import ReportSummarizingCriterion from .report_filter_expression import ReportFilterExpression from .report_grouping_criterion import ReportGroupingCriterion from .report_sorting_criterion import ReportSortingCriterion @@ -36,7 +36,7 @@ class ReportDefinition: Supports partial updates on root level properties such as: - filters - groupingCriteria - - aggregationCriteria + - summarizingCriteria - sortingCriteria """ @@ -48,7 +48,7 @@ def __init__(self, props=None, base_obj=None): self._filters = TypedObject(ReportFilterExpression) self._grouping_criteria = TypedList(ReportGroupingCriterion) - self._aggregation_criteria = TypedList(ReportAggregationCriterion) + self._summarizing_criteria = TypedList(ReportSummarizingCriterion) self._sorting_criteria = TypedList(ReportSortingCriterion) if props: @@ -73,12 +73,12 @@ def grouping_criteria(self, value): self._grouping_criteria.load(value) @property - def aggregation_criteria(self): - return self._aggregation_criteria + def summarizing_criteria(self): + return self._summarizing_criteria - @aggregation_criteria.setter - def aggregation_criteria(self, value): - self._aggregation_criteria.load(value) + @summarizing_criteria.setter + def summarizing_criteria(self, value): + self._summarizing_criteria.load(value) @property def sorting_criteria(self): diff --git a/smartsheet/models/report_aggregation_criterion.py b/smartsheet/models/report_summarizing_criterion.py similarity index 92% rename from smartsheet/models/report_aggregation_criterion.py rename to smartsheet/models/report_summarizing_criterion.py index f3ec1c4e..1eca2ab7 100644 --- a/smartsheet/models/report_aggregation_criterion.py +++ b/smartsheet/models/report_summarizing_criterion.py @@ -25,14 +25,14 @@ from .report_column_identifier import ReportColumnIdentifier -class ReportAggregationCriterion: - """Smartsheet ReportAggregationCriterion data model. +class ReportSummarizingCriterion: + """Smartsheet ReportSummarizingCriterion data model. - Report aggregation criterion. + Report summarizing criterion. """ def __init__(self, props=None, base_obj=None): - """Initialize the ReportAggregationCriterion model.""" + """Initialize the ReportSummarizingCriterion model.""" self._base = None if base_obj is not None: self._base = base_obj diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 7b929218..fb7c3c7a 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -391,7 +391,7 @@ def update_report_definition(self, report_id: int, report_definition_obj: Report Note: This endpoint supports partial updates only on root level properties of the report definition, such as filters, groupingCriteria - and aggregationCriteria. For example, you can update the report's + and summarizingCriteria. For example, you can update the report's filters without affecting its grouping criteria. However, nested properties within these objects, such as a specific filter or grouping criterion, cannot be updated individually and require a full replacement diff --git a/tests/mock_api/reports/test_update_report_definition.py b/tests/mock_api/reports/test_update_report_definition.py index 4a1ca6c1..77171138 100644 --- a/tests/mock_api/reports/test_update_report_definition.py +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -7,7 +7,7 @@ ReportDefinition, ReportFilterExpression, ReportGroupingCriterion, - ReportAggregationCriterion, + ReportSummarizingCriterion, ReportSortingCriterion, ) from tests.mock_api.mock_api_test_helper import ( @@ -101,7 +101,7 @@ def test_update_report_definition_request_body_with_nested_filters(): "isExpanded": True }) - aggregation_criterion = ReportAggregationCriterion({ + summarizing_criterion = ReportSummarizingCriterion({ "column": {"title": "Price", "type": "TEXT_NUMBER"}, "aggregationType": "SUM", "isExpanded": True @@ -115,7 +115,7 @@ def test_update_report_definition_request_body_with_nested_filters(): report_definition = ReportDefinition() report_definition.filters = filter_expression report_definition.grouping_criteria = [grouping_criterion] - report_definition.aggregation_criteria = [aggregation_criterion] + report_definition.summarizing_criteria = [summarizing_criterion] report_definition.sorting_criteria = [sorting_criterion] client.Reports.update_report_definition( @@ -142,11 +142,11 @@ def test_update_report_definition_request_body_with_nested_filters(): assert body["groupingCriteria"][0]["sortingDirection"] == "ASCENDING" assert body["groupingCriteria"][0]["isExpanded"] is True - # Verify aggregation criteria - assert len(body["aggregationCriteria"]) == 1 - assert body["aggregationCriteria"][0]["column"]["title"] == "Price" - assert body["aggregationCriteria"][0]["aggregationType"] == "SUM" - assert body["aggregationCriteria"][0]["isExpanded"] is True + # Verify summarizing criteria + assert len(body["summarizingCriteria"]) == 1 + assert body["summarizingCriteria"][0]["column"]["title"] == "Price" + assert body["summarizingCriteria"][0]["aggregationType"] == "SUM" + assert body["summarizingCriteria"][0]["isExpanded"] is True # Verify sorting criteria assert len(body["sortingCriteria"]) == 1 @@ -298,33 +298,33 @@ def test_update_report_definition_error_5xx(): def test_update_report_definition_multiple_aggregation_types(): - """Test with multiple aggregation criteria using different aggregation types.""" + """Test with multiple summarizing criteria using different aggregation types.""" request_id = uuid.uuid4().hex client = get_mock_api_client( "/reports/update-report-definition/all-response-body-properties", request_id ) - aggregation_criteria = [ - ReportAggregationCriterion({ + summarizing_criteria = [ + ReportSummarizingCriterion({ "column": {"title": "Price", "type": "TEXT_NUMBER"}, "aggregationType": "SUM" }), - ReportAggregationCriterion({ + ReportSummarizingCriterion({ "column": {"title": "Quantity", "type": "TEXT_NUMBER"}, "aggregationType": "AVG" }), - ReportAggregationCriterion({ + ReportSummarizingCriterion({ "column": {"title": "Date", "type": "DATE"}, "aggregationType": "MIN" }), - ReportAggregationCriterion({ + ReportSummarizingCriterion({ "column": {"title": "Date", "type": "DATE"}, "aggregationType": "MAX" }), ] report_definition = ReportDefinition() - report_definition.aggregation_criteria = aggregation_criteria + report_definition.summarizing_criteria = summarizing_criteria client.Reports.update_report_definition( report_id=TEST_REPORT_ID, @@ -335,8 +335,8 @@ def test_update_report_definition_multiple_aggregation_types(): body = json.loads(wiremock_request["body"]) # Verify all aggregation types are correctly serialized - assert len(body["aggregationCriteria"]) == 4 - assert body["aggregationCriteria"][0]["aggregationType"] == "SUM" - assert body["aggregationCriteria"][1]["aggregationType"] == "AVG" - assert body["aggregationCriteria"][2]["aggregationType"] == "MIN" - assert body["aggregationCriteria"][3]["aggregationType"] == "MAX" + assert len(body["summarizingCriteria"]) == 4 + assert body["summarizingCriteria"][0]["aggregationType"] == "SUM" + assert body["summarizingCriteria"][1]["aggregationType"] == "AVG" + assert body["summarizingCriteria"][2]["aggregationType"] == "MIN" + assert body["summarizingCriteria"][3]["aggregationType"] == "MAX" From 571be30e03aed6061c882ecc168063e763835d86 Mon Sep 17 00:00:00 2001 From: Scott McGowan Date: Thu, 19 Mar 2026 16:24:34 +0000 Subject: [PATCH 10/10] feat: update report definition response changed to PUT from PATCH no longer returning changed definition no longer needs query param --- smartsheet/reports.py | 6 ++---- tests/mock_api/reports/test_update_report_definition.py | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/smartsheet/reports.py b/smartsheet/reports.py index fb7c3c7a..efa62f11 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -384,7 +384,7 @@ def set_publish_status(self, report_id, report_publish_obj) -> Union[Result[Repo return response - def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition, update_filters: bool = False) -> Union[Result[None], Error]: + def update_report_definition(self, report_id: int, report_definition_obj: ReportDefinition) -> Union[Result[None], Error]: """Updates a report's definition. Update a Report's definition based on the specified ID. @@ -400,14 +400,12 @@ def update_report_definition(self, report_id: int, report_definition_obj: Report Args: report_id (int): Report ID report_definition_obj (ReportDefinition): ReportDefinition object. - update_filters (bool): Whether the `filters` property should be updated. Defaults to False. Returns: Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. """ _op = fresh_operation("update_report_definition") - _op["method"] = "PATCH" + _op["method"] = "PUT" _op["path"] = "/reports/" + str(report_id) + "/definition" - _op["query_params"]["updateFilters"] = str(update_filters).lower() _op["json"] = report_definition_obj expected = ["Result", None] diff --git a/tests/mock_api/reports/test_update_report_definition.py b/tests/mock_api/reports/test_update_report_definition.py index 77171138..4f02ce44 100644 --- a/tests/mock_api/reports/test_update_report_definition.py +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -23,7 +23,7 @@ def test_update_report_definition_generated_url_is_correct(): - """Test that the URL is correctly generated for PATCH /reports/{id}/definition.""" + """Test that the URL is correctly generated for PUT /reports/{id}/definition.""" request_id = uuid.uuid4().hex client = get_mock_api_client( "/reports/update-report-definition/all-response-body-properties", request_id @@ -44,8 +44,7 @@ def test_update_report_definition_generated_url_is_correct(): wiremock_request = get_wiremock_request(request_id) url = urlparse(wiremock_request["absoluteUrl"]) assert url.path == f'/2.0/reports/{TEST_REPORT_ID}/definition' - assert url.query == "updateFilters=false" - assert wiremock_request["method"] == "PATCH" + assert wiremock_request["method"] == "PUT" def test_update_report_definition_request_body_with_nested_filters():