diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7dca0..8696dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [x.x.x] - Unreleased +### Added + +- Support for PUT /sheets/{sheetId}/dataclassification endpoint (Set Data Classification) +- Support for DELETE /sheets/{sheetId}/dataclassification endpoint (Remove Data Classification) +- Added `data_classification` field to Sheet model + ## [3.9.0] - 2026-04-30 ### Added diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index fb31a11..d46976d 100644 --- a/smartsheet/models/__init__.py +++ b/smartsheet/models/__init__.py @@ -43,6 +43,7 @@ from .criteria import Criteria from .cross_sheet_reference import CrossSheetReference from .currency import Currency +from .data_classification import DataClassification from .date_object_value import DateObjectValue from .discussion import Discussion from .downloaded_file import DownloadedFile diff --git a/smartsheet/models/data_classification.py b/smartsheet/models/data_classification.py new file mode 100644 index 0000000..dc6c87f --- /dev/null +++ b/smartsheet/models/data_classification.py @@ -0,0 +1,57 @@ +# 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 ..types import EnumeratedValue, json +from ..util import deserialize, serialize +from .enums import DataClassificationType + + +class DataClassification: + + """Smartsheet DataClassification data model.""" + + def __init__(self, props=None, base_obj=None): + """Initialize the DataClassification model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._data_classification = EnumeratedValue(DataClassificationType) + + if props: + deserialize(self, props) + + self.request_response = None + + @property + def data_classification(self): + return self._data_classification + + @data_classification.setter + def data_classification(self, value): + self._data_classification.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/enums/__init__.py b/smartsheet/models/enums/__init__.py index 35a8eb8..9768086 100644 --- a/smartsheet/models/enums/__init__.py +++ b/smartsheet/models/enums/__init__.py @@ -31,6 +31,7 @@ from .criteria_target import CriteriaTarget from .cross_sheet_reference_status import CrossSheetReferenceStatus from .currency_code import CurrencyCode +from .data_classification_type import DataClassificationType from .day_descriptors import DayDescriptors from .day_ordinal import DayOrdinal from .event_action import EventAction diff --git a/smartsheet/models/enums/data_classification_type.py b/smartsheet/models/enums/data_classification_type.py new file mode 100644 index 0000000..4168ff3 --- /dev/null +++ b/smartsheet/models/enums/data_classification_type.py @@ -0,0 +1,24 @@ +# 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 DataClassificationType(Enum): + PUBLIC = 'PUBLIC' + INTERNAL = 'INTERNAL' + CONFIDENTIAL = 'CONFIDENTIAL' + SECRET = 'SECRET' diff --git a/smartsheet/models/sheet.py b/smartsheet/models/sheet.py index ab5417e..4f8f86f 100644 --- a/smartsheet/models/sheet.py +++ b/smartsheet/models/sheet.py @@ -26,7 +26,7 @@ from .contact_object_value import ContactObjectValue from .cross_sheet_reference import CrossSheetReference from .discussion import Discussion -from .enums import AccessLevel, AttachmentType +from .enums import AccessLevel, AttachmentType, DataClassificationType from .project_settings import ProjectSettings from .row import Row from .sheet_filter import SheetFilter @@ -57,6 +57,7 @@ def __init__(self, props=None, base_obj=None): self._contact_references = TypedList(ContactObjectValue) self._created_at = Timestamp() self._cross_sheet_references = TypedList(CrossSheetReference) + self._data_classification = EnumeratedValue(DataClassificationType) self._dependencies_enabled = Boolean() self._discussions = TypedList(Discussion) self._effective_attachment_options = EnumeratedList(AttachmentType) @@ -151,6 +152,14 @@ def cross_sheet_references(self): def cross_sheet_references(self, value): self._cross_sheet_references.load(value) + @property + def data_classification(self): + return self._data_classification + + @data_classification.setter + def data_classification(self, value): + self._data_classification.set(value) + @property def dependencies_enabled(self): return self._dependencies_enabled.value diff --git a/smartsheet/sheets.py b/smartsheet/sheets.py index aa473db..33d329c 100644 --- a/smartsheet/sheets.py +++ b/smartsheet/sheets.py @@ -282,7 +282,7 @@ def delete_column(self, sheet_id, column_id) -> Union[Result[None], Error]: return response def delete_rows(self, sheet_id, ids, ignore_rows_not_found=False) -> Union[Result[List[NumberObjectValue]], Error]: - """Deletes one or more Row(s) from the specified Sheeet. + """Deletes one or more Row(s) from the specified Sheet. Args: sheet_id (int): Sheet ID @@ -334,6 +334,47 @@ def delete_share(self, sheet_id, share_id) -> Union[Result[None], Error]: return response + def set_data_classification(self, sheet_id, data_classification_obj) -> Union[Result[None], Error]: + """Sets the data classification on a Sheet. + + Args: + sheet_id (int): Sheet ID + data_classification_obj + (DataClassification): DataClassification object. + + Returns: + Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. + """ + _op = fresh_operation("set_data_classification") + _op["method"] = "PUT" + _op["path"] = "/sheets/" + str(sheet_id) + "/dataclassification" + _op["json"] = data_classification_obj + + expected = ["Result", None] + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response + + def delete_data_classification(self, sheet_id) -> Union[Result[None], Error]: + """Removes the data classification from a Sheet. Requires ADMIN or OWNER access. + + Args: + sheet_id (int): Sheet ID + + Returns: + Union[Result[None], Error]: The result of the operation, or an Error object if the request fails. + """ + _op = fresh_operation("delete_data_classification") + _op["method"] = "DELETE" + _op["path"] = "/sheets/" + str(sheet_id) + "/dataclassification" + + expected = ["Result", None] + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response + def delete_sheet(self, sheet_id) -> Union[Result[None], Error]: """Delete the specified Sheet. diff --git a/tests/mock_api/sheets/__init__.py b/tests/mock_api/sheets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mock_api/sheets/common_test_constants.py b/tests/mock_api/sheets/common_test_constants.py new file mode 100644 index 0000000..0d7ef7d --- /dev/null +++ b/tests/mock_api/sheets/common_test_constants.py @@ -0,0 +1,3 @@ +TEST_SHEET_ID = 1234567890123456 +TEST_SUCCESS_MESSAGE = "SUCCESS" +TEST_RESULT_CODE = 0 diff --git a/tests/mock_api/sheets/test_delete_data_classification.py b/tests/mock_api/sheets/test_delete_data_classification.py new file mode 100644 index 0000000..7a9460d --- /dev/null +++ b/tests/mock_api/sheets/test_delete_data_classification.py @@ -0,0 +1,64 @@ +import uuid +from urllib.parse import urlparse + +from smartsheet.models import Error +from tests.mock_api.sheets.common_test_constants import 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_delete_data_classification_generated_url_is_correct(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/sheets/delete-data-classification/all-response-body-properties", request_id + ) + + client.Sheets.delete_data_classification( + sheet_id=TEST_SHEET_ID, + ) + + wiremock_request = get_wiremock_request(request_id) + url = urlparse(wiremock_request["absoluteUrl"]) + assert url.path == f'/2.0/sheets/{TEST_SHEET_ID}/dataclassification' + + +def test_delete_data_classification_all_response_properties(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/sheets/delete-data-classification/all-response-body-properties", request_id + ) + + response = client.Sheets.delete_data_classification( + sheet_id=TEST_SHEET_ID, + ) + + assert response.message == TEST_SUCCESS_MESSAGE + assert response.result_code == TEST_RESULT_CODE + + +def test_delete_data_classification_error_4xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/400-response", request_id + ) + + response = client.Sheets.delete_data_classification( + sheet_id=TEST_SHEET_ID, + ) + + assert isinstance(response, Error) + + +def test_delete_data_classification_error_5xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/500-response", request_id + ) + + response = client.Sheets.delete_data_classification( + sheet_id=TEST_SHEET_ID, + ) + + assert isinstance(response, Error) diff --git a/tests/mock_api/sheets/test_set_data_classification.py b/tests/mock_api/sheets/test_set_data_classification.py new file mode 100644 index 0000000..bb722e6 --- /dev/null +++ b/tests/mock_api/sheets/test_set_data_classification.py @@ -0,0 +1,82 @@ +import json +import uuid +from urllib.parse import urlparse + +from smartsheet.models import Error +from smartsheet.models.data_classification import DataClassification +from tests.mock_api.sheets.common_test_constants import 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_set_data_classification_generated_url_is_correct(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/sheets/set-data-classification/all-response-body-properties", request_id + ) + + data_classification_obj = DataClassification({"dataClassification": "CONFIDENTIAL"}) + + client.Sheets.set_data_classification( + sheet_id=TEST_SHEET_ID, + data_classification_obj=data_classification_obj, + ) + + wiremock_request = get_wiremock_request(request_id) + url = urlparse(wiremock_request["absoluteUrl"]) + assert url.path == f'/2.0/sheets/{TEST_SHEET_ID}/dataclassification' + + +def test_set_data_classification_all_response_properties(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/sheets/set-data-classification/all-response-body-properties", request_id + ) + + data_classification_obj = DataClassification({"dataClassification": "CONFIDENTIAL"}) + + response = client.Sheets.set_data_classification( + sheet_id=TEST_SHEET_ID, + data_classification_obj=data_classification_obj, + ) + + 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 == {"dataClassification": "CONFIDENTIAL"} + + +def test_set_data_classification_error_4xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/400-response", request_id + ) + + data_classification_obj = DataClassification({"dataClassification": "CONFIDENTIAL"}) + + response = client.Sheets.set_data_classification( + sheet_id=TEST_SHEET_ID, + data_classification_obj=data_classification_obj, + ) + + assert isinstance(response, Error) + + +def test_set_data_classification_error_5xx(): + request_id = uuid.uuid4().hex + client = get_mock_api_client( + "/errors/500-response", request_id + ) + + data_classification_obj = DataClassification({"dataClassification": "CONFIDENTIAL"}) + + response = client.Sheets.set_data_classification( + sheet_id=TEST_SHEET_ID, + data_classification_obj=data_classification_obj, + ) + + assert isinstance(response, Error)