From 9cef25e99a0b00c8d8f68cb17d388dcb411bc224 Mon Sep 17 00:00:00 2001 From: Ivan Velev Date: Tue, 5 May 2026 11:30:46 +0300 Subject: [PATCH 1/4] Data classification Put and Delete --- smartsheet/models/__init__.py | 1 + smartsheet/models/data_classification.py | 57 +++++++++++++ smartsheet/models/enums/__init__.py | 1 + .../models/enums/data_classification_type.py | 24 ++++++ smartsheet/sheets.py | 43 +++++++++- test.py | 54 ++++++++++++ tests/mock_api/sheets/__init__.py | 0 .../mock_api/sheets/common_test_constants.py | 3 + .../sheets/test_delete_data_classification.py | 64 +++++++++++++++ .../sheets/test_set_data_classification.py | 82 +++++++++++++++++++ 10 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 smartsheet/models/data_classification.py create mode 100644 smartsheet/models/enums/data_classification_type.py create mode 100644 test.py create mode 100644 tests/mock_api/sheets/__init__.py create mode 100644 tests/mock_api/sheets/common_test_constants.py create mode 100644 tests/mock_api/sheets/test_delete_data_classification.py create mode 100644 tests/mock_api/sheets/test_set_data_classification.py diff --git a/smartsheet/models/__init__.py b/smartsheet/models/__init__.py index fb31a118..d46976db 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 00000000..dc6c87fa --- /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 35a8eb82..97680869 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 00000000..4168ff3f --- /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/sheets.py b/smartsheet/sheets.py index aa473dba..33d329c9 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/test.py b/test.py new file mode 100644 index 00000000..bba04533 --- /dev/null +++ b/test.py @@ -0,0 +1,54 @@ +import smartsheet + +smart = smartsheet.Smartsheet(access_token="7FQvrGGM8gXpDtYnuKpbnDBmFtbOncXyMT38V", api_base="https://api.test.smartsheet.com/2.0") # Create a Smartsheet client + +response = smart.Sheets.list_sheets() # Call the list_sheets() function and store the response object +sheetId = response.data[0].id # Get the ID of the first sheet in the response +sheet = smart.Sheets.get_sheet(sheetId) # Load the sheet by using its ID + +#print(f"The sheet {sheet.name} has {sheet.total_row_count} rows") # #Print information about the sheet + + +def test_row_operations(grid_id, iterations): + """Test creating, updating, and deleting a row N times on a specific grid.""" + + for i in range(iterations): + #print(f"\nIteration {i + 1}/{iterations}") + + # Get the sheet to find column IDs + sheet = smart.Sheets.get_sheet(grid_id) + column_id = sheet.columns[0].id + + # Create a row + new_row = smartsheet.models.Row() + new_row.to_bottom = True + new_row.cells.append({ + 'column_id': column_id, + 'value': f'Test Row {i + 1}' + }) + + #print(f" Creating row...") + create_response = smart.Sheets.add_rows(grid_id, [new_row]) + row_id = create_response.data[0].id + #print(f" Created row with ID: {row_id}") + + # Update the row + updated_row = smartsheet.models.Row() + updated_row.id = row_id + updated_row.cells.append({ + 'column_id': column_id, + 'value': f'Updated Test Row {i + 1}' + }) + + #print(f" Updating row...") + smart.Sheets.update_rows(grid_id, [updated_row]) + #print(f" Updated row {row_id}") + + # Delete the row + #print(f" Deleting row...") + smart.Sheets.delete_rows(grid_id, [row_id]) + #print(f" Deleted row {row_id}") + + +# Run the test with sheet ID and number of iterations +test_row_operations(sheetId, 250) \ No newline at end of file diff --git a/tests/mock_api/sheets/__init__.py b/tests/mock_api/sheets/__init__.py new file mode 100644 index 00000000..e69de29b 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 00000000..0d7ef7d2 --- /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 00000000..7a9460d4 --- /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 00000000..bb722e67 --- /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) From bcdce4989edfd52c01be49f3f90fbbbf91e02023 Mon Sep 17 00:00:00 2001 From: Ivan Velev Date: Tue, 5 May 2026 11:31:01 +0300 Subject: [PATCH 2/4] Data classification field --- smartsheet/models/sheet.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/smartsheet/models/sheet.py b/smartsheet/models/sheet.py index ab5417e0..4f8f86f6 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 From 7e6ca2f43e7001a0c4d77ee038f7bc33cfac5a02 Mon Sep 17 00:00:00 2001 From: Ivan Velev Date: Thu, 7 May 2026 18:25:40 +0300 Subject: [PATCH 3/4] Changelog entry # Conflicts: # CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7dca0b..8696dd46 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 From 6cada226fa92afae42a8f9186c1253e36615ebee Mon Sep 17 00:00:00 2001 From: ivelev96 Date: Fri, 8 May 2026 09:41:29 +0300 Subject: [PATCH 4/4] Delete test.py --- test.py | 54 ------------------------------------------------------ 1 file changed, 54 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index bba04533..00000000 --- a/test.py +++ /dev/null @@ -1,54 +0,0 @@ -import smartsheet - -smart = smartsheet.Smartsheet(access_token="7FQvrGGM8gXpDtYnuKpbnDBmFtbOncXyMT38V", api_base="https://api.test.smartsheet.com/2.0") # Create a Smartsheet client - -response = smart.Sheets.list_sheets() # Call the list_sheets() function and store the response object -sheetId = response.data[0].id # Get the ID of the first sheet in the response -sheet = smart.Sheets.get_sheet(sheetId) # Load the sheet by using its ID - -#print(f"The sheet {sheet.name} has {sheet.total_row_count} rows") # #Print information about the sheet - - -def test_row_operations(grid_id, iterations): - """Test creating, updating, and deleting a row N times on a specific grid.""" - - for i in range(iterations): - #print(f"\nIteration {i + 1}/{iterations}") - - # Get the sheet to find column IDs - sheet = smart.Sheets.get_sheet(grid_id) - column_id = sheet.columns[0].id - - # Create a row - new_row = smartsheet.models.Row() - new_row.to_bottom = True - new_row.cells.append({ - 'column_id': column_id, - 'value': f'Test Row {i + 1}' - }) - - #print(f" Creating row...") - create_response = smart.Sheets.add_rows(grid_id, [new_row]) - row_id = create_response.data[0].id - #print(f" Created row with ID: {row_id}") - - # Update the row - updated_row = smartsheet.models.Row() - updated_row.id = row_id - updated_row.cells.append({ - 'column_id': column_id, - 'value': f'Updated Test Row {i + 1}' - }) - - #print(f" Updating row...") - smart.Sheets.update_rows(grid_id, [updated_row]) - #print(f" Updated row {row_id}") - - # Delete the row - #print(f" Deleting row...") - smart.Sheets.delete_rows(grid_id, [row_id]) - #print(f" Deleted row {row_id}") - - -# Run the test with sheet ID and number of iterations -test_row_operations(sheetId, 250) \ No newline at end of file