widen-client 0.0.15__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.
- widen_client/__init__.py +19 -0
- widen_client/baseapi.py +52 -0
- widen_client/client.py +382 -0
- widen_client/entities/__init__.py +0 -0
- widen_client/entities/assets.py +113 -0
- widen_client/entities/metadata.py +134 -0
- widen_client/entities/uploads.py +39 -0
- widen_client-0.0.15.dist-info/METADATA +67 -0
- widen_client-0.0.15.dist-info/RECORD +12 -0
- widen_client-0.0.15.dist-info/WHEEL +5 -0
- widen_client-0.0.15.dist-info/licenses/LICENSE +1 -0
- widen_client-0.0.15.dist-info/top_level.txt +1 -0
widen_client/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
widen-client public API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .client import Widen, WidenClient
|
|
6
|
+
from .entities.assets import Assets
|
|
7
|
+
from .entities.metadata import Metadata
|
|
8
|
+
from .entities.uploads import Uploads
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'Widen',
|
|
13
|
+
'WidenClient',
|
|
14
|
+
'Assets',
|
|
15
|
+
'Metadata',
|
|
16
|
+
'Uploads',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = '0.0.15'
|
widen_client/baseapi.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from urllib.parse import urlencode
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseApi:
|
|
5
|
+
"""
|
|
6
|
+
A base class to build paths and a baseline CRUD interface for an entity set.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self, client):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self._client = client
|
|
12
|
+
|
|
13
|
+
if not self.endpoint:
|
|
14
|
+
raise NotImplementedError('self.endpoint must be set when subclassing this class.')
|
|
15
|
+
|
|
16
|
+
def _build_path(self, key=None, endpoint=None, property=None, queryparams=None, *args, **kwargs):
|
|
17
|
+
"""
|
|
18
|
+
Build path to endpoint, including support for key lookup of a single entity.
|
|
19
|
+
"""
|
|
20
|
+
endpoint = endpoint or self.endpoint
|
|
21
|
+
|
|
22
|
+
if key:
|
|
23
|
+
endpoint += f'/{key}'
|
|
24
|
+
|
|
25
|
+
if property:
|
|
26
|
+
endpoint += f'/{property}'
|
|
27
|
+
|
|
28
|
+
if queryparams:
|
|
29
|
+
endpoint += f'?{urlencode(queryparams)}'
|
|
30
|
+
|
|
31
|
+
return endpoint
|
|
32
|
+
|
|
33
|
+
def get(self, key, property=None, queryparams=None):
|
|
34
|
+
"""
|
|
35
|
+
Get an entity. e.g. GET /path/to/endpoint/key
|
|
36
|
+
"""
|
|
37
|
+
response_json, _ = self._client._get(self._build_path(key=key, property=property, queryparams=queryparams))
|
|
38
|
+
return response_json
|
|
39
|
+
|
|
40
|
+
def exists(self, key, queryparams=None):
|
|
41
|
+
"""
|
|
42
|
+
Check if an entity exists.
|
|
43
|
+
"""
|
|
44
|
+
from . import WidenError
|
|
45
|
+
from requests.exceptions import RequestException
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
_, status_code = self._client._get(self._build_path(key=key, queryparams=queryparams))
|
|
49
|
+
except (WidenError, RequestException) as exception:
|
|
50
|
+
return False
|
|
51
|
+
else:
|
|
52
|
+
return status_code == 200
|
widen_client/client.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from http.client import responses
|
|
3
|
+
import logging
|
|
4
|
+
from itertools import takewhile
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from requests.adapters import HTTPAdapter
|
|
10
|
+
from urllib3 import Retry
|
|
11
|
+
|
|
12
|
+
from widen_client.entities.assets import Assets
|
|
13
|
+
from widen_client.entities.metadata import Metadata
|
|
14
|
+
from widen_client.entities.uploads import Uploads
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CustomRetry(Retry):
|
|
21
|
+
def get_backoff_time(self):
|
|
22
|
+
"""Formula for computing the current backoff
|
|
23
|
+
|
|
24
|
+
:rtype: float
|
|
25
|
+
"""
|
|
26
|
+
# We want to consider only the last consecutive errors sequence (Ignore redirects).
|
|
27
|
+
consecutive_errors_len = len(
|
|
28
|
+
list(
|
|
29
|
+
takewhile(lambda x: x.redirect_location is None, reversed(self.history))
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
if consecutive_errors_len <= 1:
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
backoff_value = self.backoff_factor * (2 ** consecutive_errors_len)
|
|
36
|
+
return min(self.BACKOFF_MAX, backoff_value)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WidenError(Exception):
|
|
40
|
+
def __init__(self, message, request_info=None):
|
|
41
|
+
self.request_info = request_info
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class WidenAttributeError(Exception):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_error_data_from_response(response):
|
|
50
|
+
"""
|
|
51
|
+
Construct a dictionary with error data from a >= 400 level response
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
error_data = response.json()
|
|
55
|
+
except ValueError:
|
|
56
|
+
# in case of a 500 error, the response might not be a JSON
|
|
57
|
+
error_data = {'response': response}
|
|
58
|
+
|
|
59
|
+
error_data.update({
|
|
60
|
+
key: value
|
|
61
|
+
for key, value in response.headers.items()
|
|
62
|
+
if key.startswith('X-Ratelimit')
|
|
63
|
+
})
|
|
64
|
+
return error_data
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _enabled_or_noop(fn):
|
|
68
|
+
@functools.wraps(fn)
|
|
69
|
+
def wrapper(self, *args, **kwargs):
|
|
70
|
+
if self.enabled:
|
|
71
|
+
return fn(self, *args, **kwargs)
|
|
72
|
+
return {}, 503
|
|
73
|
+
return wrapper
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class WidenClient:
|
|
77
|
+
"""
|
|
78
|
+
Low-level client for interfacing with Widen v2 API.
|
|
79
|
+
|
|
80
|
+
This client layer is responsible for authenticating, handling constructing/making requests, logging API calls,
|
|
81
|
+
and catching request/connection exceptions, and catching/raising some select REST responses (4xx+, 204, etc.)
|
|
82
|
+
|
|
83
|
+
Documentation: https://widenv2.docs.apiary.io/#
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
enabled=True,
|
|
89
|
+
access_token=None,
|
|
90
|
+
timeout=60,
|
|
91
|
+
retries_after_server_error=0,
|
|
92
|
+
backoff_factor=1.0,
|
|
93
|
+
request_hooks=None,
|
|
94
|
+
request_headers=None
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Create a session and setup access token in request header.
|
|
98
|
+
|
|
99
|
+
:param enabled: Whether the client should execute any requests
|
|
100
|
+
:param access_token: Widen access token
|
|
101
|
+
:param timeout: (optional) Seconds to wait for the server to send data before timing out (default to 60)
|
|
102
|
+
:param retries_after_server_error: (optional) Number of times to retry request after receiving a server error.
|
|
103
|
+
The error statuses that will be retried are:
|
|
104
|
+
- 429 (Too Many Requests)
|
|
105
|
+
- 502 (Bad Gateway)
|
|
106
|
+
- 504 (Gateway Timeout)
|
|
107
|
+
response (defaults to 0, which is to not retry)
|
|
108
|
+
:param backoff_factor: (optional) A backoff factor to apply between attempts after the second try, following the
|
|
109
|
+
formula: {backoff factor} * (2 ** ({number of total retries} - 1))
|
|
110
|
+
:param request_hooks: (optional) Hooks for :py:func:`requests.requests`.
|
|
111
|
+
:param request_headers: (optional) Headers for :py:func:`requests.requests`.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
super(WidenClient).__init__()
|
|
115
|
+
self.enabled = enabled
|
|
116
|
+
self.base_url = f'https://api.widencollective.com/v2/'
|
|
117
|
+
self.timeout = timeout
|
|
118
|
+
|
|
119
|
+
self.request_headers = request_headers or requests.utils.default_headers()
|
|
120
|
+
self.request_hooks = request_hooks or requests.hooks.default_hooks()
|
|
121
|
+
|
|
122
|
+
self.session = self._get_session(
|
|
123
|
+
access_token=access_token,
|
|
124
|
+
retries_after_server_error=retries_after_server_error,
|
|
125
|
+
backoff_factor=backoff_factor
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
self.widen_api_request_correlation_id = uuid.uuid4()
|
|
129
|
+
|
|
130
|
+
def _get_session(self, access_token=None, retries_after_server_error=0, backoff_factor=1.0):
|
|
131
|
+
"""
|
|
132
|
+
Create a session and setup access token in request header.
|
|
133
|
+
|
|
134
|
+
:param access_token: Widen API Access Token
|
|
135
|
+
:param retries_after_server_error: (optional) Number of times to retry request after receiving a server error.
|
|
136
|
+
The error statuses that will be retried are:
|
|
137
|
+
- 429 (Too Many Requests)
|
|
138
|
+
- 502 (Bad Gateway)
|
|
139
|
+
- 504 (Gateway Timeout)
|
|
140
|
+
:param backoff_factor: (optional) A backoff factor to apply between attempts after the second try, following the
|
|
141
|
+
formula: {backoff factor} * (2 ** ({number of total retries} - 1))
|
|
142
|
+
"""
|
|
143
|
+
session = requests.Session()
|
|
144
|
+
session.encoding = 'utf8'
|
|
145
|
+
session.headers.update({
|
|
146
|
+
'Authorization': f'Bearer {access_token}',
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
if retries_after_server_error:
|
|
150
|
+
retry_strategy = CustomRetry(
|
|
151
|
+
total=retries_after_server_error,
|
|
152
|
+
backoff_factor=backoff_factor,
|
|
153
|
+
status_forcelist=[429, 502, 504],
|
|
154
|
+
method_whitelist=['HEAD', 'GET', 'POST', 'PUT', 'DELETE' 'OPTIONS']
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
adapter = HTTPAdapter(max_retries=retry_strategy)
|
|
158
|
+
session.mount("https://", adapter)
|
|
159
|
+
|
|
160
|
+
return session
|
|
161
|
+
|
|
162
|
+
@_enabled_or_noop
|
|
163
|
+
def _get(self, url):
|
|
164
|
+
"""
|
|
165
|
+
Handle authenticated GET requests
|
|
166
|
+
|
|
167
|
+
:param url: The url for the endpoint including path parameters
|
|
168
|
+
:return: The JSON output from the API and status code
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
url = urljoin(self.base_url, url)
|
|
172
|
+
try:
|
|
173
|
+
response = self._make_request(**dict(
|
|
174
|
+
method='GET',
|
|
175
|
+
url=url,
|
|
176
|
+
timeout=self.timeout,
|
|
177
|
+
hooks=self.request_hooks,
|
|
178
|
+
headers=self.request_headers
|
|
179
|
+
))
|
|
180
|
+
except requests.exceptions.RequestException as exception:
|
|
181
|
+
raise exception
|
|
182
|
+
except requests.exceptions.ReadTimeout as exception:
|
|
183
|
+
request_info = {
|
|
184
|
+
'method': 'PUT',
|
|
185
|
+
'url': url,
|
|
186
|
+
}
|
|
187
|
+
raise WidenError(_get_error_data_from_response(exception.response), request_info=request_info)
|
|
188
|
+
else:
|
|
189
|
+
if response.status_code >= 400:
|
|
190
|
+
request_info = {
|
|
191
|
+
'method': 'GET',
|
|
192
|
+
'url': url,
|
|
193
|
+
}
|
|
194
|
+
raise WidenError(_get_error_data_from_response(response), request_info=request_info)
|
|
195
|
+
|
|
196
|
+
if response.status_code == 204:
|
|
197
|
+
return {}, response.status_code
|
|
198
|
+
|
|
199
|
+
return response.json(), response.status_code
|
|
200
|
+
|
|
201
|
+
@_enabled_or_noop
|
|
202
|
+
def _post(self, url, data=None, files=None):
|
|
203
|
+
"""
|
|
204
|
+
Handle authenticated POST requests
|
|
205
|
+
|
|
206
|
+
:param url: The url for the endpoint including path parameters
|
|
207
|
+
:param data: The request body parameters
|
|
208
|
+
:return: The JSON output from the API and status code
|
|
209
|
+
"""
|
|
210
|
+
url = urljoin(self.base_url, url)
|
|
211
|
+
post_params = {
|
|
212
|
+
'method': 'POST',
|
|
213
|
+
'url': url,
|
|
214
|
+
'files': files,
|
|
215
|
+
'timeout': self.timeout,
|
|
216
|
+
'hooks': self.request_hooks,
|
|
217
|
+
'headers': self.request_headers
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if files:
|
|
221
|
+
post_params.update({'data': data})
|
|
222
|
+
else:
|
|
223
|
+
self.session.headers.update({'Content-Type': 'application/json'})
|
|
224
|
+
post_params.update({'json': data})
|
|
225
|
+
try:
|
|
226
|
+
response = self._make_request(**post_params)
|
|
227
|
+
except requests.exceptions.RequestException as exception:
|
|
228
|
+
raise exception
|
|
229
|
+
except requests.exceptions.ReadTimeout as exception:
|
|
230
|
+
request_info = {
|
|
231
|
+
'method': 'PUT',
|
|
232
|
+
'url': url,
|
|
233
|
+
'json': data,
|
|
234
|
+
}
|
|
235
|
+
raise WidenError(_get_error_data_from_response(exception.response), request_info=request_info)
|
|
236
|
+
else:
|
|
237
|
+
if response.status_code >= 400:
|
|
238
|
+
request_info = {
|
|
239
|
+
'method': 'POST',
|
|
240
|
+
'url': url,
|
|
241
|
+
'files': files,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# No need to include file data for exception capturing.
|
|
245
|
+
if not files:
|
|
246
|
+
request_info.update({'json': data})
|
|
247
|
+
|
|
248
|
+
raise WidenError(_get_error_data_from_response(response), request_info=request_info)
|
|
249
|
+
|
|
250
|
+
if response.status_code == 204:
|
|
251
|
+
return {}, response.status_code
|
|
252
|
+
|
|
253
|
+
return response.json(), response.status_code
|
|
254
|
+
|
|
255
|
+
def _make_request(self, **kwargs):
|
|
256
|
+
logger.info(
|
|
257
|
+
f'Request to Widen API [{self.widen_api_request_correlation_id}]: '
|
|
258
|
+
f'{kwargs.get("method")} {kwargs.get("url")}'
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if kwargs.get('json'):
|
|
262
|
+
logger.info(f'Request to Widen API [{self.widen_api_request_correlation_id}]: {kwargs.get("json")}')
|
|
263
|
+
|
|
264
|
+
response = self.session.request(**kwargs)
|
|
265
|
+
|
|
266
|
+
if response.status_code >= 400:
|
|
267
|
+
error_data = _get_error_data_from_response(response)
|
|
268
|
+
logger.info(
|
|
269
|
+
f'Response from Widen API [{self.widen_api_request_correlation_id}]: '
|
|
270
|
+
f'{response.status_code} {responses[response.status_code]}: {error_data}'
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
logger.info(
|
|
274
|
+
f'Response from Widen API [{self.widen_api_request_correlation_id}]: '
|
|
275
|
+
f'{response.status_code} {responses[response.status_code]}'
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Once the request is finished it set a new correlation id
|
|
279
|
+
self.widen_api_request_correlation_id = uuid.uuid4()
|
|
280
|
+
|
|
281
|
+
return response
|
|
282
|
+
|
|
283
|
+
@_enabled_or_noop
|
|
284
|
+
def _put(self, url, data=None):
|
|
285
|
+
"""
|
|
286
|
+
Handle authenticated PUT requests
|
|
287
|
+
|
|
288
|
+
:param url: The url for the endpoint including path parameters
|
|
289
|
+
:param data: The request body parameters
|
|
290
|
+
:return: The JSON output from the API and status code
|
|
291
|
+
"""
|
|
292
|
+
url = urljoin(self.base_url, url)
|
|
293
|
+
self.session.headers.update({'Content-Type': 'application/json'})
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
response = self._make_request(**dict(
|
|
297
|
+
method='PUT',
|
|
298
|
+
url=url,
|
|
299
|
+
json=data,
|
|
300
|
+
timeout=self.timeout,
|
|
301
|
+
hooks=self.request_hooks,
|
|
302
|
+
headers=self.request_headers
|
|
303
|
+
))
|
|
304
|
+
except requests.exceptions.RequestException as exception:
|
|
305
|
+
raise exception
|
|
306
|
+
except requests.exceptions.ReadTimeout as exception:
|
|
307
|
+
request_info = {
|
|
308
|
+
'method': 'PUT',
|
|
309
|
+
'url': url,
|
|
310
|
+
'json': data,
|
|
311
|
+
}
|
|
312
|
+
raise WidenError(_get_error_data_from_response(exception.response), request_info=request_info)
|
|
313
|
+
else:
|
|
314
|
+
if response.status_code >= 400:
|
|
315
|
+
request_info = {
|
|
316
|
+
'method': 'PUT',
|
|
317
|
+
'url': url,
|
|
318
|
+
'json': data,
|
|
319
|
+
}
|
|
320
|
+
raise WidenError(_get_error_data_from_response(response), request_info=request_info)
|
|
321
|
+
|
|
322
|
+
if response.status_code == 204:
|
|
323
|
+
return {}, response.status_code
|
|
324
|
+
|
|
325
|
+
return response.json(), response.status_code
|
|
326
|
+
|
|
327
|
+
@_enabled_or_noop
|
|
328
|
+
def _delete(self, url):
|
|
329
|
+
"""
|
|
330
|
+
Handle authenticated DELETE requests.
|
|
331
|
+
|
|
332
|
+
:param url: The url for the endpoint including path parameters
|
|
333
|
+
:return: The JSON output from the API and status code
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
url = urljoin(self.base_url, url)
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
response = self._make_request(**dict(
|
|
340
|
+
method='DELETE',
|
|
341
|
+
url=url,
|
|
342
|
+
timeout=self.timeout,
|
|
343
|
+
hooks=self.request_hooks,
|
|
344
|
+
headers=self.request_headers
|
|
345
|
+
))
|
|
346
|
+
except requests.exceptions.RequestException as exception:
|
|
347
|
+
raise exception
|
|
348
|
+
except requests.exceptions.ReadTimeout as exception:
|
|
349
|
+
request_info = {
|
|
350
|
+
'method': 'PUT',
|
|
351
|
+
'url': url,
|
|
352
|
+
}
|
|
353
|
+
raise WidenError(_get_error_data_from_response(exception.response), request_info=request_info)
|
|
354
|
+
else:
|
|
355
|
+
if response.status_code >= 400:
|
|
356
|
+
request_info = {
|
|
357
|
+
'method': 'DELETE',
|
|
358
|
+
'url': url,
|
|
359
|
+
}
|
|
360
|
+
raise WidenError(_get_error_data_from_response(response), request_info=request_info)
|
|
361
|
+
|
|
362
|
+
if response.status_code == 204:
|
|
363
|
+
return {}, response.status_code
|
|
364
|
+
|
|
365
|
+
return response.json(), response.status_code
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class Widen(WidenClient):
|
|
369
|
+
"""
|
|
370
|
+
Widen class for interfacing with Widen v2 API
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
def __init__(self, *args, **kwargs):
|
|
374
|
+
"""
|
|
375
|
+
Initialize client class and attach all available entity endpoints
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
super().__init__(*args, **kwargs)
|
|
379
|
+
|
|
380
|
+
self.assets = Assets(self)
|
|
381
|
+
self.uploads = Uploads(self)
|
|
382
|
+
self.metadata = Metadata(self)
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from ..baseapi import BaseApi
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Assets(BaseApi):
|
|
5
|
+
def __init__(self, *args, **kwargs):
|
|
6
|
+
"""
|
|
7
|
+
Initialize the endpoint.
|
|
8
|
+
"""
|
|
9
|
+
self.endpoint = 'assets'
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
|
|
12
|
+
def list_asset_groups(self) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
List asset groups.
|
|
15
|
+
"""
|
|
16
|
+
response_json, _ = self._client._get(self._build_path(property='assetgroups'))
|
|
17
|
+
return response_json
|
|
18
|
+
|
|
19
|
+
def search(self, querystring: str, queryparams: dict = None) -> dict:
|
|
20
|
+
"""
|
|
21
|
+
List entities by search query. `querystring` parameter takes the quick search syntax documented here:
|
|
22
|
+
https://community.widen.com/collective/s/article/How-do-I-search-for-assets
|
|
23
|
+
"""
|
|
24
|
+
if not queryparams:
|
|
25
|
+
queryparams = {}
|
|
26
|
+
|
|
27
|
+
queryparams.update({
|
|
28
|
+
'query': querystring,
|
|
29
|
+
'scroll': True,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
response_json, _ = self._client._get(self._build_path(property='search', queryparams=queryparams))
|
|
33
|
+
|
|
34
|
+
if self._client.enabled:
|
|
35
|
+
if response_json['scroll_id']:
|
|
36
|
+
for items in self._scroll(response_json['scroll_id'], expand=queryparams.get('expand', '')):
|
|
37
|
+
response_json['items'].extend(items)
|
|
38
|
+
|
|
39
|
+
return response_json
|
|
40
|
+
|
|
41
|
+
def _scroll(self, scroll_id: str, expand: str) -> list:
|
|
42
|
+
"""
|
|
43
|
+
Fetch all remaining scroll results for a given response, returning an iterator with the results.
|
|
44
|
+
"""
|
|
45
|
+
queryparams = {
|
|
46
|
+
'scroll_id': scroll_id,
|
|
47
|
+
'expand': expand,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
result, _ = self._client._get(self._build_path(property='search/scroll', queryparams=queryparams))
|
|
51
|
+
|
|
52
|
+
if self._client.enabled:
|
|
53
|
+
items = result['items']
|
|
54
|
+
|
|
55
|
+
while items:
|
|
56
|
+
yield result['items']
|
|
57
|
+
result, _ = self._client._get(self._build_path(property='search/scroll', queryparams=queryparams))
|
|
58
|
+
items = result['items']
|
|
59
|
+
else:
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
def get_metadata(self, key: str) -> dict:
|
|
63
|
+
"""
|
|
64
|
+
Get metadata for an asset.
|
|
65
|
+
"""
|
|
66
|
+
response_json, _ = self._client._get(self._build_path(key=key, property='metadata'))
|
|
67
|
+
return response_json
|
|
68
|
+
|
|
69
|
+
def update_metadata(self, key: str, metadata: dict) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
Update metadata for an asset. Only keys passed will be updated.
|
|
72
|
+
"""
|
|
73
|
+
patch = {
|
|
74
|
+
'patch': ','.join(metadata['fields'].keys())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
response_json, _ = self._client._put(self._build_path(key=key, property='metadata', queryparams=patch), metadata)
|
|
78
|
+
return response_json
|
|
79
|
+
|
|
80
|
+
def get_security(self, key: str) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Get security for an asset.
|
|
83
|
+
"""
|
|
84
|
+
response_json, _ = self._client._get(self._build_path(key=key, property='security'))
|
|
85
|
+
return response_json
|
|
86
|
+
|
|
87
|
+
def update_security(self, key: str, security: dict) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
Update security for an asset. Only keys passed will be updated.
|
|
90
|
+
"""
|
|
91
|
+
patch = {
|
|
92
|
+
'patch': ','.join(security.keys()),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
response_json, _ = self._client._put(self._build_path(key=key, property='security', queryparams=patch), security)
|
|
96
|
+
return response_json
|
|
97
|
+
|
|
98
|
+
def rename(self, key: str, filename: str) -> dict:
|
|
99
|
+
"""
|
|
100
|
+
Renames an assets's filename.
|
|
101
|
+
"""
|
|
102
|
+
response_json, _ = self._client._put(
|
|
103
|
+
self._build_path(key=key, property='filename'),
|
|
104
|
+
{'filename': filename}
|
|
105
|
+
)
|
|
106
|
+
return response_json
|
|
107
|
+
|
|
108
|
+
def delete(self, key: str) -> dict:
|
|
109
|
+
"""
|
|
110
|
+
Delete an asset.
|
|
111
|
+
"""
|
|
112
|
+
response_json, _ = self._client._delete(self._build_path(key=key))
|
|
113
|
+
return response_json
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
|
|
3
|
+
from ..baseapi import BaseApi
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Metadata(BaseApi):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
"""
|
|
9
|
+
Initialize the endpoint.
|
|
10
|
+
"""
|
|
11
|
+
self.endpoint = 'metadata'
|
|
12
|
+
super().__init__(*args, **kwargs)
|
|
13
|
+
|
|
14
|
+
def get(self, *args, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
Get an entity. e.g. GET /path/to/endpoint/key
|
|
17
|
+
"""
|
|
18
|
+
# Getting metadata not supported, use list/get_controlled_vocabulary instead
|
|
19
|
+
from .. import WidenAttributeError
|
|
20
|
+
raise WidenAttributeError
|
|
21
|
+
|
|
22
|
+
def exists(self, *args, **kwargs):
|
|
23
|
+
"""
|
|
24
|
+
Check if an entity exists.
|
|
25
|
+
"""
|
|
26
|
+
# Checking if metadata exists not supported, use get_controlled_vocabulary instead
|
|
27
|
+
from .. import WidenAttributeError
|
|
28
|
+
raise WidenAttributeError
|
|
29
|
+
|
|
30
|
+
def get_viewable_fields(self, field_types: str = 'all', display_name_after: str = None, display_name_starts_with: str = None, limit: int = 100) -> dict:
|
|
31
|
+
"""
|
|
32
|
+
List all metadata fields that you have permission to see.
|
|
33
|
+
|
|
34
|
+
:param field_types: The field type that you want to query.
|
|
35
|
+
:param display_name_after: A filter that only returns metadata fields with display names alphabetically greater than this value.
|
|
36
|
+
:param display_name_starts_with: A filter that only returns metadata fields with display names that start with this value.
|
|
37
|
+
:param limit: The max number of metadata fields returned by this query. Must be between 1 and 100.
|
|
38
|
+
"""
|
|
39
|
+
queryparams = {'field_types': field_types}
|
|
40
|
+
|
|
41
|
+
if display_name_after is not None:
|
|
42
|
+
queryparams.update({'display_name_after': display_name_after})
|
|
43
|
+
if display_name_starts_with is not None:
|
|
44
|
+
queryparams.update({'display_name_starts_with': display_name_starts_with})
|
|
45
|
+
if limit is not None:
|
|
46
|
+
queryparams.update({'limit': limit})
|
|
47
|
+
|
|
48
|
+
response_json, _ = self._client._get(self._build_path(property='fields/viewable'))
|
|
49
|
+
return response_json
|
|
50
|
+
|
|
51
|
+
def list_controlled_vocabulary_values(self, display_key: str) -> dict:
|
|
52
|
+
"""
|
|
53
|
+
List all values from the controlled vocabulary for a metadata field.
|
|
54
|
+
|
|
55
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
56
|
+
"""
|
|
57
|
+
response_json, _ = self._client._get(self._build_path(property=f'{display_key}/vocabulary'))
|
|
58
|
+
return response_json
|
|
59
|
+
|
|
60
|
+
def add_controlled_vocabulary_value(self, display_key: str, value: str, index: int = None) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Add a new value to the controlled vocabulary for a metadata field.
|
|
63
|
+
|
|
64
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
65
|
+
:param value: The new controlled vocabulary value you wish to add.
|
|
66
|
+
:param index: Optional index of where to insert the new controlled vocabulary value. If omitted, the new value will be added to the end of the list
|
|
67
|
+
"""
|
|
68
|
+
data = {'value': value}
|
|
69
|
+
|
|
70
|
+
if index is not None:
|
|
71
|
+
data.update({'index': index})
|
|
72
|
+
|
|
73
|
+
response_json, _ = self._client._post(self._build_path(property=f'{display_key}/vocabulary'), data=data)
|
|
74
|
+
return response_json
|
|
75
|
+
|
|
76
|
+
def get_controlled_vocabulary_value(self, display_key: str, existing_value: str) -> dict:
|
|
77
|
+
"""
|
|
78
|
+
Get the existing value from the controlled vocabulary for a metadata field.
|
|
79
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
80
|
+
:param existing_value: The controlled vocabulary value.
|
|
81
|
+
"""
|
|
82
|
+
existing_value = quote(existing_value, safe='') # encode possible slashes in vocabulary value
|
|
83
|
+
response_json, _ = self._client._get(self._build_path(property=f'{display_key}/vocabulary/{existing_value}'))
|
|
84
|
+
return response_json
|
|
85
|
+
|
|
86
|
+
def get_controlled_vocabulary_value_exists(self, display_key: str, value: str) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Check if value exists as a controlled vocabulary for a metadata field.
|
|
89
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
90
|
+
:param value: The controlled vocabulary value.
|
|
91
|
+
"""
|
|
92
|
+
from .. import WidenError
|
|
93
|
+
from requests.exceptions import RequestException
|
|
94
|
+
|
|
95
|
+
value = quote(value, safe='') # encode possible slashes in vocabulary value
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
_, status_code = self._client._get(self._build_path(property=f'{display_key}/vocabulary/{value}'))
|
|
99
|
+
except (WidenError, RequestException):
|
|
100
|
+
return False
|
|
101
|
+
else:
|
|
102
|
+
return status_code == 200
|
|
103
|
+
|
|
104
|
+
def update_controlled_vocabulary_value(self, display_key: str, existing_value: str, value: str = None, index: int = None) -> dict:
|
|
105
|
+
"""
|
|
106
|
+
Update an existing value from the controlled vocabulary for a metadata field.
|
|
107
|
+
|
|
108
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
109
|
+
:param existing_value: The controlled vocabulary value you wish to update.
|
|
110
|
+
:param value: Optional new display name for this controlled vocabulary value. Either value, index, or both must be provided.
|
|
111
|
+
:param index: Optional index of where to re-order the updated controlled vocabulary value. Either value, index, or both must be provided.
|
|
112
|
+
"""
|
|
113
|
+
data = {}
|
|
114
|
+
|
|
115
|
+
if value is None and index is None:
|
|
116
|
+
raise ValueError('Either value, index, or both must be provided.')
|
|
117
|
+
if value is not None:
|
|
118
|
+
data.update({'value': value})
|
|
119
|
+
if index is not None:
|
|
120
|
+
data.update({'index': index})
|
|
121
|
+
|
|
122
|
+
existing_value = quote(existing_value, safe='') # encode possible slashes in vocabulary value
|
|
123
|
+
response_json, _ = self._client._put(self._build_path(property=f'{display_key}/vocabulary/{existing_value}'), data=data)
|
|
124
|
+
return response_json
|
|
125
|
+
|
|
126
|
+
def delete_controlled_vocabulary_value(self, display_key: str, existing_value: str) -> dict:
|
|
127
|
+
"""
|
|
128
|
+
Delete an existing value from the controlled vocabulary for a metadata field.
|
|
129
|
+
:param display_key: Display key of the controlled vocabulary metadata field.
|
|
130
|
+
:param existing_value: The controlled vocabulary value you wish to delete.
|
|
131
|
+
"""
|
|
132
|
+
existing_value = quote(existing_value, safe='') # encode possible slashes in vocabulary value
|
|
133
|
+
response_json, _ = self._client._delete(self._build_path(property=f'{display_key}/vocabulary/{existing_value}'))
|
|
134
|
+
return response_json
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import BinaryIO
|
|
2
|
+
|
|
3
|
+
from ..baseapi import BaseApi
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Uploads(BaseApi):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
"""
|
|
9
|
+
Initialize the endpoint.
|
|
10
|
+
"""
|
|
11
|
+
self.endpoint = 'uploads'
|
|
12
|
+
super().__init__(*args, **kwargs)
|
|
13
|
+
|
|
14
|
+
def list_profiles(self):
|
|
15
|
+
"""
|
|
16
|
+
List profiles.
|
|
17
|
+
"""
|
|
18
|
+
response_json, _ = self._client._get(self._build_path(property='profiles'))
|
|
19
|
+
return response_json
|
|
20
|
+
|
|
21
|
+
def upload_asset(self, profile: str, file: BinaryIO, filename: str) -> dict:
|
|
22
|
+
"""
|
|
23
|
+
Uploads a new asset.
|
|
24
|
+
|
|
25
|
+
:param profile: Upload Profile Name.
|
|
26
|
+
:param file: Optional binary file for upload. Either the file, the url parameter, or the file id must be provided with this call.
|
|
27
|
+
:param filename: Filename (overrides name of MIME file).
|
|
28
|
+
"""
|
|
29
|
+
post_data = {
|
|
30
|
+
'profile': profile,
|
|
31
|
+
'filename': filename,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
files = {
|
|
35
|
+
'file': file,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
response_json, _ = self._client._post(self._build_path(), post_data, files)
|
|
39
|
+
return response_json
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: widen-client
|
|
3
|
+
Version: 0.0.15
|
|
4
|
+
Summary: Python client for interacting with Widen v2 API
|
|
5
|
+
Author-email: Gagosian Gallery Engineering <engineering@gagosian.com>
|
|
6
|
+
License: UNLICENSED
|
|
7
|
+
|
|
8
|
+
Project-URL: Homepage, https://github.com/gagosian/widen-client
|
|
9
|
+
Project-URL: Repository, https://github.com/gagosian/widen-client
|
|
10
|
+
Keywords: widen,dam,rest,client,wrapper
|
|
11
|
+
Classifier: Environment :: Web Environment
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: requests>=2.32.5
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Widen Client
|
|
26
|
+
A Python client for interacting with Widen v2 API.
|
|
27
|
+
|
|
28
|
+
Documentation: https://widenv2.docs.apiary.io/#
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
This client is privately hosted. To install, run
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipenv install -e git+ssh://git@github.com/gagosian/widen-client.git#egg=widen-client
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
```python
|
|
40
|
+
from widen_client import Widen
|
|
41
|
+
|
|
42
|
+
widen = Widen(access_token='...')
|
|
43
|
+
|
|
44
|
+
widen.assets.search('dBnumber:CHUNG 2021.0001')
|
|
45
|
+
|
|
46
|
+
widen.assets.get('46579cd6-f58d-49ed-b4af-ea32c84ab20f')
|
|
47
|
+
|
|
48
|
+
widen.assets.get_metadata('46579cd6-f58d-49ed-b4af-ea32c84ab20f')
|
|
49
|
+
|
|
50
|
+
widen.assets.update_metadata('46579cd6-f58d-49ed-b4af-ea32c84ab20f', {
|
|
51
|
+
'fields': {
|
|
52
|
+
'alternateCatalogNumber': [],
|
|
53
|
+
'artist': ['Prince, Richard'],
|
|
54
|
+
'artistEndYear': ['2004'],
|
|
55
|
+
'artworkParentheticalTitle': [''],
|
|
56
|
+
'artworkTitle': ['Untitled'],
|
|
57
|
+
'artworkType': ['sculpture'],
|
|
58
|
+
'description': ['This is not a dog.'],
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Available Entities
|
|
64
|
+
The following entities are available:
|
|
65
|
+
|
|
66
|
+
- Assets (`/assets/`)
|
|
67
|
+
- Metadata (`/metadata/`)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
widen_client/__init__.py,sha256=98xlZqJ_CVlvjOuB-xc51lEtZpbVegSPmC_B90TxsZU,303
|
|
2
|
+
widen_client/baseapi.py,sha256=j5TQCXZtJfzhJpht4tCaYtWGJ1dsoYGwig-z19GkSVE,1564
|
|
3
|
+
widen_client/client.py,sha256=AA7dphFkCAbkGseABytWXYuQqRGMminbjsP7z4wr2Qo,13196
|
|
4
|
+
widen_client/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
widen_client/entities/assets.py,sha256=NSM5ENtGL8XUWNXegu6T2-xn10N5FCWb813KyiieuT4,3731
|
|
6
|
+
widen_client/entities/metadata.py,sha256=O6pVK7XX3zekqks3timnYIDcMcrDpUxKsQO6IcYdmhM,6402
|
|
7
|
+
widen_client/entities/uploads.py,sha256=BJsrkgkdViyBqh2MVkDsmwIcvgIO8p50Oa2e01gD57o,1100
|
|
8
|
+
widen_client-0.0.15.dist-info/licenses/LICENSE,sha256=IkOii96WG8-llXw9Ctmq4BsLwELw09KJCJglpxsIxFw,11
|
|
9
|
+
widen_client-0.0.15.dist-info/METADATA,sha256=YzlOjsO9J3JUso9s0DD6QRYEdRL7mBQcYShhAOiW7gE,1953
|
|
10
|
+
widen_client-0.0.15.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
11
|
+
widen_client-0.0.15.dist-info/top_level.txt,sha256=4pzRQUeykfNuKG4ygxokWt8kRHpgVW_-PZhG1Pr4KQI,13
|
|
12
|
+
widen_client-0.0.15.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UNLICENSED
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
widen_client
|