-
Notifications
You must be signed in to change notification settings - Fork 38
Added support for Smartsheet-Integration-Source #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mainline
Are you sure you want to change the base?
Changes from all commits
80331ed
0344ff2
07d2dba
df8044a
1dbc195
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 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 SmartsheetIntegrationSourceType(Enum): | ||
| AI = 1 | ||
| SCRIPT = 2 | ||
| APPLICATION = 3 | ||
| PERSONAL_ACCOUNT = 4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
| from .models import Error, ErrorResult | ||
| from .session import pinned_session | ||
| from .util import is_multipart, serialize | ||
| from .smartsheet_integration_source_validator import is_valid_format | ||
|
|
||
| __all__ = ("Smartsheet", "fresh_operation", "AbstractUserCalcBackoff") | ||
|
|
||
|
|
@@ -121,6 +122,7 @@ class Smartsheet: | |
| def __init__( | ||
| self, | ||
| access_token=None, | ||
| smartsheet_integration_source=None, | ||
| max_connections=8, | ||
| user_agent=None, | ||
| max_retry_time=30, | ||
|
|
@@ -134,6 +136,9 @@ def __init__( | |
| access_token (str): Access Token for making client | ||
| requests. May also be set as an env variable in | ||
| SMARTSHEET_ACCESS_TOKEN. (required) | ||
| smartsheet_integration_source (str): Integration source identifier. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wouldn't it make sense to make this a class with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could just be named |
||
| Format: $TYPE,$ORG_NAME,$INTEGRATOR_NAME | ||
| Required. Must be provided to identify the integration source. | ||
| max_connections (int): Maximum connection pool size. | ||
| max_retry_time (int or AbstractUserCalcBackoff): user provided maximum | ||
| elapsed time or AbstractUserCalcBackoff class for user back off calculation on retry. | ||
|
|
@@ -159,6 +164,10 @@ def __init__( | |
| "as a parameter." | ||
| ) | ||
|
|
||
| # Validate Smartsheet integration source format | ||
| is_valid_format(smartsheet_integration_source) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you can use |
||
| self._smartsheet_integration_source = smartsheet_integration_source | ||
|
|
||
| if isinstance(max_retry_time, AbstractUserCalcBackoff): | ||
| self._user_calc_backoff = max_retry_time | ||
| else: | ||
|
|
@@ -236,6 +245,23 @@ def with_change_agent(self, change_agent): | |
| """ | ||
| self._change_agent = change_agent | ||
|
|
||
| def with_smartsheet_integration_source(self, smartsheet_integration_source): | ||
| """ | ||
| Request headers will contain the 'Smartsheet-Integration-Source' header value | ||
|
|
||
| Args: | ||
| smartsheet_integration_source: (str) the name of this integration source | ||
|
|
||
| Format: $TYPE,$ORG_NAME,$INTEGRATOR_NAME | ||
| (NB: Comma is used as a delimiter and is required if value is missing) | ||
| $INTEGRATION-TYPE - Required, the type of the integrator (e.g. AI, SCRIPT, APPLICATION) | ||
| $SMAR-ORGANIZATION-NAME - Optional (but COMMA is required), organization name (e.g. Microsoft, Google, OpenAI, etc.) | ||
| $INTEGRATOR-NAME - Required, the name of the integrator (e.g. Claude, Copilot, ChatGPT, DeepSeek, etc.) | ||
| """ | ||
| # Validate before setting | ||
| is_valid_format(smartsheet_integration_source) | ||
| self._smartsheet_integration_source = smartsheet_integration_source | ||
|
|
||
| def request(self, prepped_request, expected, operation): | ||
| """ | ||
| Make a request from the Smartsheet API. | ||
|
|
@@ -447,6 +473,16 @@ def prepare_request(self, _op): | |
| del prepped_request.headers["Smartsheet-Change-Agent"] | ||
| except KeyError: | ||
| pass | ||
|
|
||
| if self._smartsheet_integration_source is not None: | ||
| prepped_request.headers.update( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dict.update() method is overkill here to set a single key. You should just use |
||
| {"Smartsheet-Integration-Source": self._smartsheet_integration_source} | ||
| ) | ||
| else: | ||
| try: | ||
| del prepped_request.headers["Smartsheet-Integration-Source"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the deletion in the else case? |
||
| except KeyError: | ||
| pass | ||
|
|
||
| return prepped_request | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # pylint: disable=C0111 | ||
| from __future__ import annotations | ||
|
|
||
| from enum import Enum | ||
|
|
||
| from .exceptions import SmartsheetException | ||
| from .models.enums.smartsheet_integration_source_type import SmartsheetIntegrationSourceType | ||
|
|
||
| # Documentation link for error messages | ||
| DOCUMENTATION_LINK = "https://developers.smartsheet.com/api/smartsheet/guides/basics/http-and-rest#http-headers" | ||
|
|
||
| def is_valid_format(input_value: str) -> bool: | ||
| """ | ||
| Validates a smartsheet integration source string in the format: | ||
| type, organisation name, integrator name | ||
|
|
||
| - type: must be one of the enum values | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. must be one of the SmartsheetIntegrationSourceType enum values. |
||
| - organisation name: optional (can be empty) | ||
| - integrator name: non-empty | ||
| """ | ||
| if input_value is None: | ||
| raise SmartsheetException("Smartsheet integration source cannot be null") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The error message should use |
||
|
|
||
| parts = input_value.split(",", -1) # -1 keeps empty slots | ||
| if len(parts) != 3: | ||
| raise SmartsheetException( | ||
| "Invalid smartsheet integration source format. " | ||
| f"Expected format: 'TYPE,ORGANIZATION,INTEGRATOR. {DOCUMENTATION_LINK}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The message here has an opening single quote but the closing one is missing. |
||
| ) | ||
|
|
||
| integration_type = parts[0] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are trimmed in the C# SDK. Maybe its worth doing it here as well for consistency. |
||
| integrator_name = parts[2] | ||
|
|
||
| if not _is_valid_type(integration_type): | ||
| allowed = [t.name for t in SmartsheetIntegrationSourceType] | ||
| raise SmartsheetException( | ||
| "Invalid smartsheet integration source format. " | ||
| f"The integration type has to be one of the following: {allowed}. " | ||
| f"Invalid integration type: {integration_type} {DOCUMENTATION_LINK}" | ||
| ) | ||
|
|
||
| if integrator_name == "": | ||
| raise SmartsheetException( | ||
| "Invalid smartsheet integration source format. " | ||
| "The integrator name cannot be empty." | ||
| ) | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def _is_valid_type(integration_type_value: str | None) -> bool: | ||
| if integration_type_value is None or integration_type_value == "": | ||
| return False | ||
| for source_type in SmartsheetIntegrationSourceType: | ||
| if source_type.name.lower() == integration_type_value.lower(): | ||
| return True | ||
| return False | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # pylint: disable=missing-function-docstring | ||
| import pytest | ||
|
|
||
| from smartsheet.smartsheet_integration_source_validator import ( | ||
| is_valid_format, | ||
| SmartsheetIntegrationSourceType, | ||
| ) | ||
| from smartsheet.exceptions import SmartsheetException | ||
|
|
||
|
|
||
| def test_valid_formats_minimum_and_with_org(): | ||
| assert is_valid_format("AI,,Integrator") is True | ||
| assert is_valid_format("APPLICATION,Org Name,Integrator Name") is True | ||
| assert is_valid_format("SCRIPT,Acme Inc,MyBot") is True | ||
|
|
||
|
|
||
| def test_invalid_when_null(): | ||
| with pytest.raises(SmartsheetException) as exc: | ||
| is_valid_format(None) # type: ignore[arg-type] | ||
| assert "cannot be null" in str(exc.value) | ||
|
|
||
|
|
||
| def test_invalid_when_wrong_number_of_parts(): | ||
| for bad in ["AI,OnlyTwo", "AI,Org,Name,Extra", "AI"]: | ||
| with pytest.raises(SmartsheetException) as exc: | ||
| is_valid_format(bad) | ||
| assert "Invalid smartsheet integration source format" in str(exc.value) | ||
|
|
||
|
|
||
| def test_invalid_when_type_not_allowed(): | ||
| with pytest.raises(SmartsheetException) as exc: | ||
| is_valid_format("NOT_A_TYPE,Org,Integrator") | ||
| assert "The integration type has to be one of the following" in str(exc.value) | ||
| # ensure enum contains expected members | ||
| names = {t.name for t in SmartsheetIntegrationSourceType} | ||
| assert {"AI", "SCRIPT", "APPLICATION"}.issubset(names) | ||
|
|
||
|
|
||
| def test_invalid_when_integrator_missing(): | ||
| with pytest.raises(SmartsheetException) as exc: | ||
| is_valid_format("AI,Org,") | ||
| assert "integrator name cannot be empty" in str(exc.value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be configured using the
Smartsheetclass.