SquirroClient 0.1__tar.gz

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 (58) hide show
  1. SquirroClient-0.1/MANIFEST.in +1 -0
  2. SquirroClient-0.1/PKG-INFO +28 -0
  3. SquirroClient-0.1/README +7 -0
  4. SquirroClient-0.1/SquirroClient.egg-info/PKG-INFO +28 -0
  5. SquirroClient-0.1/SquirroClient.egg-info/SOURCES.txt +56 -0
  6. SquirroClient-0.1/SquirroClient.egg-info/dependency_links.txt +1 -0
  7. SquirroClient-0.1/SquirroClient.egg-info/requires.txt +5 -0
  8. SquirroClient-0.1/SquirroClient.egg-info/top_level.txt +1 -0
  9. SquirroClient-0.1/setup.cfg +4 -0
  10. SquirroClient-0.1/setup.py +38 -0
  11. SquirroClient-0.1/squirro_client/__init__.py +11 -0
  12. SquirroClient-0.1/squirro_client/base.py +475 -0
  13. SquirroClient-0.1/squirro_client/document_uploader.py +311 -0
  14. SquirroClient-0.1/squirro_client/exceptions.py +77 -0
  15. SquirroClient-0.1/squirro_client/internal.py +79 -0
  16. SquirroClient-0.1/squirro_client/item_uploader.py +712 -0
  17. SquirroClient-0.1/squirro_client/topic/__init__.py +1963 -0
  18. SquirroClient-0.1/squirro_client/topic/communities.py +316 -0
  19. SquirroClient-0.1/squirro_client/topic/community_subscription.py +135 -0
  20. SquirroClient-0.1/squirro_client/topic/community_types.py +249 -0
  21. SquirroClient-0.1/squirro_client/topic/configuration.py +365 -0
  22. SquirroClient-0.1/squirro_client/topic/contributingrecords.py +132 -0
  23. SquirroClient-0.1/squirro_client/topic/dashboards.py +689 -0
  24. SquirroClient-0.1/squirro_client/topic/email_templates.py +68 -0
  25. SquirroClient-0.1/squirro_client/topic/enrichments.py +176 -0
  26. SquirroClient-0.1/squirro_client/topic/entities.py +90 -0
  27. SquirroClient-0.1/squirro_client/topic/facets.py +202 -0
  28. SquirroClient-0.1/squirro_client/topic/file_upload.py +72 -0
  29. SquirroClient-0.1/squirro_client/topic/globaltemp.py +58 -0
  30. SquirroClient-0.1/squirro_client/topic/guidefiles.py +81 -0
  31. SquirroClient-0.1/squirro_client/topic/machinelearning.py +453 -0
  32. SquirroClient-0.1/squirro_client/topic/ml_candidate_set.py +112 -0
  33. SquirroClient-0.1/squirro_client/topic/ml_groundtruth.py +497 -0
  34. SquirroClient-0.1/squirro_client/topic/ml_model.py +217 -0
  35. SquirroClient-0.1/squirro_client/topic/ml_publish.py +201 -0
  36. SquirroClient-0.1/squirro_client/topic/ml_sentence_splitter.py +29 -0
  37. SquirroClient-0.1/squirro_client/topic/ml_template.py +86 -0
  38. SquirroClient-0.1/squirro_client/topic/ml_user_feedback.py +188 -0
  39. SquirroClient-0.1/squirro_client/topic/notes.py +196 -0
  40. SquirroClient-0.1/squirro_client/topic/objects.py +285 -0
  41. SquirroClient-0.1/squirro_client/topic/pipeline_sections.py +24 -0
  42. SquirroClient-0.1/squirro_client/topic/pipeline_status.py +68 -0
  43. SquirroClient-0.1/squirro_client/topic/pipeline_workflows.py +423 -0
  44. SquirroClient-0.1/squirro_client/topic/project_translations.py +131 -0
  45. SquirroClient-0.1/squirro_client/topic/projects.py +881 -0
  46. SquirroClient-0.1/squirro_client/topic/savedsearches.py +235 -0
  47. SquirroClient-0.1/squirro_client/topic/smart_answers.py +25 -0
  48. SquirroClient-0.1/squirro_client/topic/smartfilters.py +466 -0
  49. SquirroClient-0.1/squirro_client/topic/sources.py +880 -0
  50. SquirroClient-0.1/squirro_client/topic/subscriptions.py +273 -0
  51. SquirroClient-0.1/squirro_client/topic/suggest_images.py +50 -0
  52. SquirroClient-0.1/squirro_client/topic/synonyms.py +156 -0
  53. SquirroClient-0.1/squirro_client/topic/tasks.py +78 -0
  54. SquirroClient-0.1/squirro_client/topic/themes.py +155 -0
  55. SquirroClient-0.1/squirro_client/topic/trenddetection.py +489 -0
  56. SquirroClient-0.1/squirro_client/topic/widgets_assets.py +313 -0
  57. SquirroClient-0.1/squirro_client/user.py +814 -0
  58. SquirroClient-0.1/squirro_client/util.py +65 -0
