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.
@@ -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'
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ UNLICENSED
@@ -0,0 +1 @@
1
+ widen_client