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.
Files changed (195) hide show
  1. smartsheet/__init__.py +37 -0
  2. smartsheet/attachments.py +565 -0
  3. smartsheet/cells.py +164 -0
  4. smartsheet/contacts.py +78 -0
  5. smartsheet/discussions.py +411 -0
  6. smartsheet/events.py +79 -0
  7. smartsheet/exceptions.py +130 -0
  8. smartsheet/favorites.py +116 -0
  9. smartsheet/folders.py +438 -0
  10. smartsheet/groups.py +186 -0
  11. smartsheet/home.py +180 -0
  12. smartsheet/images.py +61 -0
  13. smartsheet/models/__init__.py +126 -0
  14. smartsheet/models/access_token.py +95 -0
  15. smartsheet/models/account.py +77 -0
  16. smartsheet/models/alternate_email.py +88 -0
  17. smartsheet/models/asset_share.py +165 -0
  18. smartsheet/models/asset_shares_paginated_result.py +84 -0
  19. smartsheet/models/attachment.py +181 -0
  20. smartsheet/models/auto_number_format.py +81 -0
  21. smartsheet/models/automation_action.py +162 -0
  22. smartsheet/models/automation_rule.py +164 -0
  23. smartsheet/models/boolean_object_value.py +38 -0
  24. smartsheet/models/bulk_item_failure.py +77 -0
  25. smartsheet/models/bulk_item_result.py +111 -0
  26. smartsheet/models/cell.py +193 -0
  27. smartsheet/models/cell_data_item.py +152 -0
  28. smartsheet/models/cell_history.py +67 -0
  29. smartsheet/models/cell_link.py +91 -0
  30. smartsheet/models/cell_link_widget_content.py +101 -0
  31. smartsheet/models/chart_widget_content.py +124 -0
  32. smartsheet/models/column.py +253 -0
  33. smartsheet/models/comment.py +126 -0
  34. smartsheet/models/contact.py +88 -0
  35. smartsheet/models/contact_object_value.py +59 -0
  36. smartsheet/models/container_destination.py +74 -0
  37. smartsheet/models/copy_or_move_row_destination.py +54 -0
  38. smartsheet/models/copy_or_move_row_directive.py +64 -0
  39. smartsheet/models/copy_or_move_row_result.py +67 -0
  40. smartsheet/models/criteria.py +82 -0
  41. smartsheet/models/cross_sheet_reference.py +134 -0
  42. smartsheet/models/currency.py +64 -0
  43. smartsheet/models/date_object_value.py +56 -0
  44. smartsheet/models/datetime_object_value.py +56 -0
  45. smartsheet/models/discussion.py +183 -0
  46. smartsheet/models/downloaded_file.py +106 -0
  47. smartsheet/models/duration.py +112 -0
  48. smartsheet/models/email.py +82 -0
  49. smartsheet/models/enums/__init__.py +56 -0
  50. smartsheet/models/enums/access_level.py +26 -0
  51. smartsheet/models/enums/asset_type.py +10 -0
  52. smartsheet/models/enums/attachment_parent_type.py +23 -0
  53. smartsheet/models/enums/attachment_sub_type.py +26 -0
  54. smartsheet/models/enums/attachment_type.py +29 -0
  55. smartsheet/models/enums/automation_action_frequency.py +24 -0
  56. smartsheet/models/enums/automation_action_type.py +23 -0
  57. smartsheet/models/enums/automation_rule_disabled_reason.py +27 -0
  58. smartsheet/models/enums/cell_link_status.py +28 -0
  59. smartsheet/models/enums/column_type.py +31 -0
  60. smartsheet/models/enums/criteria_target.py +21 -0
  61. smartsheet/models/enums/cross_sheet_reference_status.py +28 -0
  62. smartsheet/models/enums/currency_code.py +43 -0
  63. smartsheet/models/enums/day_descriptors.py +30 -0
  64. smartsheet/models/enums/day_ordinal.py +25 -0
  65. smartsheet/models/enums/event_action.py +76 -0
  66. smartsheet/models/enums/event_obejct_type.py +34 -0
  67. smartsheet/models/enums/event_source.py +27 -0
  68. smartsheet/models/enums/global_template.py +23 -0
  69. smartsheet/models/enums/operator.py +62 -0
  70. smartsheet/models/enums/paper_type.py +29 -0
  71. smartsheet/models/enums/predecessor_type.py +24 -0
  72. smartsheet/models/enums/publish_accessible_by.py +22 -0
  73. smartsheet/models/enums/schedule_type.py +25 -0
  74. smartsheet/models/enums/seat_type.py +17 -0
  75. smartsheet/models/enums/share_scope.py +22 -0
  76. smartsheet/models/enums/share_type.py +22 -0
  77. smartsheet/models/enums/sheet_email_format.py +23 -0
  78. smartsheet/models/enums/sheet_filter_operator.py +22 -0
  79. smartsheet/models/enums/sheet_filter_type.py +23 -0
  80. smartsheet/models/enums/sort_direction.py +22 -0
  81. smartsheet/models/enums/symbol.py +45 -0
  82. smartsheet/models/enums/system_column_type.py +25 -0
  83. smartsheet/models/enums/update_request_status.py +23 -0
  84. smartsheet/models/enums/user_status.py +23 -0
  85. smartsheet/models/enums/widget_type.py +32 -0
  86. smartsheet/models/error.py +74 -0
  87. smartsheet/models/error_result.py +117 -0
  88. smartsheet/models/event.py +153 -0
  89. smartsheet/models/event_result.py +86 -0
  90. smartsheet/models/explicit_null.py +24 -0
  91. smartsheet/models/favorite.py +81 -0
  92. smartsheet/models/folder.py +177 -0
  93. smartsheet/models/font_family.py +63 -0
  94. smartsheet/models/format_details.py +55 -0
  95. smartsheet/models/format_tables.py +191 -0
  96. smartsheet/models/group.py +134 -0
  97. smartsheet/models/group_member.py +104 -0
  98. smartsheet/models/home.py +110 -0
  99. smartsheet/models/hyperlink.py +81 -0
  100. smartsheet/models/image.py +101 -0
  101. smartsheet/models/image_url.py +91 -0
  102. smartsheet/models/image_url_map.py +68 -0
  103. smartsheet/models/image_widget_content.py +117 -0
  104. smartsheet/models/index_result.py +118 -0
  105. smartsheet/models/json_object.py +59 -0
  106. smartsheet/models/multi_contact_object_value.py +49 -0
  107. smartsheet/models/multi_picklist_object_value.py +48 -0
  108. smartsheet/models/multi_row_email.py +60 -0
  109. smartsheet/models/number_object_value.py +38 -0
  110. smartsheet/models/o_auth_error.py +86 -0
  111. smartsheet/models/object_value.py +130 -0
  112. smartsheet/models/paginated_children_result.py +80 -0
  113. smartsheet/models/predecessor.py +102 -0
  114. smartsheet/models/predecessor_list.py +49 -0
  115. smartsheet/models/primitive_object_value.py +59 -0
  116. smartsheet/models/profile_image.py +72 -0
  117. smartsheet/models/project_settings.py +89 -0
  118. smartsheet/models/recipient.py +63 -0
  119. smartsheet/models/report.py +90 -0
  120. smartsheet/models/report_cell.py +59 -0
  121. smartsheet/models/report_column.py +67 -0
  122. smartsheet/models/report_publish.py +95 -0
  123. smartsheet/models/report_row.py +68 -0
  124. smartsheet/models/report_widget_content.py +78 -0
  125. smartsheet/models/result.py +105 -0
  126. smartsheet/models/row.py +336 -0
  127. smartsheet/models/row_email.py +83 -0
  128. smartsheet/models/row_mapping.py +77 -0
  129. smartsheet/models/schedule.py +140 -0
  130. smartsheet/models/scope.py +70 -0
  131. smartsheet/models/search_result.py +67 -0
  132. smartsheet/models/search_result_item.py +150 -0
  133. smartsheet/models/selection_range.py +86 -0
  134. smartsheet/models/sent_update_request.py +172 -0
  135. smartsheet/models/server_info.py +67 -0
  136. smartsheet/models/share.py +183 -0
  137. smartsheet/models/sheet.py +462 -0
  138. smartsheet/models/sheet_email.py +81 -0
  139. smartsheet/models/sheet_filter.py +106 -0
  140. smartsheet/models/sheet_filter_details.py +76 -0
  141. smartsheet/models/sheet_publish.py +184 -0
  142. smartsheet/models/sheet_summary.py +59 -0
  143. smartsheet/models/sheet_user_permissions.py +58 -0
  144. smartsheet/models/sheet_user_settings.py +72 -0
  145. smartsheet/models/shortcut_data_item.py +102 -0
  146. smartsheet/models/shortcut_widget_content.py +61 -0
  147. smartsheet/models/sight.py +175 -0
  148. smartsheet/models/sight_publish.py +77 -0
  149. smartsheet/models/sort_criterion.py +64 -0
  150. smartsheet/models/sort_specifier.py +55 -0
  151. smartsheet/models/source.py +83 -0
  152. smartsheet/models/string_object_value.py +38 -0
  153. smartsheet/models/summary_field.py +256 -0
  154. smartsheet/models/template.py +171 -0
  155. smartsheet/models/title_rich_text_widget_content.py +68 -0
  156. smartsheet/models/token_paginated_result.py +79 -0
  157. smartsheet/models/update_request.py +110 -0
  158. smartsheet/models/user.py +58 -0
  159. smartsheet/models/user_model.py +280 -0
  160. smartsheet/models/user_plan.py +77 -0
  161. smartsheet/models/user_profile.py +89 -0
  162. smartsheet/models/version.py +57 -0
  163. smartsheet/models/web_content_widget_content.py +60 -0
  164. smartsheet/models/webhook.py +219 -0
  165. smartsheet/models/webhook_secret.py +58 -0
  166. smartsheet/models/webhook_stats.py +76 -0
  167. smartsheet/models/webhook_subscope.py +50 -0
  168. smartsheet/models/widget.py +211 -0
  169. smartsheet/models/widget_content.py +52 -0
  170. smartsheet/models/widget_hyperlink.py +74 -0
  171. smartsheet/models/workspace.py +185 -0
  172. smartsheet/object_value.py +72 -0
  173. smartsheet/passthrough.py +127 -0
  174. smartsheet/reports.py +382 -0
  175. smartsheet/search.py +100 -0
  176. smartsheet/server.py +48 -0
  177. smartsheet/session.py +70 -0
  178. smartsheet/sharing.py +163 -0
  179. smartsheet/sheets.py +2062 -0
  180. smartsheet/sights.py +370 -0
  181. smartsheet/smartsheet.py +684 -0
  182. smartsheet/templates.py +87 -0
  183. smartsheet/token.py +128 -0
  184. smartsheet/types.py +323 -0
  185. smartsheet/users.py +490 -0
  186. smartsheet/util.py +199 -0
  187. smartsheet/version.py +34 -0
  188. smartsheet/webhooks.py +161 -0
  189. smartsheet/workspaces.py +647 -0
  190. smartsheet_python_sdk-3.5.5.dist-info/METADATA +120 -0
  191. smartsheet_python_sdk-3.5.5.dist-info/RECORD +195 -0
  192. smartsheet_python_sdk-3.5.5.dist-info/WHEEL +5 -0
  193. smartsheet_python_sdk-3.5.5.dist-info/licenses/LICENSE.md +201 -0
  194. smartsheet_python_sdk-3.5.5.dist-info/licenses/NOTICE +10 -0
  195. smartsheet_python_sdk-3.5.5.dist-info/top_level.txt +1 -0
@@ -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