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
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def read_file(filename):
"smartools.operations",
"smartools.types",
],
version="1.4.0",
version="2.0.0",
license="MIT",
description="A wrapper for the smartsheet-python-sdk that monkey-patches in new methods & functionality.",
long_description=read_file("README.md"),
Expand Down
7 changes: 7 additions & 0 deletions smartools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@
except (ImportError, AttributeError):
pass

# SmartoolsShare cannot be patched by the loop above because no other
# smartsheet.models submodule imports Share (Result uses a dynamic lookup).
# Patch it explicitly so that Result.result.setter picks up SmartoolsShare.
from smartools.models.share import SmartoolsShare as _SmartoolsShare
import smartsheet.models as _sm
_sm.Share = _SmartoolsShare

# Import Smartsheet and copy all init variables such as __gov_base__ and __api_base__.
from smartsheet import *

Expand Down
3 changes: 3 additions & 0 deletions smartools/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@
from .cell import SmartoolsCell
from .column import SmartoolsColumn
from .summary_field import SmartoolsSummaryField
from .asset_share_paginated_result import SmartoolsAssetSharesPaginatedResult
from .share import SmartoolsShare
from .container_children import SmartoolsContainerChildren
22 changes: 22 additions & 0 deletions smartools/models/asset_share_paginated_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .share import SmartoolsShare


class SmartoolsAssetSharesPaginatedResult:
"""Result object for list_asset_shares.

Exposes .items (new unified API convention) and .data (.items alias for
backward compatibility with old per-asset-type list_shares callers).

The new /shares endpoint returns the share list under the "items" key;
the old per-asset endpoints used "data". We check "items" first.
"""

def __init__(self, props, dynamic_type=None, base_obj=None):
self._base = base_obj
self.next_page_token = props.get("nextPageToken")
raw = props.get("items") or props.get("data", [])
self.items = [SmartoolsShare(item, base_obj) for item in raw]

@property
def data(self):
return self.items
34 changes: 34 additions & 0 deletions smartools/models/container_children.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class SmartoolsContainerChildren:
"""Parses the paginated response from /workspaces/{id}/children
or /folders/{id}/children.

Children are returned as a flat list with a "resourceType" field
distinguishing sheets, folders, reports, sights, and templates.

Raw dicts are stored (not pre-created model objects) so that TypedList
on the receiving Workspace/Folder can instantiate using the monkey-patched
model class (e.g. SmartoolsSheet) rather than the original SDK class.
"""

def __init__(self, props, dynamic_type=None, base_obj=None):
self._base = base_obj
self.next_page_token = props.get("nextPageToken")

self.sheets = []
self.folders = []
self.reports = []
self.sights = []
self.templates = []

for item in props.get("data", []):
resource_type = item.get("resourceType")
if resource_type == "sheet":
self.sheets.append(item)
elif resource_type == "folder":
self.folders.append(item)
elif resource_type == "report":
self.reports.append(item)
elif resource_type in ("sight", "dashboard"):
self.sights.append(item)
elif resource_type == "template":
self.templates.append(item)
32 changes: 32 additions & 0 deletions smartools/models/share.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from smartsheet.models import Share


class SmartoolsShare(Share):
"""Share model that handles the new unified API's string-typed numeric IDs.

The new /shares endpoint returns userId and groupId as JSON strings
(e.g. "967346962622340") instead of integers. The base Share model's
Number validator rejects strings, so we intercept those setters here.

Monkey-patching in smartools/__init__.py will replace smartsheet.models.Share
with this class, so all code paths (Result, list responses) benefit
automatically.
"""

@Share.user_id.setter
def user_id(self, value):
if isinstance(value, str):
try:
value = int(value)
except (ValueError, TypeError):
return
self._user_id.value = value

@Share.group_id.setter
def group_id(self, value):
if isinstance(value, str):
try:
value = int(value)
except (ValueError, TypeError):
return
self._group_id.value = value
52 changes: 51 additions & 1 deletion smartools/operations/folders.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
from types import SimpleNamespace

from smartsheet import fresh_operation
from smartsheet.folders import Folders
from smartsheet.models import ContainerDestination
from smartsheet.models import ContainerDestination, Folder

