smartsheet-python-sdk 3.5.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- smartsheet/__init__.py +37 -0
- smartsheet/attachments.py +565 -0
- smartsheet/cells.py +164 -0
- smartsheet/contacts.py +78 -0
- smartsheet/discussions.py +411 -0
- smartsheet/events.py +79 -0
- smartsheet/exceptions.py +130 -0
- smartsheet/favorites.py +116 -0
- smartsheet/folders.py +438 -0
- smartsheet/groups.py +186 -0
- smartsheet/home.py +180 -0
- smartsheet/images.py +61 -0
- smartsheet/models/__init__.py +126 -0
- smartsheet/models/access_token.py +95 -0
- smartsheet/models/account.py +77 -0
- smartsheet/models/alternate_email.py +88 -0
- smartsheet/models/asset_share.py +165 -0
- smartsheet/models/asset_shares_paginated_result.py +84 -0
- smartsheet/models/attachment.py +181 -0
- smartsheet/models/auto_number_format.py +81 -0
- smartsheet/models/automation_action.py +162 -0
- smartsheet/models/automation_rule.py +164 -0
- smartsheet/models/boolean_object_value.py +38 -0
- smartsheet/models/bulk_item_failure.py +77 -0
- smartsheet/models/bulk_item_result.py +111 -0
- smartsheet/models/cell.py +193 -0
- smartsheet/models/cell_data_item.py +152 -0
- smartsheet/models/cell_history.py +67 -0
- smartsheet/models/cell_link.py +91 -0
- smartsheet/models/cell_link_widget_content.py +101 -0
- smartsheet/models/chart_widget_content.py +124 -0
- smartsheet/models/column.py +253 -0
- smartsheet/models/comment.py +126 -0
- smartsheet/models/contact.py +88 -0
- smartsheet/models/contact_object_value.py +59 -0
- smartsheet/models/container_destination.py +74 -0
- smartsheet/models/copy_or_move_row_destination.py +54 -0
- smartsheet/models/copy_or_move_row_directive.py +64 -0
- smartsheet/models/copy_or_move_row_result.py +67 -0
- smartsheet/models/criteria.py +82 -0
- smartsheet/models/cross_sheet_reference.py +134 -0
- smartsheet/models/currency.py +64 -0
- smartsheet/models/date_object_value.py +56 -0
- smartsheet/models/datetime_object_value.py +56 -0
- smartsheet/models/discussion.py +183 -0
- smartsheet/models/downloaded_file.py +106 -0
- smartsheet/models/duration.py +112 -0
- smartsheet/models/email.py +82 -0
- smartsheet/models/enums/__init__.py +56 -0
- smartsheet/models/enums/access_level.py +26 -0
- smartsheet/models/enums/asset_type.py +10 -0
- smartsheet/models/enums/attachment_parent_type.py +23 -0
- smartsheet/models/enums/attachment_sub_type.py +26 -0
- smartsheet/models/enums/attachment_type.py +29 -0
- smartsheet/models/enums/automation_action_frequency.py +24 -0
- smartsheet/models/enums/automation_action_type.py +23 -0
- smartsheet/models/enums/automation_rule_disabled_reason.py +27 -0
- smartsheet/models/enums/cell_link_status.py +28 -0
- smartsheet/models/enums/column_type.py +31 -0
- smartsheet/models/enums/criteria_target.py +21 -0
- smartsheet/models/enums/cross_sheet_reference_status.py +28 -0
- smartsheet/models/enums/currency_code.py +43 -0
- smartsheet/models/enums/day_descriptors.py +30 -0
- smartsheet/models/enums/day_ordinal.py +25 -0
- smartsheet/models/enums/event_action.py +76 -0
- smartsheet/models/enums/event_obejct_type.py +34 -0
- smartsheet/models/enums/event_source.py +27 -0
- smartsheet/models/enums/global_template.py +23 -0
- smartsheet/models/enums/operator.py +62 -0
- smartsheet/models/enums/paper_type.py +29 -0
- smartsheet/models/enums/predecessor_type.py +24 -0
- smartsheet/models/enums/publish_accessible_by.py +22 -0
- smartsheet/models/enums/schedule_type.py +25 -0
- smartsheet/models/enums/seat_type.py +17 -0
- smartsheet/models/enums/share_scope.py +22 -0
- smartsheet/models/enums/share_type.py +22 -0
- smartsheet/models/enums/sheet_email_format.py +23 -0
- smartsheet/models/enums/sheet_filter_operator.py +22 -0
- smartsheet/models/enums/sheet_filter_type.py +23 -0
- smartsheet/models/enums/sort_direction.py +22 -0
- smartsheet/models/enums/symbol.py +45 -0
- smartsheet/models/enums/system_column_type.py +25 -0
- smartsheet/models/enums/update_request_status.py +23 -0
- smartsheet/models/enums/user_status.py +23 -0
- smartsheet/models/enums/widget_type.py +32 -0
- smartsheet/models/error.py +74 -0
- smartsheet/models/error_result.py +117 -0
- smartsheet/models/event.py +153 -0
- smartsheet/models/event_result.py +86 -0
- smartsheet/models/explicit_null.py +24 -0
- smartsheet/models/favorite.py +81 -0
- smartsheet/models/folder.py +177 -0
- smartsheet/models/font_family.py +63 -0
- smartsheet/models/format_details.py +55 -0
- smartsheet/models/format_tables.py +191 -0
- smartsheet/models/group.py +134 -0
- smartsheet/models/group_member.py +104 -0
- smartsheet/models/home.py +110 -0
- smartsheet/models/hyperlink.py +81 -0
- smartsheet/models/image.py +101 -0
- smartsheet/models/image_url.py +91 -0
- smartsheet/models/image_url_map.py +68 -0
- smartsheet/models/image_widget_content.py +117 -0
- smartsheet/models/index_result.py +118 -0
- smartsheet/models/json_object.py +59 -0
- smartsheet/models/multi_contact_object_value.py +49 -0
- smartsheet/models/multi_picklist_object_value.py +48 -0
- smartsheet/models/multi_row_email.py +60 -0
- smartsheet/models/number_object_value.py +38 -0
- smartsheet/models/o_auth_error.py +86 -0
- smartsheet/models/object_value.py +130 -0
- smartsheet/models/paginated_children_result.py +80 -0
- smartsheet/models/predecessor.py +102 -0
- smartsheet/models/predecessor_list.py +49 -0
- smartsheet/models/primitive_object_value.py +59 -0
- smartsheet/models/profile_image.py +72 -0
- smartsheet/models/project_settings.py +89 -0
- smartsheet/models/recipient.py +63 -0
- smartsheet/models/report.py +90 -0
- smartsheet/models/report_cell.py +59 -0
- smartsheet/models/report_column.py +67 -0
- smartsheet/models/report_publish.py +95 -0
- smartsheet/models/report_row.py +68 -0
- smartsheet/models/report_widget_content.py +78 -0
- smartsheet/models/result.py +105 -0
- smartsheet/models/row.py +336 -0
- smartsheet/models/row_email.py +83 -0
- smartsheet/models/row_mapping.py +77 -0
- smartsheet/models/schedule.py +140 -0
- smartsheet/models/scope.py +70 -0
- smartsheet/models/search_result.py +67 -0
- smartsheet/models/search_result_item.py +150 -0
- smartsheet/models/selection_range.py +86 -0
- smartsheet/models/sent_update_request.py +172 -0
- smartsheet/models/server_info.py +67 -0
- smartsheet/models/share.py +183 -0
- smartsheet/models/sheet.py +462 -0
- smartsheet/models/sheet_email.py +81 -0
- smartsheet/models/sheet_filter.py +106 -0
- smartsheet/models/sheet_filter_details.py +76 -0
- smartsheet/models/sheet_publish.py +184 -0
- smartsheet/models/sheet_summary.py +59 -0
- smartsheet/models/sheet_user_permissions.py +58 -0
- smartsheet/models/sheet_user_settings.py +72 -0
- smartsheet/models/shortcut_data_item.py +102 -0
- smartsheet/models/shortcut_widget_content.py +61 -0
- smartsheet/models/sight.py +175 -0
- smartsheet/models/sight_publish.py +77 -0
- smartsheet/models/sort_criterion.py +64 -0
- smartsheet/models/sort_specifier.py +55 -0
- smartsheet/models/source.py +83 -0
- smartsheet/models/string_object_value.py +38 -0
- smartsheet/models/summary_field.py +256 -0
- smartsheet/models/template.py +171 -0
- smartsheet/models/title_rich_text_widget_content.py +68 -0
- smartsheet/models/token_paginated_result.py +79 -0
- smartsheet/models/update_request.py +110 -0
- smartsheet/models/user.py +58 -0
- smartsheet/models/user_model.py +280 -0
- smartsheet/models/user_plan.py +77 -0
- smartsheet/models/user_profile.py +89 -0
- smartsheet/models/version.py +57 -0
- smartsheet/models/web_content_widget_content.py +60 -0
- smartsheet/models/webhook.py +219 -0
- smartsheet/models/webhook_secret.py +58 -0
- smartsheet/models/webhook_stats.py +76 -0
- smartsheet/models/webhook_subscope.py +50 -0
- smartsheet/models/widget.py +211 -0
- smartsheet/models/widget_content.py +52 -0
- smartsheet/models/widget_hyperlink.py +74 -0
- smartsheet/models/workspace.py +185 -0
- smartsheet/object_value.py +72 -0
- smartsheet/passthrough.py +127 -0
- smartsheet/reports.py +382 -0
- smartsheet/search.py +100 -0
- smartsheet/server.py +48 -0
- smartsheet/session.py +70 -0
- smartsheet/sharing.py +163 -0
- smartsheet/sheets.py +2062 -0
- smartsheet/sights.py +370 -0
- smartsheet/smartsheet.py +684 -0
- smartsheet/templates.py +87 -0
- smartsheet/token.py +128 -0
- smartsheet/types.py +323 -0
- smartsheet/users.py +490 -0
- smartsheet/util.py +199 -0
- smartsheet/version.py +34 -0
- smartsheet/webhooks.py +161 -0
- smartsheet/workspaces.py +647 -0
- smartsheet_python_sdk-3.5.5.dist-info/METADATA +120 -0
- smartsheet_python_sdk-3.5.5.dist-info/RECORD +195 -0
- smartsheet_python_sdk-3.5.5.dist-info/WHEEL +5 -0
- smartsheet_python_sdk-3.5.5.dist-info/licenses/LICENSE.md +201 -0
- smartsheet_python_sdk-3.5.5.dist-info/licenses/NOTICE +10 -0
- smartsheet_python_sdk-3.5.5.dist-info/top_level.txt +1 -0
smartsheet/smartsheet.py
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
# pylint: disable=C0111,R0902,R0913,W0614,C0302,W0401,R0912,W0611,C0301,W0621,W0404,R1720,W0702,W0613
|
|
2
|
+
# Smartsheet Python SDK.
|
|
3
|
+
#
|
|
4
|
+
# Copyright 2016 Smartsheet.com, Inc.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License"): you may
|
|
7
|
+
# not use this file except in compliance with the License. You may obtain
|
|
8
|
+
# a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
14
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
15
|
+
# License for the specific language governing permissions and limitations
|
|
16
|
+
# under the License.
|
|
17
|
+
|
|
18
|
+
from __future__ import absolute_import
|
|
19
|
+
|
|
20
|
+
import importlib
|
|
21
|
+
import inspect
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import logging.config
|
|
25
|
+
import os
|
|
26
|
+
import random
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
import requests
|
|
32
|
+
import six
|
|
33
|
+
|
|
34
|
+
from . import __api_base__, __version__, models
|
|
35
|
+
from .exceptions import ApiError, HttpError, UnexpectedRequestError
|
|
36
|
+
from .models import Error, ErrorResult
|
|
37
|
+
from .session import pinned_session
|
|
38
|
+
from .util import is_multipart, serialize
|
|
39
|
+
|
|
40
|
+
__all__ = ("Smartsheet", "fresh_operation", "AbstractUserCalcBackoff")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def fresh_operation(op_id):
|
|
44
|
+
"""Create a default operation object."""
|
|
45
|
+
operation = {
|
|
46
|
+
"path": "",
|
|
47
|
+
"headers": {},
|
|
48
|
+
"header_params": {},
|
|
49
|
+
"path_params": {},
|
|
50
|
+
"query_params": {},
|
|
51
|
+
"params": {},
|
|
52
|
+
"files": None,
|
|
53
|
+
"form_data": None,
|
|
54
|
+
"json": None,
|
|
55
|
+
"id": op_id,
|
|
56
|
+
"dl_path": None,
|
|
57
|
+
"auth_settings": "access_token",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return operation
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def setup_logging():
|
|
64
|
+
"""Allow for easy insight into SDK behavior."""
|
|
65
|
+
log_env = os.environ.get("LOG_CFG", None)
|
|
66
|
+
if log_env is not None:
|
|
67
|
+
if os.path.exists(log_env):
|
|
68
|
+
import json
|
|
69
|
+
|
|
70
|
+
with open(log_env, "rt", encoding="utf8") as config_file:
|
|
71
|
+
config = json.load(config_file)
|
|
72
|
+
logging.config.dictConfig(config)
|
|
73
|
+
else:
|
|
74
|
+
if log_env.upper() == "DEBUG":
|
|
75
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
76
|
+
elif log_env.upper() == "INFO":
|
|
77
|
+
logging.basicConfig(level=logging.INFO)
|
|
78
|
+
# we will do most of the logging here so turn down the requests library
|
|
79
|
+
logging.getLogger("requests").setLevel(logging.WARNING)
|
|
80
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class AbstractUserCalcBackoff:
|
|
84
|
+
def calc_backoff(self, previous_attempts, total_elapsed_time, error_result):
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
f"Class {self.__class__.__name__} doesn't implement calc_backoff()"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DefaultCalcBackoff(AbstractUserCalcBackoff):
|
|
91
|
+
def __init__(self, max_retry_time):
|
|
92
|
+
self._max_retry_time = max_retry_time
|
|
93
|
+
|
|
94
|
+
def calc_backoff(self, previous_attempts, total_elapsed_time, error_result):
|
|
95
|
+
"""
|
|
96
|
+
Default back off calculator on retry.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
previous_attempts(int) : number of previous retry attempts
|
|
100
|
+
total_elapsed_time(float): elapsed time in seconds
|
|
101
|
+
error_result(Smartsheet.models.ErrorResult): ErrorResult object for previous API attempt
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
(float) Back off time in seconds (any negative number will drop out of retry loop)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Use exponential backoff
|
|
108
|
+
backoff = (2**previous_attempts) + random.random()
|
|
109
|
+
|
|
110
|
+
if (total_elapsed_time + backoff) > self._max_retry_time:
|
|
111
|
+
return -1
|
|
112
|
+
|
|
113
|
+
return backoff
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Smartsheet:
|
|
117
|
+
"""Use this to make requests to the Smartsheet API."""
|
|
118
|
+
|
|
119
|
+
models = models
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
access_token=None,
|
|
124
|
+
max_connections=8,
|
|
125
|
+
user_agent=None,
|
|
126
|
+
max_retry_time=30,
|
|
127
|
+
proxies=None,
|
|
128
|
+
api_base=__api_base__,
|
|
129
|
+
):
|
|
130
|
+
"""
|
|
131
|
+
Set up base client object.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
access_token (str): Access Token for making client
|
|
135
|
+
requests. May also be set as an env variable in
|
|
136
|
+
SMARTSHEET_ACCESS_TOKEN. (required)
|
|
137
|
+
max_connections (int): Maximum connection pool size.
|
|
138
|
+
max_retry_time (int or AbstractUserCalcBackoff): user provided maximum
|
|
139
|
+
elapsed time or AbstractUserCalcBackoff class for user back off calculation on retry.
|
|
140
|
+
user_agent (str): The user agent to use when making requests. This
|
|
141
|
+
helps us identify requests coming from your application. We
|
|
142
|
+
recommend you use the format "AppName/Version". If set, we
|
|
143
|
+
append "/SmartsheetPythonSDK/__version__" to the user_agent.
|
|
144
|
+
proxies (dict): See the `requests module
|
|
145
|
+
<http://docs.python-requests.org/en/latest/user/advanced/#proxies>`_
|
|
146
|
+
for more details.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
self.raise_exceptions = False
|
|
150
|
+
if access_token:
|
|
151
|
+
self._access_token = access_token
|
|
152
|
+
else:
|
|
153
|
+
self._access_token = os.environ.get("SMARTSHEET_ACCESS_TOKEN", None)
|
|
154
|
+
|
|
155
|
+
if self._access_token is None:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
"Access Token must be set in the environment "
|
|
158
|
+
"or passed to smartsheet.Smartsheet() "
|
|
159
|
+
"as a parameter."
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if isinstance(max_retry_time, AbstractUserCalcBackoff):
|
|
163
|
+
self._user_calc_backoff = max_retry_time
|
|
164
|
+
else:
|
|
165
|
+
self._user_calc_backoff = DefaultCalcBackoff(max_retry_time)
|
|
166
|
+
|
|
167
|
+
self._session = pinned_session(pool_maxsize=max_connections)
|
|
168
|
+
if proxies:
|
|
169
|
+
self._session.proxies = proxies
|
|
170
|
+
|
|
171
|
+
base_user_agent = "SmartsheetPythonSDK/" + __version__
|
|
172
|
+
if user_agent:
|
|
173
|
+
self._user_agent = f"{base_user_agent}/{user_agent}"
|
|
174
|
+
else:
|
|
175
|
+
caller = "__unknown__"
|
|
176
|
+
stack = inspect.stack()
|
|
177
|
+
module = inspect.getmodule(stack[-1][0])
|
|
178
|
+
if module is not None:
|
|
179
|
+
caller = inspect.getmodule(stack[-1][0]).__name__
|
|
180
|
+
self._user_agent = f"{base_user_agent}/{caller}"
|
|
181
|
+
|
|
182
|
+
self._log = logging.getLogger(__name__)
|
|
183
|
+
setup_logging()
|
|
184
|
+
self._url = ""
|
|
185
|
+
self._api_base = api_base
|
|
186
|
+
self._assume_user = None
|
|
187
|
+
self._test_scenario_name = None
|
|
188
|
+
self._wiremock_test_name = None
|
|
189
|
+
self._wiremock_request_id = None
|
|
190
|
+
self._change_agent = None
|
|
191
|
+
|
|
192
|
+
def assume_user(self, email=None):
|
|
193
|
+
"""Assume identity of specified user.
|
|
194
|
+
|
|
195
|
+
As an administrator, you can assume the identity of any user
|
|
196
|
+
in your organization.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
email (str): Valid email address of user whose identity
|
|
200
|
+
should be assumed.
|
|
201
|
+
"""
|
|
202
|
+
if email is None:
|
|
203
|
+
self._assume_user = None
|
|
204
|
+
else:
|
|
205
|
+
# email = email.replace('@', '%40')
|
|
206
|
+
self._assume_user = six.moves.urllib.parse.quote(email)
|
|
207
|
+
|
|
208
|
+
def errors_as_exceptions(self, preference=True):
|
|
209
|
+
"""
|
|
210
|
+
Set preference on whether or not to raise exceptions on API errors.
|
|
211
|
+
When preference is True, exceptions will be raised. When False,
|
|
212
|
+
instances of the Error data type will be returned.
|
|
213
|
+
|
|
214
|
+
The property `raise_exceptions` defaults to False. Therefore, this
|
|
215
|
+
method should only be called if exceptions *should* be raised.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
preference (bool): Flag indicating whether errors should be raised
|
|
219
|
+
as exceptions.
|
|
220
|
+
"""
|
|
221
|
+
self.raise_exceptions = preference
|
|
222
|
+
|
|
223
|
+
def as_test_scenario(self, name):
|
|
224
|
+
"""
|
|
225
|
+
Identify requests made with this client as a test scenario.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
name (str): The name of the test scenario.
|
|
229
|
+
"""
|
|
230
|
+
self._test_scenario_name = name
|
|
231
|
+
|
|
232
|
+
def with_wiremock_test_case(self, test_name: str, request_id: str):
|
|
233
|
+
"""
|
|
234
|
+
Configure client with x-test-name and x-request-id headers.
|
|
235
|
+
Used for wiremock test cases.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
test_name (str): The name of the wiremock test case.
|
|
239
|
+
request_id (str): The unique request ID for this test scenario.
|
|
240
|
+
"""
|
|
241
|
+
self._wiremock_test_name = test_name
|
|
242
|
+
self._wiremock_request_id = request_id
|
|
243
|
+
|
|
244
|
+
def with_change_agent(self, change_agent):
|
|
245
|
+
"""
|
|
246
|
+
Request headers will contain the 'Smartsheet-Change-Agent' header value
|
|
247
|
+
|
|
248
|
+
Agrs:
|
|
249
|
+
change_agent: (str) the name of this change agent
|
|
250
|
+
"""
|
|
251
|
+
self._change_agent = change_agent
|
|
252
|
+
|
|
253
|
+
def request(self, prepped_request, expected, operation):
|
|
254
|
+
"""
|
|
255
|
+
Make a request from the Smartsheet API.
|
|
256
|
+
|
|
257
|
+
Make a request from the Smartsheet API and validate that inputs
|
|
258
|
+
and outputs are as expected. The API response is converted from
|
|
259
|
+
raw wire messages to a native objects based on the value of `expected`.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
prepped_request (Request): Prepared request for the operation.
|
|
263
|
+
expected (list|str): The expected response data type.
|
|
264
|
+
operation(dict): Dictionary containing operation details
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
The API operation result object.
|
|
268
|
+
"""
|
|
269
|
+
res = self.request_with_retry(prepped_request, operation)
|
|
270
|
+
native = res.native(expected)
|
|
271
|
+
|
|
272
|
+
if not self.raise_exceptions:
|
|
273
|
+
return native
|
|
274
|
+
|
|
275
|
+
if isinstance(native, self.models.Error):
|
|
276
|
+
the_ex = getattr(sys.modules[__name__], native.result.name)
|
|
277
|
+
raise the_ex(native, str(native.result.code) + ": " + native.result.message)
|
|
278
|
+
else:
|
|
279
|
+
return native
|
|
280
|
+
|
|
281
|
+
def _log_request(self, operation, response):
|
|
282
|
+
"""
|
|
283
|
+
Wrapper for request/response logger
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
operation (dict):
|
|
287
|
+
response (Response):
|
|
288
|
+
"""
|
|
289
|
+
# request
|
|
290
|
+
self._log.info(
|
|
291
|
+
'{"request": {"command": "%s %s"}}',
|
|
292
|
+
response.request.method,
|
|
293
|
+
response.request.url,
|
|
294
|
+
)
|
|
295
|
+
if response.request.body is not None:
|
|
296
|
+
body_dumps = f'"<< {response.request.headers.get("Content-Type")} content type suppressed >>"'
|
|
297
|
+
if is_multipart(response.request):
|
|
298
|
+
body_dumps = '"<< multipart body suppressed >>"'
|
|
299
|
+
elif response.request.headers.get("Content-Type") is not None and "application/json" in response.request.headers.get("Content-Type"):
|
|
300
|
+
body = response.request.body.decode("utf8")
|
|
301
|
+
body_dumps = json.dumps(json.loads(body), sort_keys=True)
|
|
302
|
+
self._log.debug('{"requestBody": %s}', body_dumps)
|
|
303
|
+
# response
|
|
304
|
+
content_dumps = f'"<< {response.headers.get("Content-Type")} content type suppressed >>"'
|
|
305
|
+
if response.headers.get("Content-Type") is not None and "application/json" in response.headers.get("Content-Type"):
|
|
306
|
+
content = response.content.decode("utf8")
|
|
307
|
+
content_dumps = json.dumps(json.loads(content), sort_keys=True)
|
|
308
|
+
if 200 <= response.status_code <= 299:
|
|
309
|
+
if operation["dl_path"] is None:
|
|
310
|
+
self._log.debug(
|
|
311
|
+
'{"response": {"statusCode": %d, "reason": "%s", "content": %s}}',
|
|
312
|
+
response.status_code,
|
|
313
|
+
response.reason,
|
|
314
|
+
content_dumps,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
self._log.debug(
|
|
318
|
+
'{"response": {"statusCode": %d, "reason": "%s"}}',
|
|
319
|
+
response.status_code,
|
|
320
|
+
response.reason,
|
|
321
|
+
)
|
|
322
|
+
else:
|
|
323
|
+
self._log.error(
|
|
324
|
+
'{"response": {"statusCode": %d, "reason": "%s", "content": %s}}',
|
|
325
|
+
response.status_code,
|
|
326
|
+
response.reason,
|
|
327
|
+
content_dumps,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def _request(self, prepped_request, operation):
|
|
331
|
+
"""
|
|
332
|
+
Wrapper for the low-level Request action.
|
|
333
|
+
|
|
334
|
+
Only low-level error handling.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
prepped_request (Request): Prepared request for the operation.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Operation Result object.
|
|
341
|
+
"""
|
|
342
|
+
stream = False
|
|
343
|
+
if operation["dl_path"]:
|
|
344
|
+
stream = True
|
|
345
|
+
try:
|
|
346
|
+
res = self._session.send(prepped_request, stream=stream)
|
|
347
|
+
self._log_request(operation, res)
|
|
348
|
+
except requests.exceptions.SSLError as rex:
|
|
349
|
+
raise HttpError(rex, "SSL handshake error, old CA bundle or old OpenSSL?") from rex
|
|
350
|
+
except requests.exceptions.RequestException as rex:
|
|
351
|
+
raise UnexpectedRequestError(rex.request, rex.response) from rex
|
|
352
|
+
|
|
353
|
+
if 200 <= res.status_code <= 299:
|
|
354
|
+
return OperationResult(res.text, res, self, operation)
|
|
355
|
+
else:
|
|
356
|
+
return OperationErrorResult(res.text, res)
|
|
357
|
+
|
|
358
|
+
def request_with_retry(self, prepped_request, operation):
|
|
359
|
+
"""
|
|
360
|
+
Perform the request with retry.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
prepped_request (Request): A prepared request object for
|
|
364
|
+
the operation.
|
|
365
|
+
operation(dict): Dictionary containing operation details
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Operation Result object.
|
|
369
|
+
"""
|
|
370
|
+
attempt = 0
|
|
371
|
+
start_time = time.time()
|
|
372
|
+
# Make a copy of the request as the access token will be redacted on response prior to logging
|
|
373
|
+
pre_redact_request = prepped_request.copy()
|
|
374
|
+
while True:
|
|
375
|
+
result = self._request(prepped_request, operation)
|
|
376
|
+
if isinstance(result, OperationErrorResult):
|
|
377
|
+
native = result.native("Error")
|
|
378
|
+
if native.result.should_retry:
|
|
379
|
+
attempt += 1
|
|
380
|
+
elapsed_time = time.time() - start_time
|
|
381
|
+
backoff = self._user_calc_backoff.calc_backoff(
|
|
382
|
+
attempt, elapsed_time, native.result
|
|
383
|
+
)
|
|
384
|
+
if backoff < 0:
|
|
385
|
+
break
|
|
386
|
+
self._log.info(
|
|
387
|
+
"HttpError status_code=%s: Retrying in %.1f seconds",
|
|
388
|
+
native.result.status_code,
|
|
389
|
+
backoff,
|
|
390
|
+
)
|
|
391
|
+
time.sleep(backoff)
|
|
392
|
+
# restore un-redacted request prior to retry
|
|
393
|
+
prepped_request = pre_redact_request.copy()
|
|
394
|
+
else:
|
|
395
|
+
break
|
|
396
|
+
else:
|
|
397
|
+
break
|
|
398
|
+
return result
|
|
399
|
+
|
|
400
|
+
def prepare_request(self, _op):
|
|
401
|
+
"""Generate a Requests prepared request object."""
|
|
402
|
+
if _op["header_params"]:
|
|
403
|
+
_op["headers"].update(_op["header_params"])
|
|
404
|
+
|
|
405
|
+
if _op["path_params"]:
|
|
406
|
+
for key, val in six.iteritems(_op["path_params"]):
|
|
407
|
+
_op["path"] = _op["path"].replace("{" + key + "}", str(val))
|
|
408
|
+
|
|
409
|
+
if _op["json"]:
|
|
410
|
+
_op["json"] = serialize(_op["json"])
|
|
411
|
+
|
|
412
|
+
if _op["query_params"]:
|
|
413
|
+
for key, val in six.iteritems(_op["query_params"]):
|
|
414
|
+
if isinstance(val, list):
|
|
415
|
+
val = ",".join([str(num) for num in val])
|
|
416
|
+
_op["query_params"][key] = val
|
|
417
|
+
|
|
418
|
+
req = requests.Request(
|
|
419
|
+
_op["method"],
|
|
420
|
+
self._api_base + _op["path"],
|
|
421
|
+
headers=_op["headers"],
|
|
422
|
+
params=_op["query_params"],
|
|
423
|
+
files=_op["files"],
|
|
424
|
+
data=_op["form_data"],
|
|
425
|
+
json=_op["json"],
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
prepped_request = self._session.prepare_request(req)
|
|
430
|
+
except TypeError as ex:
|
|
431
|
+
# JSON not serializable for some reason
|
|
432
|
+
self._log.error(ex)
|
|
433
|
+
|
|
434
|
+
prepped_request.headers.update({"User-Agent": self._user_agent})
|
|
435
|
+
if _op["auth_settings"] is not None:
|
|
436
|
+
auth_header_val = "Bearer " + self._access_token
|
|
437
|
+
prepped_request.headers.update({"Authorization": auth_header_val})
|
|
438
|
+
|
|
439
|
+
if self._assume_user is not None:
|
|
440
|
+
prepped_request.headers.update({"Assume-User": self._assume_user})
|
|
441
|
+
else:
|
|
442
|
+
try:
|
|
443
|
+
del prepped_request.headers["Assume-User"]
|
|
444
|
+
except KeyError:
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
if self._test_scenario_name is not None:
|
|
448
|
+
prepped_request.headers.update({"Api-Scenario": self._test_scenario_name})
|
|
449
|
+
else:
|
|
450
|
+
try:
|
|
451
|
+
del prepped_request.headers["Api-Scenario"]
|
|
452
|
+
except KeyError:
|
|
453
|
+
pass
|
|
454
|
+
if self._wiremock_test_name is not None and self._wiremock_request_id is not None:
|
|
455
|
+
prepped_request.headers["X-Test-Name"] = self._wiremock_test_name
|
|
456
|
+
prepped_request.headers["X-Request-ID"] = self._wiremock_request_id
|
|
457
|
+
|
|
458
|
+
if self._change_agent is not None:
|
|
459
|
+
prepped_request.headers.update(
|
|
460
|
+
{"Smartsheet-Change-Agent": self._change_agent}
|
|
461
|
+
)
|
|
462
|
+
else:
|
|
463
|
+
try:
|
|
464
|
+
del prepped_request.headers["Smartsheet-Change-Agent"]
|
|
465
|
+
except KeyError:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
return prepped_request
|
|
469
|
+
|
|
470
|
+
def __getattr__(self, name):
|
|
471
|
+
"""
|
|
472
|
+
Handle sub-class instantiation.
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
name (str): Name of smartsheet to instantiate.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Instance of named class.
|
|
479
|
+
"""
|
|
480
|
+
try:
|
|
481
|
+
# api class first
|
|
482
|
+
class_ = getattr(
|
|
483
|
+
importlib.import_module(__package__ + "." + name.lower()), name
|
|
484
|
+
)
|
|
485
|
+
return class_(self)
|
|
486
|
+
except ImportError:
|
|
487
|
+
# model class next:
|
|
488
|
+
try:
|
|
489
|
+
class_ = getattr(importlib.import_module(name.lower()), name)
|
|
490
|
+
return class_()
|
|
491
|
+
except ImportError:
|
|
492
|
+
self._log.error(
|
|
493
|
+
"ImportError! Could not load api or model class %s", name
|
|
494
|
+
)
|
|
495
|
+
return name
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class OperationResult:
|
|
499
|
+
"""The successful result of a call to an operation."""
|
|
500
|
+
|
|
501
|
+
def __init__(self, op_result, resp=None, base_obj=None, operation=None):
|
|
502
|
+
"""Initialize OperationResult.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
op_result (str): The result of an operation not including
|
|
506
|
+
the binary payload portion, if one exists. Must be
|
|
507
|
+
a JSON string.
|
|
508
|
+
resp (requests.models.Response): A raw HTTP response.
|
|
509
|
+
It will be used to stream the binary-body payload of the
|
|
510
|
+
response.
|
|
511
|
+
base_obj (smartsheet.Smartsheet): Configured core object
|
|
512
|
+
for subsequent convenience method requests.
|
|
513
|
+
"""
|
|
514
|
+
assert isinstance(
|
|
515
|
+
op_result, six.string_types
|
|
516
|
+
), f"op_result: expected string, got {type(op_result)!r}"
|
|
517
|
+
if resp is not None:
|
|
518
|
+
assert isinstance(
|
|
519
|
+
resp, requests.models.Response
|
|
520
|
+
), f"resp: expected requests.models.Response, got {type(resp)!r}"
|
|
521
|
+
self._base = base_obj
|
|
522
|
+
self.op_result = op_result
|
|
523
|
+
self.resp = resp
|
|
524
|
+
self.dynamic_data_types = []
|
|
525
|
+
self.operation = operation
|
|
526
|
+
|
|
527
|
+
def native(self, expected):
|
|
528
|
+
"""Initialize expected result object and return it.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
expected (list): Expected objects to return.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Operation Result object or Operation Error Result object.
|
|
535
|
+
"""
|
|
536
|
+
try:
|
|
537
|
+
if expected != "DownloadedFile":
|
|
538
|
+
data = self.resp.json()
|
|
539
|
+
else:
|
|
540
|
+
# default
|
|
541
|
+
filename = ["download"]
|
|
542
|
+
|
|
543
|
+
if "Content-Disposition" in self.resp.headers:
|
|
544
|
+
# use the provided filename
|
|
545
|
+
filename = re.findall(
|
|
546
|
+
'filename="(.+)";', self.resp.headers["Content-Disposition"]
|
|
547
|
+
)
|
|
548
|
+
else:
|
|
549
|
+
content_type = self.resp.headers.get("Content-Type", "")
|
|
550
|
+
if content_type in [
|
|
551
|
+
"application/vnd.ms-excel",
|
|
552
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
553
|
+
]:
|
|
554
|
+
filename[0] += ".xlsx"
|
|
555
|
+
elif content_type == "application/pdf":
|
|
556
|
+
filename[0] += ".pdf"
|
|
557
|
+
elif content_type == "text/csv":
|
|
558
|
+
filename[0] += ".csv"
|
|
559
|
+
|
|
560
|
+
data = {
|
|
561
|
+
"resultCode": 0,
|
|
562
|
+
"message": "SUCCESS",
|
|
563
|
+
"resp": self.resp,
|
|
564
|
+
"filename": filename[0],
|
|
565
|
+
"downloadDirectory": self.operation["dl_path"],
|
|
566
|
+
}
|
|
567
|
+
except ValueError:
|
|
568
|
+
return OperationErrorResult(self.op_result, self.resp)
|
|
569
|
+
|
|
570
|
+
if isinstance(expected, list):
|
|
571
|
+
klass = expected[0]
|
|
572
|
+
dynamic_type = expected[1]
|
|
573
|
+
class_ = getattr(importlib.import_module("smartsheet.models"), klass)
|
|
574
|
+
obj = class_(data, dynamic_type, self._base)
|
|
575
|
+
if hasattr(obj, "request_response"):
|
|
576
|
+
obj.request_response = self.resp
|
|
577
|
+
|
|
578
|
+
return obj
|
|
579
|
+
|
|
580
|
+
class_ = getattr(importlib.import_module("smartsheet.models"), expected)
|
|
581
|
+
|
|
582
|
+
obj = class_(data, self._base)
|
|
583
|
+
if hasattr(obj, "request_response"):
|
|
584
|
+
obj.request_response = self.resp
|
|
585
|
+
|
|
586
|
+
return obj
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class OperationErrorResult:
|
|
590
|
+
"""The error result of a call to an operation."""
|
|
591
|
+
|
|
592
|
+
error_lookup = {
|
|
593
|
+
0: {
|
|
594
|
+
"name": "ApiError",
|
|
595
|
+
"recommendation": "Do not retry without fixing the problem. ",
|
|
596
|
+
"should_retry": False,
|
|
597
|
+
},
|
|
598
|
+
4001: {
|
|
599
|
+
"name": "SystemMaintenanceError",
|
|
600
|
+
"recommendation": (
|
|
601
|
+
"Retry using exponential backoff. Hint: "
|
|
602
|
+
"Wait time between retries should measure "
|
|
603
|
+
"in minutes (not seconds)."
|
|
604
|
+
),
|
|
605
|
+
"should_retry": True,
|
|
606
|
+
},
|
|
607
|
+
4002: {
|
|
608
|
+
"name": "ServerTimeoutExceededError",
|
|
609
|
+
"recommendation": "Retry using exponential backoff.",
|
|
610
|
+
"should_retry": True,
|
|
611
|
+
},
|
|
612
|
+
4003: {
|
|
613
|
+
"name": "RateLimitExceededError",
|
|
614
|
+
"recommendation": (
|
|
615
|
+
"Retry using exponential backoff. Hint: "
|
|
616
|
+
"Reduce the rate at which you are sending "
|
|
617
|
+
"requests."
|
|
618
|
+
),
|
|
619
|
+
"should_retry": True,
|
|
620
|
+
},
|
|
621
|
+
4004: {
|
|
622
|
+
"name": "UnexpectedErrorShouldRetryError",
|
|
623
|
+
"recommendation": "Retry using exponential backoff.",
|
|
624
|
+
"should_retry": True,
|
|
625
|
+
},
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
def __init__(self, op_result, resp):
|
|
629
|
+
"""
|
|
630
|
+
Initialize OperationErrorResult.
|
|
631
|
+
|
|
632
|
+
Args:
|
|
633
|
+
op_result (str): The result of an operation not including the
|
|
634
|
+
binary payload portion, if one exists.
|
|
635
|
+
resp (requests.models.Response): A raw HTTP response.
|
|
636
|
+
"""
|
|
637
|
+
self.op_result = op_result
|
|
638
|
+
self.resp = resp
|
|
639
|
+
self._log = logging.getLogger(__name__)
|
|
640
|
+
|
|
641
|
+
def native(self, expected):
|
|
642
|
+
"""
|
|
643
|
+
Sadly, we won't be returning what was expected.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
expected (list): Dashed expectations
|
|
647
|
+
"""
|
|
648
|
+
# look up name of the error
|
|
649
|
+
error_payload = {}
|
|
650
|
+
try:
|
|
651
|
+
error_payload = self.resp.json()
|
|
652
|
+
except json.JSONDecodeError:
|
|
653
|
+
# Do not fail if the response is not JSON
|
|
654
|
+
pass
|
|
655
|
+
error_code = error_payload.get("errorCode", 0)
|
|
656
|
+
try:
|
|
657
|
+
error_name = OperationErrorResult.error_lookup[error_code]["name"]
|
|
658
|
+
recommendation = OperationErrorResult.error_lookup[error_code][
|
|
659
|
+
"recommendation"
|
|
660
|
+
]
|
|
661
|
+
should_retry = OperationErrorResult.error_lookup[error_code]["should_retry"]
|
|
662
|
+
except:
|
|
663
|
+
# If error_code is present in the response but not in the lookup, default to ApiError
|
|
664
|
+
error_name = OperationErrorResult.error_lookup[0]["name"]
|
|
665
|
+
recommendation = OperationErrorResult.error_lookup[0]["recommendation"]
|
|
666
|
+
should_retry = OperationErrorResult.error_lookup[0]["should_retry"]
|
|
667
|
+
|
|
668
|
+
obj = Error(
|
|
669
|
+
{
|
|
670
|
+
"result": ErrorResult(
|
|
671
|
+
{
|
|
672
|
+
"name": error_name,
|
|
673
|
+
"status_code": self.resp.status_code,
|
|
674
|
+
"code": error_code,
|
|
675
|
+
"message": error_payload.get("message"),
|
|
676
|
+
"ref_id": error_payload.get("refId"),
|
|
677
|
+
"recommendation": recommendation,
|
|
678
|
+
"should_retry": should_retry,
|
|
679
|
+
}
|
|
680
|
+
),
|
|
681
|
+
"request_response": self.resp,
|
|
682
|
+
}
|
|
683
|
+
)
|
|
684
|
+
return obj
|