@@ -0,0 +1 @@
1
+ prune tests
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.1
2
+ Name: SquirroClient
3
+ Version: 0.1
4
+ Summary: Python client for the Squirro API
5
+ Home-page: http://dev.squirro.com/docs/tools/python/index.html
6
+ Author: Squirro Team
7
+ Author-email: support@squirro.com
8
+ License: Commercial
9
+ Platform: UNKNOWN
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Requires-Python: >=3.6
19
+
20
+ Python client for the Squirro API.
21
+
22
+ This client provides full access to the Squirro API as documented on
23
+ http://dev.squirro.com/docs/index.html
24
+
25
+ The client documentation is available at
26
+ https://go.squirro.com/squirro-client-documentation
27
+
28
+
@@ -0,0 +1,7 @@
1
+ Python client for the Squirro API.
2
+
3
+ This client provides full access to the Squirro API as documented on
4
+ http://dev.squirro.com/docs/index.html
5
+
6
+ The client documentation is available at
7
+ https://go.squirro.com/squirro-client-documentation
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.1
2
+ Name: SquirroClient
3
+ Version: 0.1
4
+ Summary: Python client for the Squirro API
5
+ Home-page: http://dev.squirro.com/docs/tools/python/index.html
6
+ Author: Squirro Team
7
+ Author-email: support@squirro.com
8
+ License: Commercial
9
+ Platform: UNKNOWN
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Environment :: Web Environment
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: Other/Proprietary License
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Topic :: Internet :: WWW/HTTP
18
+ Requires-Python: >=3.6
19
+
20
+ Python client for the Squirro API.
21
+
22
+ This client provides full access to the Squirro API as documented on
23
+ http://dev.squirro.com/docs/index.html
24
+
25
+ The client documentation is available at
26
+ https://go.squirro.com/squirro-client-documentation
27
+
28
+
@@ -0,0 +1,56 @@
1
+ MANIFEST.in
2
+ README
3
+ setup.py
4
+ SquirroClient.egg-info/PKG-INFO
5
+ SquirroClient.egg-info/SOURCES.txt
6
+ SquirroClient.egg-info/dependency_links.txt
7
+ SquirroClient.egg-info/requires.txt
8
+ SquirroClient.egg-info/top_level.txt
9
+ squirro_client/__init__.py
10
+ squirro_client/base.py
11
+ squirro_client/document_uploader.py
12
+ squirro_client/exceptions.py
13
+ squirro_client/internal.py
14
+ squirro_client/item_uploader.py
15
+ squirro_client/user.py
16
+ squirro_client/util.py
17
+ squirro_client/topic/__init__.py
18
+ squirro_client/topic/communities.py
19
+ squirro_client/topic/community_subscription.py
20
+ squirro_client/topic/community_types.py
21
+ squirro_client/topic/configuration.py
22
+ squirro_client/topic/contributingrecords.py
23
+ squirro_client/topic/dashboards.py
24
+ squirro_client/topic/email_templates.py
25
+ squirro_client/topic/enrichments.py
26
+ squirro_client/topic/entities.py
27
+ squirro_client/topic/facets.py
28
+ squirro_client/topic/file_upload.py
29
+ squirro_client/topic/globaltemp.py
30
+ squirro_client/topic/guidefiles.py
31
+ squirro_client/topic/machinelearning.py
32
+ squirro_client/topic/ml_candidate_set.py
33
+ squirro_client/topic/ml_groundtruth.py
34
+ squirro_client/topic/ml_model.py
35
+ squirro_client/topic/ml_publish.py
36
+ squirro_client/topic/ml_sentence_splitter.py
37
+ squirro_client/topic/ml_template.py
38
+ squirro_client/topic/ml_user_feedback.py
39
+ squirro_client/topic/notes.py
40
+ squirro_client/topic/objects.py
41
+ squirro_client/topic/pipeline_sections.py
42
+ squirro_client/topic/pipeline_status.py
43
+ squirro_client/topic/pipeline_workflows.py
44
+ squirro_client/topic/project_translations.py
45
+ squirro_client/topic/projects.py
46
+ squirro_client/topic/savedsearches.py
47
+ squirro_client/topic/smart_answers.py
48
+ squirro_client/topic/smartfilters.py
49
+ squirro_client/topic/sources.py
50
+ squirro_client/topic/subscriptions.py
51
+ squirro_client/topic/suggest_images.py
52
+ squirro_client/topic/synonyms.py
53
+ squirro_client/topic/tasks.py
54
+ squirro_client/topic/themes.py
55
+ squirro_client/topic/trenddetection.py
56
+ squirro_client/topic/widgets_assets.py
@@ -0,0 +1,5 @@
1
+ hjson
2
+ orjson
3
+ requests<3
4
+ six
5
+ tarsafe
@@ -0,0 +1 @@
1
+ squirro_client
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """
2
+ Build file for the SquirroClient.
3
+
4
+ To publish this on PyPI use:
5
+
6
+ # Build and upload
7
+ python setup.py sdist register upload
8
+ """
9
+ import os
10
+
11
+ from setuptools import find_packages, setup
12
+
13
+ install_requires = open("requirements.txt").read().splitlines()
14
+
15
+ setup(
16
+ name="SquirroClient",
17
+ # Version number also needs to be updated in squirro_client/__init__.py
18
+ version=os.environ.get("SQUIRRO_VERSION", "0.1"),
19
+ description="Python client for the Squirro API",
20
+ long_description=open("README").read(),
21
+ author="Squirro Team",
22
+ author_email="support@squirro.com",
23
+ url="http://dev.squirro.com/docs/tools/python/index.html",
24
+ packages=find_packages(),
25
+ install_requires=install_requires,
26
+ python_requires=">=3.6",
27
+ license="Commercial",
28
+ classifiers=[
29
+ "Development Status :: 5 - Production/Stable",
30
+ "Environment :: Web Environment",
31
+ "Intended Audience :: Developers",
32
+ "License :: Other/Proprietary License",
33
+ "Programming Language :: Python :: 3.8",
34
+ "Programming Language :: Python :: 3.9",
35
+ "Programming Language :: Python :: 3.10",
36
+ "Topic :: Internet :: WWW/HTTP",
37
+ ],
38
+ )
@@ -0,0 +1,11 @@
1
+ """This root level directives are imported from submodules. They are made
2
+ available here as well to keep the number of imports to a minimum for most
3
+ applications.
4
+ """
5
+
6
+ from .base import SquirroClient
7
+ from .document_uploader import DocumentUploader
8
+ from .exceptions import * # noqa: F403
9
+ from .item_uploader import ItemUploader
10
+
11
+ __version__ ="3.9.3"
@@ -0,0 +1,475 @@
1
+ import copy
2
+ import logging
3
+ import warnings
4
+
5
+ from .exceptions import (
6
+ AuthenticationError,
7
+ AuthorizationError,
8
+ ClientError,
9
+ ConflictError,
10
+ ConnectionError,
11
+ InputDataError,
12
+ NotFoundError,
13
+ RetryError,
14
+ TransportError,
15
+ UnknownError,
16
+ )
17
+ from .topic import TopicApiMixin
18
+ from .user import UserApiMixin
19
+ from .util import _dumps, _loads
20
+
21
+ log = logging.getLogger(__name__)
22
+ # SQ-7983: lower the log level of retry module to give some feedback
23
+ # see bug report for details
24
+ logging.getLogger("urllib3.util.retry").setLevel(logging.DEBUG)
25
+
26
+ warnings.filterwarnings("ignore", category=ImportWarning)
27
+ # Disable `ResourceWarning` for python 3 due to unclosed request sessions
28
+ warnings.filterwarnings("ignore", category=ResourceWarning)
29
+
30
+
31
+ #: Default headers for all requests.
32
+ HEADERS = {"Accept": "application/json"}
33
+
34
+ #: Default endpoint URLs for the Squirro platform.
35
+ ENDPOINT = {
36
+ "user": "https://user-api.squirro.com",
37
+ "topic": "https://topic-api.squirro.com",
38
+ }
39
+
40
+ #: Version of the Squirro API to be used.
41
+ API_VERSION = "v0"
42
+
43
+ #: Format for datetime values.
44
+ ISO_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
45
+
46
+ #: Default timeout option.
47
+ TIMEOUT_SECS = 55
48
+
49
+ #: Default retry options.
50
+ RETRY = {
51
+ "total": 10,
52
+ "connect": 5,
53
+ "read": 3,
54
+ "redirect": 5,
55
+ "allowed_methods": frozenset(
56
+ ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE", "POST"]
57
+ ),
58
+ "status_forcelist": frozenset([502, 503, 504]),
59
+ "backoff_factor": 1,
60
+ }
61
+
62
+
63
+ class SquirroClient(UserApiMixin, TopicApiMixin):
64
+ """Client to access the Squirro API.
65
+
66
+ :param client_id: The client id for accessing the API.
67
+ :param client_secret: The client secret for accessing the API.
68
+ :param cluster: The cluster endpoint for accessing the API.
69
+ :param user_api_url: Endpoint URL for the user API service.
70
+ :param topic_api_url: Endpoint URL for the topic API service.
71
+ :param tenant: Tenant domain.
72
+ :param version: API version to use, defaults to 'v0'.
73
+ :param requests: `requests` instance for HTTP calls.
74
+ :param retry_total: Total number of retries to allow. Takes precedence
75
+ over other counts.
76
+ :param retry_connect: How many connection-related errors to retry on.
77
+ :param retry_read: How many times to retry on read errors.
78
+ :param retry_redirect: How many redirects to perform.
79
+ :param retry_method_whitelist: Set of uppercase HTTP method verbs that we
80
+ should retry on.
81
+ :param retry_status_forcelist: A set of integer HTTP status codes that we
82
+ should force a retry on.
83
+ :param retry_backoff_factor: A backoff factor to apply between attempts.
84
+ :param timeout_secs: How many seconds to wait for data before giving up
85
+ (default 55).
86
+
87
+ Example::
88
+
89
+ >>> from squirro_client import SquirroClient
90
+ >>> client = SquirroClient('client_id', 'client_secret')
91
+ >>> client = SquirroClient(None, None,
92
+ ... cluster='http://squirro.example.com')
93
+ >>> client = SquirroClient(None, None,
94
+ ... cluster='http://squirro.example.com',
95
+ ... retry_total=10)
96
+ """
97
+
98
+ def __init__(self, client_id, client_secret, **kwargs):
99
+ # assign authentication class state
100
+ self.client_id = client_id
101
+ self.client_secret = client_secret
102
+ self.tenant = kwargs.get("tenant", None)
103
+
104
+ self._access_token = None
105
+ self._refresh_token = None
106
+
107
+ # endpoint URLs
108
+ self.version = kwargs.get("version", API_VERSION)
109
+ cluster = kwargs.get("cluster")
110
+
111
+ if cluster:
112
+ cluster = cluster.rstrip("/")
113
+ base = "%(cluster)s/api/%(api)s"
114
+ self.user_api_url = base % {"cluster": cluster, "api": "user"}
115
+ self.topic_api_url = base % {"cluster": cluster, "api": "topic"}
116
+
117
+ else:
118
+ user_api_url = kwargs.get("user_api_url") or ENDPOINT["user"]
119
+ topic_api_url = kwargs.get("topic_api_url") or ENDPOINT["topic"]
120
+
121
+ self.user_api_url = user_api_url.rstrip("/")
122
+ self.topic_api_url = topic_api_url.rstrip("/")
123
+
124
+ # retry default options, use values provided by the caller to update
125
+ # the default retry options
126
+ self.retry = copy.deepcopy(RETRY)
127
+ for k, v in kwargs.items():
128
+ if k.startswith("retry_"):
129
+ # `method_whitelist` has been deprecated
130
+ if k == "retry_method_whitelist":
131
+ k = "retry_allowed_methods"
132
+
133
+ self.retry[k.replace("retry_", "")] = v
134
+
135
+ # default timeout
136
+ self.timeout_secs = TIMEOUT_SECS
137
+ kwargs_timeout_secs = kwargs.get("timeout_secs")
138
+ if kwargs_timeout_secs:
139
+ self.timeout_secs = kwargs_timeout_secs
140
+
141
+ # either use the user supplied requests instance or the actual
142
+ # requests module
143
+ self._requests = kwargs.get("requests", None)
144
+ if self._requests is None:
145
+ import requests
146
+
147
+ self._requests = requests
148
+ self.session = self._new_session()
149
+
150
+ @property
151
+ def access_token(self):
152
+ """Property to get or set the access token of the client."""
153
+ return self._access_token
154
+
155
+ @access_token.setter
156
+ def access_token(self, new_access_token):
157
+ """Store the received access token and create a new `requests`
158
+ session.
159
+ """
160
+ self._access_token = new_access_token
161
+ self.session = self._new_session(self._access_token)
162
+
163
+ @property
164
+ def refresh_token(self):
165
+ """Property to get or set the refresh token of the client."""
166
+ return self._refresh_token
167
+
168
+ @refresh_token.setter
169
+ def refresh_token(self, new_refresh_token):
170
+ """Store the received refresh token."""
171
+ self._refresh_token = new_refresh_token
172
+
173
+ def authenticate(
174
+ self,
175
+ tenant=None,
176
+ access_token=None,
177
+ refresh_token=None,
178
+ auth_service=None,
179
+ auth_user=None,
180
+ username=None,
181
+ password=None,
182
+ user_information=None,
183
+ ):
184
+ """Authenticate with the Squirro platform by either using access and
185
+ refresh tokens, username and password, or external service name and
186
+ user identifier.
187
+
188
+ :param tenant: The tenant for accessing the Squirro platform.
189
+ :param access_token: User access token.
190
+ :param refresh_token: User refresh token.
191
+ :param auth_service: External authentication service name.
192
+ :param auth_user: External authentication user identifier.
193
+ :param username: User name.
194
+ :param password: User password.
195
+ :param user_information: Additional information about the user for
196
+ internal use.
197
+
198
+ :raises: :class:`squirro_client.exceptions.AuthenticationError` if
199
+ authentication fails.
200
+
201
+ Example::
202
+
203
+ >>> client.authenticate(username='test@test.com',
204
+ ... password='test')
205
+ >>> client.authenticate(access_token='token01',
206
+ ... refresh_token='token02')
207
+ >>> client.authenticate(auth_service='salesforce',
208
+ ... auth_user='sfdc01')
209
+
210
+ """
211
+
212
+ # set the tenant if it is provided
213
+ if tenant is not None:
214
+ self.tenant = tenant
215
+
216
+ # assemble base request data
217
+ base = {}
218
+ if self.client_id is not None:
219
+ base["client_id"] = self.client_id
220
+ if self.client_secret is not None:
221
+ base["client_secret"] = self.client_secret
222
+ if self.tenant is not None:
223
+ base["tenant"] = self.tenant
224
+ if user_information is not None:
225
+ base["user_information"] = _dumps(user_information)
226
+
227
+ # walk through all possible authentication scenarios
228
+ # - access token
229
+ # - refresh token
230
+ # - external id
231
+ # - username / password
232
+ if access_token is not None:
233
+ # login by using the access token
234
+ data = {"grant_type": "access_token", "access_token": access_token}
235
+ self._perform_authentication(dict(base, **data))
236
+
237
+ elif refresh_token is not None:
238
+ # login by using the refresh token
239
+ data = {"grant_type": "refresh_token", "refresh_token": refresh_token}
240
+ self._perform_authentication(dict(base, **data))
241
+
242
+ elif auth_service is not None and auth_user is not None:
243
+ # by external id
244
+ data = {
245
+ "grant_type": "ext_id",
246
+ "auth_service": auth_service,
247
+ "auth_user": auth_user,
248
+ }
249
+ self._perform_authentication(dict(base, **data))
250
+
251
+ elif username is not None and password is not None:
252
+ # by username and password
253
+ data = {
254
+ "grant_type": "password",
255
+ "username": username,
256
+ "password": password,
257
+ }
258
+ self._perform_authentication(dict(base, **data))
259
+
260
+ else:
261
+ log.debug("empty authentication credentials")
262
+ raise AuthenticationError(None, "empty authentication credentials")
263
+
264
+ def _perform_authentication(self, data):
265
+ """Use the provided parameters to authenticate with the Squirro
266
+ platform.
267
+ """
268
+
269
+ # create endpoint url to fetch new tokens
270
+ url = f"{self.user_api_url}/oauth2/token"
271
+
272
+ session = self._new_session()
273
+
274
+ try:
275
+ r = session.post(url, data=data, timeout=self.timeout_secs)
276
+ except self._requests.exceptions.RetryError as ex:
277
+ raise RetryError(None, ex.args[0])
278
+ except Exception as ex:
279
+ log.debug("unable to connect to the Squirro platform: %r", ex)
280
+ raise ConnectionError(None, ex)
281
+
282
+ res = {}
283
+ try:
284
+ # decode the JSON response and store tokens
285
+ if r.content:
286
+ res = _loads(r.content)
287
+ except ValueError as ex:
288
+ log.error("unable to decode JSON: %r", r.content)
289
+ raise TransportError(r.status_code, ex)
290
+
291
+ # perform error handling for known cases
292
+ error = res.get("error")
293
+ if r.status_code == 400:
294
+ raise InputDataError(r.status_code, error)
295
+ elif r.status_code in [403, 404, 410]:
296
+ raise AuthenticationError(r.status_code, error)
297
+ elif r.status_code != 200:
298
+ raise UnknownError(r.status_code, error)
299
+
300
+ # now we can set the tenant and user identifier as well
301
+ if res.get("tenant"):
302
+ self.tenant = res.get("tenant")
303
+ if res.get("user_id"):
304
+ self.user_id = res.get("user_id")
305
+ self.user_permissions = res.get("role_permissions", [])
306
+ self.token_project_permissions = res.get("project_permissions", None)
307
+ self.user_information = res.get("user_information", {})
308
+
309
+ # all is good and we store the new token
310
+ self._on_token_refresh(res.get("access_token"), res.get("refresh_token"))
311
+
312
+ def _on_token_refresh(self, access_token, refresh_token):
313
+ """Method to be called if a new access token has been received for the
314
+ user.
315
+ """
316
+
317
+ # assign class state and refresh the session
318
+ self.access_token = access_token
319
+ self.refresh_token = refresh_token
320
+
321
+ def _new_session(self, access_token=None):
322
+ """Create a `requests` session to use for communicating with the
323
+ Squirro platform.
324
+ """
325
+ # construct headers to be used for all requests
326
+ headers = dict(HEADERS)
327
+ if access_token is not None:
328
+ authorization = f"Bearer {self.access_token}"
329
+ headers["Authorization"] = authorization
330
+
331
+ session = self._requests.Session()
332
+ session.headers.update(headers)
333
+
334
+ # add retry capabilities
335
+ if hasattr(self._requests, "adapters"):
336
+ retries = self._requests.adapters.Retry(**self.retry)
337
+ adapter = self._requests.adapters.HTTPAdapter(max_retries=retries)
338
+ session.mount("http://", adapter)
339
+ session.mount("https://", adapter)
340
+
341
+ return session
342
+
343
+ def _perform_request(self, method, url, **kwargs):
344
+ """Method to issue a request to the Squirro platform. If the OAuth
345
+ access token has expired a new token is requested and the request
346
+ retried exactly once.
347
+ """
348
+
349
+ # make sure that there is an access token and a refresh token,
350
+ # otherwise we cannot continue
351
+ if self.access_token is None and self.refresh_token is None:
352
+ raise InputDataError(None, "missing access and refresh token")
353
+
354
+ # make sure that the tenant is set
355
+ if self.tenant is None:
356
+ raise InputDataError(None, "missing tenant")
357
+
358
+ # issue request and retry if authorization fails
359
+ for retry_count in range(2):
360
+ func = getattr(self.session, method)
361
+
362
+ try:
363
+ res = func(url, timeout=self.timeout_secs, **kwargs)
364
+ except self._requests.exceptions.RetryError as ex:
365
+ raise RetryError(None, ex.args[0])
366
+ except self._requests.exceptions.ConnectionError as ex:
367
+ raise ConnectionError(None, ex) from ex
368
+
369
+ # check for failed authorization
370
+ if retry_count == 0 and res.status_code == 401:
371
+ # try to refresh the tokens, if that fails we return the
372
+ # previous response
373
+ successful = self._refresh_tokens()
374
+ if not successful:
375
+ break
376
+
377
+ log.debug("retrying request %r %r", method, url)
378
+
379
+ else:
380
+ break
381
+
382
+ return res
383
+
384
+ def _refresh_tokens(self, **kwargs):
385
+ """Refresh the access token by using the refresh token."""
386
+
387
+ # return value is whether we managed to receive a new token
388
+ ret = False
389
+
390
+ # first we make sure that we have a refresh token
391
+ if not self.refresh_token:
392
+ log.debug("unable to refresh access token, missing refresh token")
393
+ return ret
394
+
395
+ # login via the refresh token
396
+ try:
397
+ # Check that we have actually received a new access token.
398
+ # On multi-node systems we may not get a new one because of
399
+ # time-drift among the servers. Hence the server that we are about
400
+ # to ask to refresh the token may not consider the access token
401
+ # expired in contrast to a different server that has determined
402
+ # the access token has expired and whose response has triggered
403
+ # this `_refresh_tokens` request. Perform a couple of retries
404
+ # in a "best effort" manner until we receive a new access token.
405
+ previous_access_token = self.access_token
406
+
407
+ for try_count in range(3):
408
+ self.authenticate(refresh_token=self.refresh_token, **kwargs)
409
+
410
+ if self.access_token != previous_access_token:
411
+ ret = True
412
+ break
413
+
414
+ log.info(
415
+ "Access token %r not refreshed on behalf of "
416
+ "refresh token %r. Retry number %d",
417
+ self.access_token,
418
+ self.refresh_token,
419
+ try_count,
420
+ )
421
+
422
+ except Exception as ex:
423
+ log.exception("unable to refresh access token: %r", ex)
424
+
425
+ return ret
426
+
427
+ def _process_response(self, res, expected_status_codes=None):
428
+ """Return the data from the response if the status code matches to what
429
+ is expected. If that is not the case the appropriate exception is
430
+ being raised.
431
+ """
432
+
433
+ # work around mutable default arguments
434
+ if expected_status_codes is None:
435
+ expected_status_codes = [200]
436
+
437
+ data = {}
438
+ try:
439
+ if res.content:
440
+ data = _loads(res.content)
441
+ except ValueError as ex:
442
+ log.error("unable to decode JSON: %r", res.content)
443
+ raise TransportError(None, ex)
444
+
445
+ # if the status code matches to what is expected we return the data
446
+ if res.status_code in expected_status_codes:
447
+ return data
448
+
449
+ # raise the appropriate exception
450
+ error = data.get("error")
451
+ if res.status_code == 400:
452
+ raise ClientError(res.status_code, error, data)
453
+
454
+ elif res.status_code in [401, 403]:
455
+ raise AuthorizationError(res.status_code, error)
456
+
457
+ elif res.status_code == 404:
458
+ raise NotFoundError(res.status_code, error)
459
+
460
+ elif res.status_code == 409:
461
+ raise ConflictError(res.status_code, error)
462
+
463
+ else:
464
+ raise UnknownError(res.status_code, error)
465
+
466
+ def _format_date(self, d):
467
+ """Converts a datetime object into a string date.
468
+
469
+ :param d: :class:`datetime.datetime` instance.
470
+ :returns: Date string with the format as specified in
471
+ :attr:`ISO_DATE_FORMAT` If d is `None` an empty string is returned.
472
+ """
473
+ if d is None:
474
+ return None
475
+ return d.strftime(ISO_DATE_FORMAT)