Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions smartsheet/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions smartsheet/models/data_classification.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a result of the comment above this should return .value (the same change should be applied below in the sheet model.

Suggested change
return self._data_classification
return self._data_classification.value


@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()
1 change: 1 addition & 0 deletions smartsheet/models/enums/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions smartsheet/models/enums/data_classification_type.py
Original file line number Diff line number Diff line change
@@ -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):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This enum should inherit from str as well. Look at SeatType for example.

PUBLIC = 'PUBLIC'
INTERNAL = 'INTERNAL'
CONFIDENTIAL = 'CONFIDENTIAL'
SECRET = 'SECRET'
11 changes: 10 additions & 1 deletion smartsheet/models/sheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
43 changes: 42 additions & 1 deletion smartsheet/sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function (and the one below) needs type definitions for sheet_id and data_classification_obj.

"""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.

Expand Down
Empty file.
3 changes: 3 additions & 0 deletions tests/mock_api/sheets/common_test_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
TEST_SHEET_ID = 1234567890123456
TEST_SUCCESS_MESSAGE = "SUCCESS"
TEST_RESULT_CODE = 0
64 changes: 64 additions & 0 deletions tests/mock_api/sheets/test_delete_data_classification.py
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should assert the response as a whole dict instead of prop by prop. This should be adjusted in all tests.

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)
82 changes: 82 additions & 0 deletions tests/mock_api/sheets/test_set_data_classification.py
Original file line number Diff line number Diff line change
@@ -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)
Loading