diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b8c9c..f9aa114 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, 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 `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 +- 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 diff --git a/docs-source/smartsheet_enums.rst b/docs-source/smartsheet_enums.rst index 20cb4d1..0bc3b70 100644 --- a/docs-source/smartsheet_enums.rst +++ b/docs-source/smartsheet_enums.rst @@ -185,6 +185,38 @@ PublishAccessibleBy :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: + ScheduleType --------------------------------------------- diff --git a/docs-source/smartsheet_models.rst b/docs-source/smartsheet_models.rst index 30de3cc..4835b2d 100644 --- a/docs-source/smartsheet_models.rst +++ b/docs-source/smartsheet_models.rst @@ -553,6 +553,14 @@ Report :undoc-members: :show-inheritance: +ReportSummarizingCriterion +-------------------------- + +.. automodule:: smartsheet.models.report_summarizing_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 ------------------- diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index 2df18ec..579c955 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_summarizing_criterion import ReportSummarizingCriterion 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 ec5e4b9..b842c2e 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 0000000..1f2d350 --- /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 0000000..18957ea --- /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 0000000..e028e01 --- /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 0000000..b74e02c --- /dev/null +++ b/smartsheet/models/enums/report_system_column_type.py @@ -0,0 +1,25 @@ +# 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): + 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_column_identifier.py b/smartsheet/models/report_column_identifier.py new file mode 100644 index 0000000..a5e64c9 --- /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 0000000..3c9d4df --- /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_summarizing_criterion import ReportSummarizingCriterion +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 + - summarizingCriteria + - 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._summarizing_criteria = TypedList(ReportSummarizingCriterion) + 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 summarizing_criteria(self): + return self._summarizing_criteria + + @summarizing_criteria.setter + def summarizing_criteria(self, value): + self._summarizing_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 0000000..0cb8989 --- /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 0000000..5d6813a --- /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 0000000..a7e7958 --- /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 0000000..7dd4898 --- /dev/null +++ b/smartsheet/models/report_sorting_criterion.py @@ -0,0 +1,71 @@ +# 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) + + 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) + + 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_summarizing_criterion.py b/smartsheet/models/report_summarizing_criterion.py new file mode 100644 index 0000000..1eca2ab --- /dev/null +++ b/smartsheet/models/report_summarizing_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 ReportSummarizingCriterion: + """Smartsheet ReportSummarizingCriterion data model. + + Report summarizing criterion. + """ + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportSummarizingCriterion 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/reports.py b/smartsheet/reports.py index 09513d6..efa62f1 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,34 @@ 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 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 + 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"] = "PUT" + _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 0000000..4f02ce4 --- /dev/null +++ b/tests/mock_api/reports/test_update_report_definition.py @@ -0,0 +1,341 @@ +import json +import uuid +from urllib.parse import urlparse + +from smartsheet.models import ( + Error, + ReportDefinition, + ReportFilterExpression, + ReportGroupingCriterion, + ReportSummarizingCriterion, + 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 PUT /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"] == "PUT" + + +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 + }) + + summarizing_criterion = ReportSummarizingCriterion({ + "column": {"title": "Price", "type": "TEXT_NUMBER"}, + "aggregationType": "SUM", + "isExpanded": True + }) + + sorting_criterion = ReportSortingCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "sortingDirection": "DESCENDING", + }) + + report_definition = ReportDefinition() + report_definition.filters = filter_expression + report_definition.grouping_criteria = [grouping_criterion] + report_definition.summarizing_criteria = [summarizing_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 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 + assert body["sortingCriteria"][0]["column"]["title"] == "Date" + assert body["sortingCriteria"][0]["sortingDirection"] == "DESCENDING" + + +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 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 + ) + + summarizing_criteria = [ + ReportSummarizingCriterion({ + "column": {"title": "Price", "type": "TEXT_NUMBER"}, + "aggregationType": "SUM" + }), + ReportSummarizingCriterion({ + "column": {"title": "Quantity", "type": "TEXT_NUMBER"}, + "aggregationType": "AVG" + }), + ReportSummarizingCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "aggregationType": "MIN" + }), + ReportSummarizingCriterion({ + "column": {"title": "Date", "type": "DATE"}, + "aggregationType": "MAX" + }), + ] + + report_definition = ReportDefinition() + report_definition.summarizing_criteria = summarizing_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["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"