@sw-tsdk/plugin-connector 3.13.1 → 3.13.2-next.3dfd44a

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 (74) hide show
  1. package/README.md +18 -18
  2. package/lib/commands/connector/build.js +168 -44
  3. package/lib/commands/connector/build.js.map +1 -1
  4. package/lib/commands/connector/sign.js +108 -12
  5. package/lib/commands/connector/sign.js.map +1 -1
  6. package/lib/commands/connector/validate.js +110 -10
  7. package/lib/commands/connector/validate.js.map +1 -1
  8. package/lib/commands/migrator/convert.d.ts +3 -0
  9. package/lib/commands/migrator/convert.js +201 -20
  10. package/lib/commands/migrator/convert.js.map +1 -1
  11. package/lib/templates/migrator-runners/plugin_override.txt +76 -4
  12. package/lib/templates/migrator-runners/runner_override.txt +30 -0
  13. package/lib/templates/migrator-runners/script_override.txt +77 -5
  14. package/lib/templates/swimlane/__init__.py +18 -0
  15. package/lib/templates/swimlane/core/__init__.py +0 -0
  16. package/lib/templates/swimlane/core/adapters/__init__.py +10 -0
  17. package/lib/templates/swimlane/core/adapters/app.py +59 -0
  18. package/lib/templates/swimlane/core/adapters/app_revision.py +49 -0
  19. package/lib/templates/swimlane/core/adapters/helper.py +84 -0
  20. package/lib/templates/swimlane/core/adapters/record.py +468 -0
  21. package/lib/templates/swimlane/core/adapters/record_revision.py +43 -0
  22. package/lib/templates/swimlane/core/adapters/report.py +65 -0
  23. package/lib/templates/swimlane/core/adapters/task.py +58 -0
  24. package/lib/templates/swimlane/core/adapters/usergroup.py +183 -0
  25. package/lib/templates/swimlane/core/bulk.py +48 -0
  26. package/lib/templates/swimlane/core/cache.py +165 -0
  27. package/lib/templates/swimlane/core/client.py +466 -0
  28. package/lib/templates/swimlane/core/cursor.py +100 -0
  29. package/lib/templates/swimlane/core/fields/__init__.py +46 -0
  30. package/lib/templates/swimlane/core/fields/attachment.py +82 -0
  31. package/lib/templates/swimlane/core/fields/base/__init__.py +15 -0
  32. package/lib/templates/swimlane/core/fields/base/cursor.py +90 -0
  33. package/lib/templates/swimlane/core/fields/base/field.py +149 -0
  34. package/lib/templates/swimlane/core/fields/base/multiselect.py +116 -0
  35. package/lib/templates/swimlane/core/fields/comment.py +48 -0
  36. package/lib/templates/swimlane/core/fields/datetime.py +112 -0
  37. package/lib/templates/swimlane/core/fields/history.py +28 -0
  38. package/lib/templates/swimlane/core/fields/list.py +266 -0
  39. package/lib/templates/swimlane/core/fields/number.py +38 -0
  40. package/lib/templates/swimlane/core/fields/reference.py +169 -0
  41. package/lib/templates/swimlane/core/fields/text.py +30 -0
  42. package/lib/templates/swimlane/core/fields/tracking.py +10 -0
  43. package/lib/templates/swimlane/core/fields/usergroup.py +137 -0
  44. package/lib/templates/swimlane/core/fields/valueslist.py +70 -0
  45. package/lib/templates/swimlane/core/resolver.py +46 -0
  46. package/lib/templates/swimlane/core/resources/__init__.py +0 -0
  47. package/lib/templates/swimlane/core/resources/app.py +136 -0
  48. package/lib/templates/swimlane/core/resources/app_revision.py +43 -0
  49. package/lib/templates/swimlane/core/resources/attachment.py +64 -0
  50. package/lib/templates/swimlane/core/resources/base.py +55 -0
  51. package/lib/templates/swimlane/core/resources/comment.py +33 -0
  52. package/lib/templates/swimlane/core/resources/record.py +499 -0
  53. package/lib/templates/swimlane/core/resources/record_revision.py +44 -0
  54. package/lib/templates/swimlane/core/resources/report.py +259 -0
  55. package/lib/templates/swimlane/core/resources/revision_base.py +69 -0
  56. package/lib/templates/swimlane/core/resources/task.py +16 -0
  57. package/lib/templates/swimlane/core/resources/usergroup.py +166 -0
  58. package/lib/templates/swimlane/core/search.py +31 -0
  59. package/lib/templates/swimlane/core/wrappedsession.py +12 -0
  60. package/lib/templates/swimlane/exceptions.py +191 -0
  61. package/lib/templates/swimlane/utils/__init__.py +132 -0
  62. package/lib/templates/swimlane/utils/date_validator.py +4 -0
  63. package/lib/templates/swimlane/utils/list_validator.py +7 -0
  64. package/lib/templates/swimlane/utils/str_validator.py +10 -0
  65. package/lib/templates/swimlane/utils/version.py +101 -0
  66. package/lib/transformers/base-transformer.js +61 -14
  67. package/lib/transformers/base-transformer.js.map +1 -1
  68. package/lib/transformers/connector-generator.d.ts +104 -2
  69. package/lib/transformers/connector-generator.js +1234 -51
  70. package/lib/transformers/connector-generator.js.map +1 -1
  71. package/lib/types/migrator-types.d.ts +22 -0
  72. package/lib/types/migrator-types.js.map +1 -1
  73. package/oclif.manifest.json +1 -1
  74. package/package.json +6 -6
