diff --git a/CHANGELOG.md b/CHANGELOG.md index 74b8c9c..d1fc4c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [x.x.x] - Unreleased +### Added + +- Support for POST /2.0/reports/{reportId}/scope endpoint +- Support for DELETE /2.0/reports/{ReportId}/scope endpoint +- Added wiremock integration tests for POST /2.0/reports/{reportId}/scope endpoint +- Added wiremock integration tests for DELETE /2.0/reports/{reportId}/scope endpoint + ## [3.7.2] - 2026-01-29 ### Added diff --git a/docs-source/smartsheet_enums.rst b/docs-source/smartsheet_enums.rst index 20cb4d1..98e7ab1 100644 --- a/docs-source/smartsheet_enums.rst +++ b/docs-source/smartsheet_enums.rst @@ -185,6 +185,14 @@ PublishAccessibleBy :undoc-members: :show-inheritance: +ReportAssetType +------------------------------------------------------ + +.. automodule:: smartsheet.models.enums.report_asset_type + :members: + :undoc-members: + :show-inheritance: + ScheduleType --------------------------------------------- diff --git a/docs-source/smartsheet_models.rst b/docs-source/smartsheet_models.rst index 30de3cc..e874d17 100644 --- a/docs-source/smartsheet_models.rst +++ b/docs-source/smartsheet_models.rst @@ -569,6 +569,14 @@ ReportColumn :undoc-members: :show-inheritance: +ReportScopeInclusion +------------ + +.. automodule:: smartsheet.models.report_scope_inclusion + :members: + :undoc-members: + :show-inheritance: + ReportPublish ------------- diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index 2df18ec..2b44975 100644 --- a/smartsheet/models/__init__.py +++ b/smartsheet/models/__init__.py @@ -84,6 +84,7 @@ from .report_column import ReportColumn from .report_publish import ReportPublish from .report_row import ReportRow +from .report_scope_inclusion import ReportScopeInclusion 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..4b2b006 100644 --- a/smartsheet/models/enums/__init__.py +++ b/smartsheet/models/enums/__init__.py @@ -41,6 +41,7 @@ from .paper_type import PaperType from .predecessor_type import PredecessorType from .publish_accessible_by import PublishAccessibleBy +from .report_asset_type import ReportAssetType from .schedule_type import ScheduleType from .share_scope import ShareScope from .share_type import ShareType diff --git a/smartsheet/models/enums/report_asset_type.py b/smartsheet/models/enums/report_asset_type.py new file mode 100644 index 0000000..b682b99 --- /dev/null +++ b/smartsheet/models/enums/report_asset_type.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 ReportAssetType(str, Enum): + SHEET = 'SHEET' + WORKSPACE = 'WORKSPACE' diff --git a/smartsheet/models/report_scope_inclusion.py b/smartsheet/models/report_scope_inclusion.py new file mode 100644 index 0000000..3a6cb7a --- /dev/null +++ b/smartsheet/models/report_scope_inclusion.py @@ -0,0 +1,64 @@ +# 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 __future__ import absolute_import + +from smartsheet.models.enums.report_asset_type import ReportAssetType + +from ..types import EnumeratedValue, Number, json +from ..util import deserialize, serialize + +class ReportScopeInclusion: + + """Smartsheet ReportScopeInclusion data model.""" + + def __init__(self, props=None, base_obj=None): + """Initialize the ReportScopeInclusion model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._asset_type = EnumeratedValue(ReportAssetType) + self._asset_id = Number() + + if props: + deserialize(self, props) + + @property + def asset_id(self) -> int: + return self._asset_id.value + + @asset_id.setter + def asset_id(self, value: int): + self._asset_id.value = value + + @property + def asset_type(self) -> ReportAssetType: + return self._asset_type + + @asset_type.setter + def asset_type(self, value: ReportAssetType): + self._asset_type.set(value) + + def to_dict(self) -> dict: + return serialize(self) + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + def __str__(self) -> str: + return self.to_json() diff --git a/smartsheet/reports.py b/smartsheet/reports.py index 09513d6..ed021a4 100644 --- a/smartsheet/reports.py +++ b/smartsheet/reports.py @@ -23,6 +23,8 @@ import os.path from datetime import datetime +from smartsheet.models.report_scope_inclusion import ReportScopeInclusion + from .util import fresh_operation from .models import Error, DownloadedFile, IndexResult, Report, ReportPublish, Result, Share @@ -383,3 +385,47 @@ def set_publish_status(self, report_id, report_publish_obj) -> Union[Result[Repo response = self._base.request(prepped_request, expected, _op) return response + + def add_report_scope(self, report_id: int, scopes: list[ReportScopeInclusion]) -> Union[Result[None], Error]: + """Add one or more scopes to the report. + + Args: + report_id (int): Report ID + scopes (list[ReportScopeInclusion]): List of scopes to add. + + Returns: + Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. + """ + _op = fresh_operation("add_report_scope") + _op["method"] = "POST" + _op["path"] = "/reports/" + str(report_id) + "/scope" + _op["json"] = scopes + + expected = ["Result", None] + + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response + + def remove_report_scope(self, report_id: int, scopes: list[ReportScopeInclusion]) -> Union[Result[None], Error]: + """Remove one or more scopes from the report. + + Args: + report_id (int): Report ID + scopes (list[ReportScopeInclusion]): List of scopes to remove. + + Returns: + Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. + """ + _op = fresh_operation("remove_report_scope") + _op["method"] = "DELETE" + _op["path"] = "/reports/" + str(report_id) + "/scope" + _op["json"] = scopes + + expected = ["Result", None] + + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response diff --git a/smartsheet/util.py b/smartsheet/util.py index dfb7723..bc8c985 100644 --- a/smartsheet/util.py +++ b/smartsheet/util.py @@ -168,7 +168,6 @@ def deserialize(obj, props): key_ = _camel_to_underscore(key) if hasattr(obj, key_): setattr(obj, key_, value) - else: _log.debug( "object '%s' is missing property '%s'", obj.__class__.__name__, key_ diff --git a/tests/mock_api/reports/common_test_constants.py b/tests/mock_api/reports/common_test_constants.py new file mode 100644 index 0000000..7f0129c --- /dev/null +++ b/tests/mock_api/reports/common_test_constants.py @@ -0,0 +1,5 @@ +TEST_SUCCESS_MESSAGE = "SUCCESS" +TEST_RESULT_CODE = 0 +TEST_SHEET_ID = 9876543210 +TEST_WORKSPACE_ID = 1122334455 +TEST_REPORT_ID = 2233445566 diff --git a/tests/mock_api/reports/test_add_report_scope.py b/tests/mock_api/reports/test_add_report_scope.py new file mode 100644 index 0000000..9ce1e1d --- /dev/null +++ b/tests/mock_api/reports/test_add_report_scope.py @@ -0,0 +1,97 @@ +import json +import uuid +from urllib.parse import urlparse + +from smartsheet.models import Error +from smartsheet.models.enums.report_asset_type import ReportAssetType +from smartsheet.models.report_scope_inclusion import ReportScopeInclusion +from tests.mock_api.reports.common_test_constants import TEST_REPORT_ID, TEST_SHEET_ID, TEST_SUCCESS_MESSAGE, TEST_RESULT_CODE +from tests.mock_api.mock_api_test_helper import ( + get_mock_api_client, + get_wiremock_request, +) + + +def test_add_report_scope_generated_url_is_correct(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/add-report-scope/all-response-body-properties", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + client.Reports.add_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + wiremock_request = get_wiremock_request(request_id) + url = urlparse(wiremock_request["absoluteUrl"]) + assert url.path == f'/2.0/reports/{TEST_REPORT_ID}/scope' + assert wiremock_request["method"] == "POST" + +def test_add_report_scope_all_response_properties(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/add-report-scope/all-response-body-properties", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.add_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert response.message == TEST_SUCCESS_MESSAGE + assert response.result_code == TEST_RESULT_CODE + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + assert body == [{"assetId": TEST_SHEET_ID, "assetType": "SHEET"}] + + + +def test_add_report_scope_error_4xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/400-response", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.add_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert isinstance(response, Error) + + +def test_add_report_scope_error_5xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/500-response", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.add_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert isinstance(response, Error) + diff --git a/tests/mock_api/reports/test_remove_report_scope.py b/tests/mock_api/reports/test_remove_report_scope.py new file mode 100644 index 0000000..2c10e81 --- /dev/null +++ b/tests/mock_api/reports/test_remove_report_scope.py @@ -0,0 +1,97 @@ +import json +import uuid +from urllib.parse import urlparse + +from smartsheet.models import Error +from smartsheet.models.enums.report_asset_type import ReportAssetType +from smartsheet.models.report_scope_inclusion import ReportScopeInclusion +from tests.mock_api.reports.common_test_constants import TEST_REPORT_ID, TEST_SHEET_ID, TEST_SUCCESS_MESSAGE, TEST_RESULT_CODE +from tests.mock_api.mock_api_test_helper import ( + get_mock_api_client, + get_wiremock_request, +) + + +def test_remove_report_scope_generated_url_is_correct(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/remove-report-scope/all-response-body-properties", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + client.Reports.remove_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + wiremock_request = get_wiremock_request(request_id) + url = urlparse(wiremock_request["absoluteUrl"]) + assert url.path == f'/2.0/reports/{TEST_REPORT_ID}/scope' + assert wiremock_request["method"] == "DELETE" + +def test_remove_report_scope_all_response_properties(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/reports/remove-report-scope/all-response-body-properties", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.remove_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert response.message == TEST_SUCCESS_MESSAGE + assert response.result_code == TEST_RESULT_CODE + + wiremock_request = get_wiremock_request(request_id) + body = json.loads(wiremock_request["body"]) + assert body == [{"assetId": TEST_SHEET_ID, "assetType": "SHEET"}] + + + +def test_remove_report_scope_error_4xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/400-response", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.remove_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert isinstance(response, Error) + + +def test_remove_report_scope_error_5xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/500-response", request_id + ) + + scopes = [ReportScopeInclusion({ + "assetId": TEST_SHEET_ID, "assetType": ReportAssetType.SHEET + } + )] + + response = client.Reports.remove_report_scope( + report_id=TEST_REPORT_ID, + scopes=scopes + ) + + assert isinstance(response, Error) +