@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,183 @@
1
+ from six.moves.urllib.parse import quote_plus
2
+
3
+ from swimlane.core.cache import check_cache
4
+ from swimlane.core.cursor import PaginatedCursor
5
+ from swimlane.core.resolver import SwimlaneResolver
6
+ from swimlane.core.resources.usergroup import Group, User
7
+ from swimlane.utils import one_of_keyword_only
8
+
9
+
10
+ class GroupListCursor(SwimlaneResolver, PaginatedCursor):
11
+ """Handles retrieval and pagination of group list endpoint"""
12
+
13
+ def __init__(self, swimlane, limit=None):
14
+ SwimlaneResolver.__init__(self, swimlane)
15
+ PaginatedCursor.__init__(self, limit)
16
+
17
+ def _parse_raw_element(self, raw_element):
18
+ return Group(self._swimlane, raw_element)
19
+
20
+ def _retrieve_raw_elements(self, page):
21
+ response = self._swimlane.request(
22
+ 'get',
23
+ 'groups',
24
+ params={
25
+ 'size': self.page_size,
26
+ 'pageNumber': page
27
+ }
28
+ )
29
+ return response.json().get('items', [])
30
+
31
+
32
+ class GroupAdapter(SwimlaneResolver):
33
+ """Handles retrieval of Swimlane Group resources"""
34
+
35
+ def list(self, limit=None):
36
+ """Retrieve list of all groups
37
+
38
+ Returns:
39
+ :class:`list` of :class:`~swimlane.core.resources.usergroup.Group`: List of all Groups
40
+ Raises:
41
+ ValueError: If limit is not of type integer or None
42
+ """
43
+
44
+ if (isinstance(limit, int) and limit > 0) or limit is None:
45
+ return GroupListCursor(swimlane=self._swimlane, limit=limit)
46
+
47
+ raise ValueError('Limit should be a positive whole number greater than 0')
48
+
49
+ @check_cache(Group)
50
+ @one_of_keyword_only('id', 'name')
51
+ def get(self, key, value):
52
+ """Retrieve single group record by id or name
53
+
54
+ Supports resource cache
55
+
56
+ Keyword Args:
57
+ id (str): Full Group ID
58
+ name (str): Group name
59
+
60
+ Raises:
61
+ TypeError: Unexpected or more than one keyword argument provided
62
+ ValueError: No matching group found based on provided inputs
63
+ ValueError: The lookup value is empty or None
64
+
65
+ Returns:
66
+ Group: Group instance matching provided inputs
67
+ """
68
+ if not value:
69
+ raise ValueError('The value provided for the key "{0}" cannot be empty or None'.format(key))
70
+
71
+ if key == 'id':
72
+ response = self._swimlane.request('get', 'groups/{}'.format(value))
73
+ return Group(self._swimlane, response.json())
74
+
75
+ else:
76
+ response = self._swimlane.request('get', 'groups/lookup?name={}'.format(value))
77
+ matched_groups = response.json()
78
+
79
+ for group_data in matched_groups:
80
+ if group_data.get('name') == value:
81
+ return Group(self._swimlane, group_data)
82
+
83
+ raise ValueError('Unable to find group with name "{}"'.format(value))
84
+
85
+
86
+ class UserListCursor(SwimlaneResolver, PaginatedCursor):
87
+ """Handles retrieval and pagination for user list endpoint"""
88
+
89
+ def __init__(self, swimlane, limit=None):
90
+ SwimlaneResolver.__init__(self, swimlane)
91
+ PaginatedCursor.__init__(self, limit)
92
+
93
+ def _parse_raw_element(self, raw_element):
94
+ return User(self._swimlane, raw_element)
95
+
96
+ def _retrieve_raw_elements(self, page):
97
+ response = self._swimlane.request(
98
+ 'get',
99
+ 'user',
100
+ params={
101
+ 'size': self.page_size,
102
+ 'pageNumber': page
103
+ }
104
+ )
105
+ return response.json().get('items', [])
106
+
107
+
108
+ class UserAdapter(SwimlaneResolver):
109
+ """Handles retrieval of Swimlane User resources"""
110
+
111
+ def list(self, limit=None):
112
+ """Retrieve all users
113
+
114
+ Returns:
115
+ :class:`UserListCursor`: Paginated cursor yielding :class:`User` instances
116
+
117
+ Raises:
118
+ ValueError: If limit is not of type integer or None
119
+ """
120
+
121
+ if (isinstance(limit, int) and limit > 0) or limit is None:
122
+ return UserListCursor(swimlane=self._swimlane, limit=limit)
123
+
124
+ raise ValueError('Limit should be a positive whole number greater than 0')
125
+
126
+ @check_cache(User)
127
+ @one_of_keyword_only('id', 'display_name')
128
+ def get(self, arg, value):
129
+ """Retrieve single user record by id or username
130
+
131
+ Warnings:
132
+ User display names are not unique. If using `display_name`, method will fail if multiple Users are returned
133
+ with the same display name
134
+
135
+ Keyword Args:
136
+ id (str): Full User ID
137
+ display_name (str): User display name
138
+
139
+ Returns:
140
+ User: User instance matching provided inputs
141
+
142
+ Raises:
143
+ TypeError: Unexpected or more than one keyword argument provided
144
+ ValueError: No matching user found based on provided inputs, or multiple Users with same display name
145
+ ValueError: The lookup value is empty or None
146
+ """
147
+ if not value:
148
+ raise ValueError('The value provided for the key "{0}" cannot be empty or None'.format(arg))
149
+
150
+ if arg == 'id':
151
+ response = self._swimlane.request('get', 'user/{}'.format(value))
152
+
153
+ try:
154
+ user_data = response.json()
155
+ except ValueError:
156
+ raise ValueError('Unable to find user with ID "{}"'.format(value))
157
+
158
+ return User(self._swimlane, user_data)
159
+
160
+ else:
161
+ response = self._swimlane.request('get', 'user/search?query={}'.format(quote_plus(value)))
162
+ matched_users = response.json()
163
+
164
+ # Display name not unique, fail if multiple users share the same target display name
165
+ target_matches = []
166
+
167
+ for user_data in matched_users:
168
+ user_display_name = user_data.get('displayName')
169
+ if user_display_name == value:
170
+ target_matches.append(user_data)
171
+
172
+ # No matches
173
+ if not target_matches:
174
+ raise ValueError('Unable to find user with display name "{}"'.format(value))
175
+
176
+ # Multiple matches
177
+ if len(target_matches) > 1:
178
+ raise ValueError('Multiple users returned with display name "{}". Matching user IDs: {}'.format(
179
+ value,
180
+ ', '.join(['"{}"'.format(r['id']) for r in target_matches])
181
+ ))
182
+
183
+ return User(self._swimlane, target_matches[0])
@@ -0,0 +1,48 @@
1
+ """Helpers for bulk methods"""
2
+
3
+
4
+ class _BulkModificationOperation(object):
5
+ """Base class for bulk_modify value modification operators
6
+
7
+ Acts as container to wrap the modification type with the target value for the bulk operation
8
+
9
+ Examples:
10
+
11
+ swimlane.records.bulk_modify(
12
+ record,
13
+ values={
14
+ 'Field A': 'new value',
15
+ 'Field B': Append('new value'),
16
+ 'Field C': Clear(),
17
+ ...
18
+ }
19
+ )
20
+ """
21
+
22
+ type = None
23
+
24
+ def __init__(self, value):
25
+ self.value = value
26
+
27
+
28
+ class Replace(_BulkModificationOperation):
29
+ """Bulk modification 'Replace with'/'Replace all with' operation"""
30
+ type = 'create'
31
+
32
+
33
+ class Clear(_BulkModificationOperation):
34
+ """Bulk modification 'Clear field' operation"""
35
+ type = 'delete'
36
+
37
+ def __init__(self):
38
+ super(Clear, self).__init__(None)
39
+
40
+
41
+ class Append(_BulkModificationOperation):
42
+ """Bulk modification 'Add to existing' operation"""
43
+ type = 'append'
44
+
45
+
46
+ class Remove(_BulkModificationOperation):
47
+ """Bulk modification 'Find and remove these' operation"""
48
+ type = 'subtract'
@@ -0,0 +1,165 @@
1
+ """Module providing support for automatic APIResource caching
2
+
3
+ A ResourcesCache instance is provided on all Swimlane client instances automatically
4
+
5
+ .. versionadded:: 2.16.2
6
+ """
7
+ import copy
8
+ import functools
9
+ import logging
10
+ try:
11
+ from collections.abc import defaultdict
12
+ except ImportError:
13
+ from collections import defaultdict
14
+
15
+ from cachetools import LFUCache
16
+
17
+ from swimlane.core.resources.base import APIResource
18
+
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class ResourcesCache(object):
24
+ """Universal APIResource instance cache
25
+
26
+ Uses separate caches per APIResource type, and provides mapping between available cache keys and real cache
27
+ primary key automatically
28
+ """
29
+
30
+ def __init__(self, per_cache_max_size):
31
+ self.__cache_max_size = per_cache_max_size
32
+ self.__caches = defaultdict(self.__cache_factory)
33
+ self.__cache_index_key_map = {}
34
+
35
+ if self.__cache_max_size == 0:
36
+ logger.debug('Cache size set to 0, resource caching disabled')
37
+
38
+ def __len__(self):
39
+ """Return sum of all cache sizes"""
40
+ return sum(c.currsize for c in self.__caches.values())
41
+
42
+ def __contains__(self, item):
43
+ """Check if resource is in cache, expects same 3-length tuple key as __getitem__"""
44
+ index_key = get_cache_index_key(item)
45
+ cache_key = self.__cache_index_key_map.get(index_key)
46
+ target_cache = self.__caches[index_key[0]]
47
+ return cache_key in target_cache
48
+
49
+ def __getitem__(self, item):
50
+ """Get cached resource, expects item to be 3-length tuple of (resource class, target key, target value)"""
51
+ key = get_cache_index_key(item)
52
+ cls = key[0]
53
+
54
+ # Check if in any fields index
55
+ cache_internal_key = self.__cache_index_key_map[key]
56
+
57
+ try:
58
+ # Return copy of cached object
59
+ return copy.copy(self.__caches[cls][cache_internal_key])
60
+ except KeyError:
61
+ # Internal cache miss for target resource, quietly remove from cache key map and let error bubble
62
+ self.__cache_index_key_map.pop(key, None)
63
+ raise
64
+
65
+ def __delitem__(self, resource):
66
+ """Remove resource instance from internal cache"""
67
+ self.__caches[type(resource)].pop(resource.get_cache_internal_key(), None)
68
+
69
+ def __cache_factory(self):
70
+ """Build and return a new cache instance"""
71
+ return LFUCache(self.__cache_max_size)
72
+
73
+ def cache(self, resource):
74
+ """Insert a resource instance into appropriate resource cache"""
75
+ if not isinstance(resource, APIResource):
76
+ raise TypeError('Cannot cache "{!r}", can only cache APIResource instances'.format(resource))
77
+
78
+ # Disable inserts to cache when disabled
79
+ if self.__cache_max_size == 0:
80
+ return
81
+
82
+ try:
83
+ cache_internal_key = resource.get_cache_internal_key()
84
+ cache_index_keys = resource.get_cache_index_keys().items()
85
+ except NotImplementedError:
86
+ logger.warning(
87
+ 'Not caching "{!r}", resource did not provide all necessary cache details'.format(resource)
88
+ )
89
+ else:
90
+ resource_type = type(resource)
91
+
92
+ for key, value in cache_index_keys:
93
+ self.__cache_index_key_map[(resource_type, key, value)] = cache_internal_key
94
+
95
+ self.__caches[resource_type][cache_internal_key] = resource
96
+
97
+ logger.debug('Cached "{!r}"'.format(resource))
98
+
99
+ def clear(self, *resource_types):
100
+ """Clear cache for each provided APIResource class, or all resources if no classes are provided"""
101
+ resource_types = resource_types or tuple(self.__caches.keys())
102
+
103
+ for cls in resource_types:
104
+ # Clear and delete cache instances to guarantee no lingering references
105
+ self.__caches[cls].clear()
106
+ del self.__caches[cls]
107
+
108
+
109
+ def get_cache_index_key(resource):
110
+ """Return a usable cache lookup key for an already initialized resource
111
+
112
+ Args:
113
+ resource (APIResource|tuple): APIResource instance or 3-length tuple key returned from this function
114
+
115
+ Raises:
116
+ TypeError: If resource is not an APIResource instance or acceptable 3-length tuple cache key
117
+ """
118
+ if isinstance(resource, APIResource):
119
+ attr, attr_value = list(resource.get_cache_index_keys().items())[0]
120
+ key = (type(resource), attr, attr_value)
121
+ else:
122
+ key = tuple(resource)
123
+
124
+ if len(key) != 3:
125
+ raise TypeError('Cache key must be tuple of (class, key, value), got "{!r}" instead'.format(key))
126
+
127
+ if not issubclass(key[0], APIResource):
128
+ raise TypeError('First value of cache key must be a subclass of APIResource, got "{!r}" instead'.format(key[0]))
129
+
130
+ return key
131
+
132
+
133
+ def check_cache(resource_type):
134
+ """Decorator for adapter methods to check cache for resource before normally sending requests to retrieve data
135
+
136
+ Only works with single kwargs, almost always used with @one_of_keyword_only decorator
137
+
138
+ Args:
139
+ resource_type (type(APIResource)): Subclass of APIResource of cache to be checked when called
140
+ """
141
+
142
+ def decorator(func):
143
+ @functools.wraps(func)
144
+ def wrapper(*args, **kwargs):
145
+ try:
146
+ adapter = args[0]
147
+ key, val = list(kwargs.items())[0]
148
+ except IndexError:
149
+ logger.warning("Couldn't generate full index key, skipping cache")
150
+ else:
151
+
152
+ index_key = (resource_type, key, val)
153
+ try:
154
+ cached_record = adapter._swimlane.resources_cache[index_key]
155
+ except KeyError:
156
+ logger.debug('Cache miss: "{!r}"'.format(index_key))
157
+ else:
158
+ logger.debug('Cache hit: "{!r}"'.format(cached_record))
159
+ return cached_record
160
+
161
+ # Fallback to default function call
162
+ return func(*args, **kwargs)
163
+
164
+ return wrapper
165
+ return decorator