@@ -0,0 +1,466 @@
1
+ """Core Swimlane client class"""
2
+
3
+ import logging
4
+
5
+ import jwt
6
+ import pendulum
7
+ import requests
8
+ import time
9
+ from pyuri import URI
10
+ from requests.compat import json
11
+ from requests.packages import urllib3
12
+ from requests.structures import CaseInsensitiveDict
13
+ from requests.exceptions import ConnectionError
14
+ from six.moves.urllib.parse import urljoin
15
+
16
+ from swimlane.core.adapters import GroupAdapter, UserAdapter, AppAdapter, HelperAdapter
17
+ from swimlane.core.cache import ResourcesCache
18
+ from swimlane.core.resolver import SwimlaneResolver
19
+ from swimlane.core.resources.usergroup import User
20
+ from swimlane.exceptions import SwimlaneHTTP400Error, InvalidSwimlaneProductVersion
21
+ from swimlane.utils.version import get_package_version, compare_versions
22
+ from swimlane.core.wrappedsession import WrappedSession
23
+
24
+ # Disable insecure request warnings
25
+ urllib3.disable_warnings()
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ # pylint: disable=invalid-name
30
+ _lib_full_version = get_package_version()
31
+ _lib_major_version, _lib_minor_version = _lib_full_version.split('.')[0:2]
32
+
33
+
34
+ class Swimlane(object):
35
+ """Swimlane API client
36
+
37
+ Core class used throughout library for all API requests and server interactions
38
+
39
+ Args:
40
+ host (str): Full RFC-1738 URL pointing to Swimlane host. Defaults will be provided for all parts
41
+ username (str): Authentication username
42
+ password (str): Authentication password
43
+ verify_ssl (bool): Verify SSL (ignored on HTTP). Disable to use self-signed certificates
44
+ default_timeout (int): Default request connect and read timeout in seconds for all requests
45
+ verify_server_version (bool): Verify server version has same major version as client package. May require
46
+ additional requests, set False to disable check
47
+ resource_cache_size (int): Maximum number of each resource type to keep in memory cache. Set 0 to disable
48
+ caching. Disabled by default
49
+ access_token (str): Authentication token, used in lieu of a username and password
50
+ write_to_read_only (bool): Enable the ability to write to Read-only fields
51
+ retry (bool): Retry request when error code is >= 500
52
+ max_retries (int): Maximum number of retry attempts
53
+ retry_interval (int): Time interval (in seconds) between two retry attempts
54
+
55
+ Attributes:
56
+ host (pyuri.URI): Full RFC-1738 URL pointing to Swimlane host
57
+ apps (AppAdapter): :class:`~swimlane.core.adapters.app.AppAdapter` configured for current Swimlane instance
58
+ users (UserAdapter): :class:`~swimlane.core.adapters.usergroup.UserAdapter` configured for current
59
+ Swimlane instance
60
+ groups (GroupAdapter): :class:`~swimlane.core.adapters.usergroup.GroupAdapter` configured for current
61
+ Swimlane instance
62
+ resources_cache (ResourcesCache): Cache checked by all supported adapters for current Swimlane instance
63
+
64
+ Examples:
65
+
66
+ ::
67
+
68
+ # Establish connection using username password
69
+ swimlane = Swimlane(
70
+ 'https://192.168.1.1',
71
+ 'username',
72
+ 'password',
73
+ verify_ssl=False
74
+ )
75
+
76
+ # Or establish connection using personal access token
77
+ swimlane = Swimlane(
78
+ 'https://192.168.1.1',
79
+ access_token='abcdefg',
80
+ verify_ssl=False
81
+ )
82
+
83
+ # Retrieve an app
84
+ app = swimlane.apps.get(name='Target App')
85
+
86
+ """
87
+
88
+ #_api_root = '/api/''
89
+ # Turbine variables
90
+ turbineAccountId = ""
91
+ turbineTenantId = ""
92
+ _api_root = f'/api/account/{turbineAccountId}/tenant/{turbineTenantId}/'
93
+ _execute_task_webhook_url = ""
94
+
95
+ def __init__(
96
+ self,
97
+ host,
98
+ username=None,
99
+ password=None,
100
+ verify_ssl=True,
101
+ default_timeout=60,
102
+ verify_server_version=True,
103
+ resource_cache_size=0,
104
+ access_token=None,
105
+ write_to_read_only: bool=False,
106
+ retry: bool=True,
107
+ max_retries: int=5,
108
+ retry_interval: int=5
109
+ ):
110
+ self.__verify_auth_params(username, password, access_token)
111
+
112
+ self.host = URI(host)
113
+ self.host.scheme = (self.host.scheme or 'https').lower()
114
+ self.host.path = None
115
+
116
+ self.resources_cache = ResourcesCache(resource_cache_size)
117
+
118
+ self.__settings = None
119
+ self.__user = None
120
+
121
+ self._write_to_read_only = write_to_read_only
122
+
123
+ self._default_timeout = default_timeout
124
+
125
+ self._session = WrappedSession()
126
+ self._session.verify = verify_ssl
127
+
128
+ self.retry = retry
129
+ self.max_retries = max_retries
130
+ self.retry_interval = retry_interval
131
+
132
+ if username is not None and password is not None:
133
+ self._session.auth = SwimlaneJwtAuth(
134
+ self,
135
+ username,
136
+ password
137
+ )
138
+ else:
139
+ self._session.auth = SwimlaneTokenAuth(
140
+ self,
141
+ access_token
142
+ )
143
+
144
+ self.apps = AppAdapter(self)
145
+ self.users = UserAdapter(self)
146
+ self.groups = GroupAdapter(self)
147
+ self.helpers = HelperAdapter(self)
148
+
149
+ #if verify_server_version:
150
+ # self.__verify_server_version()
151
+
152
+ @staticmethod
153
+ def __verify_auth_params(username, password, access_token):
154
+ """Verify that valid authentication parameters were passed to __init__"""
155
+
156
+ if all(v is not None for v in [username, password, access_token]):
157
+ raise ValueError('Cannot supply a username/password and a access token')
158
+
159
+ if (username is None or password is None) and access_token is None:
160
+ raise ValueError('Must supply a username/password or access token')
161
+
162
+ def __verify_server_version(self):
163
+ """Verify connected to supported server product version
164
+
165
+ Notes:
166
+ Logs warning if connecting to a newer minor server version
167
+
168
+ Raises:
169
+ swimlane.exceptions.InvalidServerVersion: If server major version is higher than package major version
170
+ """
171
+ if compare_versions('.'.join([_lib_major_version, _lib_minor_version]), self.product_version) > 0:
172
+ logger.warning('Client version {} connecting to server with newer minor release {}.'.format(
173
+ _lib_full_version,
174
+ self.product_version
175
+ ))
176
+
177
+ if compare_versions(_lib_major_version, self.product_version) != 0:
178
+ raise InvalidSwimlaneProductVersion(
179
+ self,
180
+ '{}.0'.format(_lib_major_version),
181
+ '{}.0'.format(str(int(_lib_major_version) + 1))
182
+ )
183
+
184
+ def __repr__(self):
185
+ return '<{cls}: {user} @ {host} v{version}>'.format(
186
+ cls=self.__class__.__name__,
187
+ user=self.user,
188
+ host=self.host,
189
+ version=self.version
190
+ )
191
+
192
+ def request(self, method, api_endpoint, **kwargs):
193
+ """Wrapper for underlying :class:`requests.Session`
194
+
195
+ Handles generating full API URL, session reuse and auth, request defaults, and invalid response status codes
196
+
197
+ Used throughout library as the core underlying request/response method for all interactions with server
198
+
199
+ Args:
200
+ method (str): Request method (get, post, put, etc.)
201
+ api_endpoint (str): Portion of URL matching API endpoint route as listed in platform /docs help page
202
+ **kwargs (dict): Remaining arguments passed through to actual request call
203
+
204
+ Notes:
205
+ All other provided kwargs are passed to underlying ``requests.Session.request()`` call
206
+
207
+ Raises:
208
+ swimlane.exceptions.SwimlaneHTTP400Error: On 400 responses with additional context about the exception
209
+ requests.HTTPError: Any other 4xx/5xx HTTP responses
210
+
211
+ Returns:
212
+ requests.Response: Successful response instances
213
+
214
+ Examples:
215
+
216
+ Request and parse server settings endpoint response
217
+
218
+ >>> server_settings = swimlane.request('get', 'settings').json()
219
+ """
220
+ while api_endpoint.startswith('/'):
221
+ api_endpoint = api_endpoint[1:]
222
+
223
+ # Ensure a timeout is set
224
+ kwargs.setdefault('timeout', self._default_timeout)
225
+
226
+ # Manually grab and dump json data to have full control over serialization
227
+ # Emulate default requests behavior
228
+ json_data = kwargs.pop('json', None)
229
+ if json_data is not None:
230
+ headers = CaseInsensitiveDict(kwargs.get('headers', {}))
231
+ headers.setdefault('Content-Type', 'application/json')
232
+ kwargs['headers'] = headers
233
+
234
+ kwargs['data'] = json.dumps(json_data, sort_keys=True, separators=(',', ':'))
235
+
236
+ # Retry logic
237
+ req_retry = kwargs.pop('retry', self.retry)
238
+
239
+ req_max_retries = kwargs.pop('max_retries', self.max_retries)
240
+ if not isinstance(req_max_retries, int):
241
+ raise TypeError('max_retries should be an integer')
242
+ if req_max_retries <= 0:
243
+ raise ValueError('max_retries should be a positive integer')
244
+
245
+ req_retry_interval = kwargs.pop('retry_interval', self.retry_interval)
246
+ if not isinstance(req_retry_interval, int):
247
+ raise TypeError('retry_interval should be an integer')
248
+ if req_retry_interval <= 0:
249
+ raise ValueError('retry_interval should be a positive integer')
250
+
251
+ while not req_max_retries<0:
252
+ if api_endpoint == 'tenant/api/users/login':
253
+ response = self._session.request(method, urljoin(str(self.host), api_endpoint), **kwargs)
254
+ else:
255
+ response = self._session.request(method, urljoin(str(self.host) + self._api_root, api_endpoint), **kwargs)
256
+
257
+ # Roll 400 errors up into SwimlaneHTTP400Errors with specific Swimlane error code support
258
+ try:
259
+ response.raise_for_status()
260
+ # Exit loop on successful request
261
+ req_max_retries = -1
262
+ except requests.HTTPError as error:
263
+ if error.response.status_code == 400:
264
+ raise SwimlaneHTTP400Error(error)
265
+ else:
266
+ if req_retry and req_max_retries>0 and error.response.status_code>=500:
267
+ req_max_retries -= 1
268
+ time.sleep(req_retry_interval)
269
+ continue
270
+ elif req_max_retries == 0:
271
+ raise ConnectionError(f'Max retries exceeded. Caused by ({error})')
272
+ raise error
273
+
274
+ return response
275
+
276
+ @property
277
+ def settings(self):
278
+ """Retrieve and cache settings from server"""
279
+ if not self.__settings:
280
+ self.__settings = self.request('get', 'settings').json()
281
+ return self.__settings
282
+
283
+ @property
284
+ def version(self):
285
+ """Full Swimlane version, <product_version>+<build_version>+<build_number>"""
286
+ return self.settings['apiVersion']
287
+
288
+ @property
289
+ def product_version(self):
290
+ """Swimlane product version"""
291
+ version_separator = '+'
292
+ if version_separator in self.version:
293
+ # Post product/build version separation
294
+ return self.version.split(version_separator)[0]
295
+ # Pre product/build version separation
296
+ return self.version.split('-')[0]
297
+
298
+ @property
299
+ def build_version(self):
300
+ """Swimlane semantic build version
301
+
302
+ Falls back to product version in pre-2.18 releases
303
+ """
304
+ version_separator = '+'
305
+ if version_separator in self.version:
306
+ # Post product/build version separation
307
+ # This will handle <product_version>+<build_version>+<build_number>
308
+ # or <build_version>+<build_number> formats of the version
309
+ return self.version.split(version_separator)[-2]
310
+ # Pre product/build version separation
311
+ return self.product_version
312
+
313
+ @property
314
+ def build_number(self):
315
+ """Swimlane build number"""
316
+ version_separator = '+'
317
+ if version_separator in self.version:
318
+ # Post product/build version separation
319
+ return self.version.split(version_separator)[2]
320
+ # Pre product/build version separation
321
+ return self.version.split('-')[1]
322
+
323
+ @property
324
+ def user(self):
325
+ """User record instance for authenticated user"""
326
+ return self._session.auth.user
327
+
328
+
329
+ class SwimlaneTokenAuth(SwimlaneResolver):
330
+ """Handles token authentication for all requests
331
+
332
+ .. versionadded:: 4.1.0
333
+ """
334
+
335
+ def __init__(self, swimlane, access_token):
336
+ super(SwimlaneTokenAuth, self).__init__(swimlane)
337
+
338
+ self._access_token = access_token
339
+ self.user = None
340
+
341
+ def __call__(self, request):
342
+ """Attach necessary headers to all requests"""
343
+
344
+ headers = {
345
+ 'Private-Token': self._access_token
346
+ }
347
+
348
+ request.headers.update(headers)
349
+
350
+ # Only make the call to user/authorize to get the user's profile if we haven't retrieved it
351
+ # already
352
+ if self.user is not None:
353
+ return request
354
+
355
+ # Temporarily remove auth from Swimlane session for auth request to avoid recursive loop during the request
356
+ self._swimlane._session.auth = None
357
+ resp = self._swimlane.request(
358
+ 'get',
359
+ 'user/authorize',
360
+ headers=headers
361
+ )
362
+ self._swimlane._session.auth = self
363
+
364
+ json_content = resp.json()
365
+ self.user = User(self._swimlane, _user_raw_from_login_content(json_content))
366
+
367
+ return request
368
+
369
+
370
+ class SwimlaneJwtAuth(SwimlaneResolver):
371
+ """Handles authentication for all requests"""
372
+
373
+ _token_expiration_buffer = pendulum.Duration(minutes=5)
374
+
375
+ def __init__(self, swimlane, username, password):
376
+ super(SwimlaneJwtAuth, self).__init__(swimlane)
377
+
378
+ self._username = username
379
+ self._password = password
380
+
381
+ self.user = None
382
+ self._login_headers = {}
383
+ self._token_expiration = pendulum.now()
384
+
385
+ def __call__(self, request):
386
+ """Attach necessary headers to all requests
387
+
388
+ Automatically reauthenticate before sending request when nearing token expiration
389
+ """
390
+
391
+ # Refresh token if it expires soon
392
+ if pendulum.now() + self._token_expiration_buffer >= self._token_expiration:
393
+ self.authenticate()
394
+
395
+ request.headers.update(self._login_headers)
396
+
397
+ return request
398
+
399
+ def authenticate(self):
400
+ """Send login request and update User instance, login headers, and token expiration"""
401
+
402
+ # Temporarily remove auth from Swimlane session for auth request to avoid recursive loop during login request
403
+ self._swimlane._session.auth = None
404
+ resp = self._swimlane.request(
405
+ 'post',
406
+ #'user/login',
407
+ '/tenant/api/users/login',
408
+ json={
409
+ 'userName': self._username,
410
+ 'password': self._password
411
+ },
412
+ )
413
+ self._swimlane._session.auth = self
414
+
415
+ # Get JWT from response content
416
+ json_content = resp.json()
417
+ token = json_content.pop('token', None)
418
+
419
+ # Grab token expiration
420
+ token_data = jwt.decode(token, algorithms=[''], options={'verify_signature': False})
421
+ token_expiration = pendulum.from_timestamp(token_data['exp'])
422
+
423
+ headers = {
424
+ 'Authorization': 'Bearer {}'.format(token)
425
+ }
426
+
427
+ # Create User instance for authenticating user from login response data
428
+ user = User(self._swimlane, _user_raw_from_login_content(json_content))
429
+
430
+ self._login_headers = headers
431
+ self.user = user
432
+ self._token_expiration = token_expiration
433
+
434
+
435
+ def _user_raw_from_login_content(login_content):
436
+ """Returns a User instance with appropriate raw data parsed from login response content"""
437
+ matching_keys = [
438
+ 'displayName',
439
+ 'lastLogin',
440
+ 'active',
441
+ 'name',
442
+ 'isMe',
443
+ 'lastPasswordChangedDate',
444
+ 'passwordResetRequired',
445
+ 'groups',
446
+ 'roles',
447
+ 'email',
448
+ 'isAdmin',
449
+ 'createdDate',
450
+ 'modifiedDate',
451
+ 'createdByUser',
452
+ 'modifiedByUser',
453
+ 'userName',
454
+ 'id',
455
+ 'disabled'
456
+ ]
457
+
458
+ raw_data = {
459
+ '$type': User._type,
460
+ }
461
+
462
+ for key in matching_keys:
463
+ if key in login_content:
464
+ raw_data[key] = login_content[key]
465
+
466
+ return raw_data
@@ -0,0 +1,100 @@
1
+ import itertools
2
+
3
+
4
+ class Cursor(object):
5
+
6
+ def __init__(self):
7
+ self._elements = []
8
+
9
+ def __len__(self):
10
+ return len(list(self._evaluate()))
11
+
12
+ def __iter__(self):
13
+ for element in self._evaluate():
14
+ yield element
15
+
16
+ def __getitem__(self, item):
17
+ return self._evaluate()[item]
18
+
19
+ def _evaluate(self):
20
+ """Hook to allow lazy evaluation or retrieval of cursor's elements
21
+
22
+ Defaults to simply returning list of self._elements
23
+ """
24
+ return self._elements
25
+
26
+
27
+ class PaginatedCursor(Cursor):
28
+ """Handle paginated lists, exposes hooks to simplify retrieval and parsing of paginated data"""
29
+
30
+ default_limit = 0
31
+ default_page_size = 10
32
+ default_page_start = None
33
+ default_page_end = None
34
+
35
+ def __init__(self, limit=default_limit, page_size=default_page_size,
36
+ page_start=default_page_start, page_end=default_page_end):
37
+ super(PaginatedCursor, self).__init__()
38
+
39
+ self.__limit = limit
40
+ self.page_size = page_size
41
+ self.page_start = page_start
42
+ self.page_end = page_end
43
+
44
+ if self.__limit:
45
+ self.page_size = min(self.page_size, self.__limit)
46
+
47
+ if self.page_start and self.page_start <= 0:
48
+ raise ValueError('page_start should be greater than 0')
49
+
50
+ if self.page_end and self.page_end <= 0:
51
+ raise ValueError('page_end should be greater than 0')
52
+
53
+ if (self.page_start and self.page_end) and (self.page_start > self.page_end):
54
+ raise ValueError('page_end cannot be less than page_start')
55
+
56
+ if (self.page_start or self.page_end) and self.__limit != 0:
57
+ raise ValueError(' page_start or page_end param is applicable only when limit is 0')
58
+
59
+ def _evaluate(self):
60
+ """Lazily retrieve and paginate report results and build Record instances from returned data"""
61
+ if self._elements:
62
+ for element in self._elements:
63
+ yield element
64
+ else:
65
+ # Determine pagination range based on parameters
66
+ if self.page_start and self.page_end:
67
+ page_range = range(self.page_start-1, self.page_end)
68
+ elif self.page_start:
69
+ page_range = itertools.count(self.page_start-1)
70
+ elif self.page_end:
71
+ page_range = range(0, self.page_end)
72
+ else:
73
+ page_range = itertools.count()
74
+
75
+ for page in page_range:
76
+ raw_elements = self._retrieve_raw_elements(page)
77
+
78
+ for raw_element in raw_elements:
79
+ element = self._parse_raw_element(raw_element)
80
+ self._elements.append(element)
81
+ yield element
82
+
83
+ if self.__limit and len(self._elements) >= self.__limit:
84
+ break
85
+
86
+ # Break conditions for ending pagination
87
+ if any([
88
+ len(raw_elements) < self.page_size,
89
+ self.__limit and len(self._elements) >= self.__limit,
90
+ self.page_size == 0
91
+ ]):
92
+ break
93
+
94
+ def _retrieve_raw_elements(self, page):
95
+ """Send request and return response for single page of data"""
96
+ raise NotImplementedError
97
+
98
+ def _parse_raw_element(self, raw_element):
99
+ """Hook to override parsing individual raw elements just before yielding"""
100
+ return raw_element
@@ -0,0 +1,46 @@
1
+ """Abstractions for Swimlane app field types to simplify getting/setting values on records"""
2
+
3
+ from six import string_types as _string_types
4
+
5
+ from swimlane.core.fields.base import Field
6
+ from swimlane.utils import (
7
+ get_recursive_subclasses as _get_recursive_subclasses,
8
+ import_submodules as _import_submodules
9
+ )
10
+
11
+ _import_submodules(__name__)
12
+
13
+
14
+ def _build_field_type_map(base_class):
15
+ """Create mapping from all $type values to their respective Field classes"""
16
+ mapping = {}
17
+
18
+ for cls in _get_recursive_subclasses(base_class):
19
+ if cls.field_type:
20
+ if isinstance(cls.field_type, tuple):
21
+ for field_type in cls.field_type:
22
+ mapping[field_type] = cls
23
+ elif isinstance(cls.field_type, _string_types):
24
+ mapping[cls.field_type] = cls
25
+ else:
26
+ raise ValueError('Field type must be str or tuple, cannot understand type "{}" on class "{}"'.format(
27
+ type(cls.field_type),
28
+ cls
29
+ ))
30
+
31
+ return mapping
32
+
33
+
34
+ _FIELD_TYPE_MAP = _build_field_type_map(Field)
35
+
36
+
37
+ def resolve_field_class(field_definition):
38
+ """Return field class most fitting of provided Swimlane field definition"""
39
+ try:
40
+ return _FIELD_TYPE_MAP[field_definition['$type']]
41
+ except KeyError as error:
42
+ error.message = 'No field available to handle Swimlane $type "{}"'.format(field_definition)
43
+ raise
44
+
45
+
46
+ __all__ = ['resolve_field_class'] + [f.__class__.__name__ for f in _FIELD_TYPE_MAP.values()]
@@ -0,0 +1,82 @@
1
+ import io
2
+ import mimetypes
3
+
4
+ from swimlane.core.fields.base import MultiSelectField, FieldCursor
5
+ from swimlane.core.resources.attachment import Attachment
6
+ from swimlane.utils.str_validator import validate_str
7
+
8
+
9
+ class AttachmentCursor(FieldCursor):
10
+ """Allows creation and iteration of attachments"""
11
+
12
+ def add(self, filename, stream, content_type=None):
13
+ """Upload a new attachment, and add it to current fields raw data to be persisted on save
14
+
15
+ Can optionally manually set the content_type, will be guessed by provided filename extension and default to
16
+ application/octet-stream if it cannot be guessed
17
+ """
18
+ validate_str(filename, 'filename')
19
+ self.validate_stream(stream, 'stream')
20
+
21
+ # Guess file Content-Type or default
22
+ content_type = content_type or mimetypes.guess_type(
23
+ filename)[0] or 'application/octet-stream'
24
+ response = self._record._swimlane.request(
25
+ 'post',
26
+ 'attachment/{appId}/{fieldId}'.format(
27
+ appId=self._record.app.id, fieldId=self._field.id),
28
+ files={
29
+ 'file': (filename, stream, content_type)
30
+ },
31
+ )
32
+
33
+ # Returns raw attachment data as list with single element
34
+ raw_attachment_data = response.json()[0]
35
+
36
+ attachment = Attachment(self._record._swimlane, raw_attachment_data, self._record.id, self._field.id)
37
+ self._elements.append(attachment)
38
+
39
+ self._sync_field()
40
+
41
+ return attachment
42
+
43
+
44
+ def validate_stream(self, stream, key):
45
+ if not isinstance(stream, io.IOBase):
46
+ raise ValueError('{} must be a stream value.'.format(key))
47
+
48
+
49
+ class AttachmentsField(MultiSelectField):
50
+
51
+ field_type = (
52
+ 'Core.Models.Fields.AttachmentField, Core',
53
+ 'Core.Models.Fields.Attachment.AttachmentField, Core'
54
+ )
55
+ cursor_class = AttachmentCursor
56
+ supported_types = [Attachment]
57
+ bulk_modify_support = False
58
+
59
+ def __init__(self, *args, **kwargs):
60
+ """Override to force-set multiselect to always True"""
61
+ super(AttachmentsField, self).__init__(*args, **kwargs)
62
+ self.multiselect = True
63
+
64
+ def get_initial_elements(self):
65
+ raw_value = self.get_swimlane() or []
66
+
67
+ return [self.cast_to_python(raw) for raw in raw_value]
68
+
69
+ def get_batch_representation(self):
70
+ """Return best batch process representation of field value"""
71
+ return self.get_swimlane()
72
+
73
+ def _set(self, value):
74
+ """Override setter, allow clearing cursor"""
75
+ super(AttachmentsField, self)._set(value)
76
+ self._cursor = None
77
+
78
+ def cast_to_python(self, value):
79
+ return Attachment(self._swimlane, value, self.record.id, self.id)
80
+
81
+ def cast_to_swimlane(self, value):
82
+ return value._raw