from smartools.types import ContainerList
from smartools.types.enumerated_value import SmartoolsEnumeratedValue
from smartools.models import FolderContent
from smartools.models.enums import SmartoolsAccessLevel
from smartools.operations.workspaces import _get_container_children

from smartsheet.models import Sheet

class SmartoolsFolders(Folders):

def get_folder(self, folder_id, include=None):
"""Get the specified Folder and its contents.

Replaces the deprecated GET /folders/{id} endpoint.
Uses the new /metadata + /children endpoints with token-based pagination.

Args:
folder_id (int): Folder ID.
include (list[str]): Optional elements to include.

Returns:
Folder
"""
_op = fresh_operation("get_folder_metadata")
_op["method"] = "GET"
_op["path"] = "/folders/" + str(folder_id) + "/metadata"
_op["query_params"]["include"] = include
prepped = self._base.prepare_request(_op)
folder = self._base.request(prepped, "Folder", _op)

children = _get_container_children(
self._base, "/folders/" + str(folder_id) + "/children"
)
folder.sheets = children.sheets
folder.folders = children.folders
folder.reports = children.reports
folder.sights = children.sights
folder.templates = children.templates

return folder

def list_folders(self, folder_id, page_size=None, page=None, include_all=None):
"""List subfolders within the specified folder.

Replaces deprecated GET /folders/{id}/folders and the deprecated
includeAll parameter. Always returns all subfolders via token-based pagination.

Returns:
SimpleNamespace with .data containing a list of Folder objects.
"""
children = _get_container_children(
self._base, "/folders/" + str(folder_id) + "/children"
)
folders = [Folder(item, self._base) for item in children.folders]
return SimpleNamespace(data=folders)

def list_sheets_in_folder(
self,
folder_id,
Expand Down
53 changes: 53 additions & 0 deletions smartools/operations/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,59 @@ def get_large_report(self, report_id, page_size=None, include=None, level=None,

return report

def share_report(self, report_id, share_obj, send_email=False):
"""Share a report via the unified sharing API.

Replaces the deprecated Reports.share_report endpoint.
Accepts a single Share object (old API signature) or a list.

Returns:
Result: Result with .result[0] containing the created Share.
"""
return self._base.Sharing.share_asset(
share_obj=share_obj,
asset_type="report",
asset_id=report_id,
send_email=send_email,
)

def list_shares(self, report_id, page_size=None, page=None, include_all=None):
"""List all shares for a report via the unified sharing API.

Replaces the deprecated Reports.list_shares endpoint.

Returns:
SmartoolsAssetSharesPaginatedResult: Result with .items and .data.
"""
return self._base.Sharing.list_asset_shares(
asset_type="report",
asset_id=report_id,
include_all=bool(include_all),
)

def update_share(self, report_id, share_id, share_obj):
"""Update a report share via the unified sharing API.

Replaces the deprecated Reports.update_share endpoint.
"""
return self._base.Sharing.update_asset_share(
share_obj=share_obj,
asset_type="report",
asset_id=report_id,
share_id=share_id,
)

def delete_share(self, report_id, share_id):
"""Delete a report share via the unified sharing API.

Replaces the deprecated Reports.delete_share endpoint.
"""
return self._base.Sharing.delete_asset_share(
asset_type="report",
asset_id=report_id,
share_id=share_id,
)

def get_access_level(
self,
report_id,
Expand Down
146 changes: 146 additions & 0 deletions smartools/operations/sharing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import logging

from smartsheet import fresh_operation


class SmartoolsSharing:
"""Unified sharing operations for sheets, reports, sights, and workspaces.

Implements the Smartsheet unified sharing API (POST/GET/PATCH/DELETE /shares)
which replaces the deprecated per-asset-type sharing methods
(Sheets.list_shares, Workspaces.share_workspace, Sights.share_sight, etc.).
"""

def __init__(self, smartsheet_obj):
self._base = smartsheet_obj
self._log = logging.getLogger(__name__)

def list_asset_shares(self, asset_type, asset_id, max_items=None, last_key=None,
include_all=False):
"""List all shares for the specified asset.

Args:
asset_type (str): Asset type — 'sheet', 'report', 'sight', or 'workspace'.
asset_id (int): Asset ID.
max_items (int): Maximum number of items per page.
last_key (str): Pagination token from a previous response.
include_all (bool): When True, auto-paginates to collect all shares.

Returns:
SmartoolsAssetSharesPaginatedResult: Result with .items and .data (alias).
"""
_op = fresh_operation("list_asset_shares")
_op["method"] = "GET"
_op["path"] = "/shares"
_op["query_params"]["assetType"] = asset_type
_op["query_params"]["assetId"] = asset_id
_op["query_params"]["maxItems"] = max_items
_op["query_params"]["lastKey"] = last_key

expected = ["AssetSharesPaginatedResult", "Share"]
prepped = self._base.prepare_request(_op)
result = self._base.request(prepped, expected, _op)

if include_all:
while result.next_page_token:
next_page = self.list_asset_shares(
asset_type=asset_type,
asset_id=asset_id,
last_key=result.next_page_token,
)
result.items.extend(next_page.items)
result.next_page_token = next_page.next_page_token

return result

def share_asset(self, share_obj, asset_type, asset_id, send_email=None):
"""Share an asset with one or more users or groups.

Args:
share_obj (Share | list[Share]): Share object or list of Share objects.
asset_type (str): Asset type — 'sheet', 'report', 'sight', or 'workspace'.
asset_id (int): Asset ID.
send_email (bool): Whether to notify the user by email.

Returns:
Result: Result with .result[0] containing the created Share.
"""
if not isinstance(share_obj, list):
share_obj = [share_obj]

_op = fresh_operation("share_asset")
_op["method"] = "POST"
_op["path"] = "/shares"
_op["query_params"]["assetType"] = asset_type
_op["query_params"]["assetId"] = asset_id
_op["query_params"]["sendEmail"] = send_email
_op["json"] = share_obj

expected = ["Result", "Share"]
prepped = self._base.prepare_request(_op)
return self._base.request(prepped, expected, _op)

def update_asset_share(self, share_obj, asset_type, asset_id, share_id):
"""Update the access level of an existing share.

Args:
share_obj (Share): Share object with updated access_level.
asset_type (str): Asset type — 'sheet', 'report', 'sight', or 'workspace'.
asset_id (int): Asset ID.
share_id (str): Share ID to update.

Returns:
Share: The updated Share object.
"""
_op = fresh_operation("update_asset_share")
_op["method"] = "PATCH"
_op["path"] = "/shares/" + str(share_id)
_op["query_params"]["assetType"] = asset_type
_op["query_params"]["assetId"] = asset_id
_op["json"] = share_obj

expected = "Share"
prepped = self._base.prepare_request(_op)
return self._base.request(prepped, expected, _op)

def delete_asset_share(self, asset_type, asset_id, share_id):
"""Remove a share from an asset.

Args:
asset_type (str): Asset type — 'sheet', 'report', 'sight', or 'workspace'.
asset_id (int): Asset ID.
share_id (str): Share ID to delete.

Returns:
Result: Result object (resultCode 0 on success).
"""
_op = fresh_operation("delete_asset_share")
_op["method"] = "DELETE"
_op["path"] = "/shares/" + str(share_id)
_op["query_params"]["assetType"] = asset_type
_op["query_params"]["assetId"] = asset_id

expected = ["Result", None]
prepped = self._base.prepare_request(_op)
return self._base.request(prepped, expected, _op)

def get_asset_share(self, asset_type, asset_id, share_id):
"""Retrieve a specific share for an asset.

Args:
asset_type (str): Asset type — 'sheet', 'report', 'sight', or 'workspace'.
asset_id (int): Asset ID.
share_id (str): Share ID to retrieve.

Returns:
Share: The Share object.
"""
_op = fresh_operation("get_asset_share")
_op["method"] = "GET"
_op["path"] = "/shares/" + str(share_id)
_op["query_params"]["assetType"] = asset_type
_op["query_params"]["assetId"] = asset_id

expected = "Share"
prepped = self._base.prepare_request(_op)
return self._base.request(prepped, expected, _op)
Loading