@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.
- package/README.md +18 -18
- package/lib/commands/connector/build.js +168 -44
- package/lib/commands/connector/build.js.map +1 -1
- package/lib/commands/connector/sign.js +108 -12
- package/lib/commands/connector/sign.js.map +1 -1
- package/lib/commands/connector/validate.js +110 -10
- package/lib/commands/connector/validate.js.map +1 -1
- package/lib/commands/migrator/convert.d.ts +3 -0
- package/lib/commands/migrator/convert.js +201 -20
- package/lib/commands/migrator/convert.js.map +1 -1
- package/lib/templates/migrator-runners/plugin_override.txt +76 -4
- package/lib/templates/migrator-runners/runner_override.txt +30 -0
- package/lib/templates/migrator-runners/script_override.txt +77 -5
- package/lib/templates/swimlane/__init__.py +18 -0
- package/lib/templates/swimlane/core/__init__.py +0 -0
- package/lib/templates/swimlane/core/adapters/__init__.py +10 -0
- package/lib/templates/swimlane/core/adapters/app.py +59 -0
- package/lib/templates/swimlane/core/adapters/app_revision.py +49 -0
- package/lib/templates/swimlane/core/adapters/helper.py +84 -0
- package/lib/templates/swimlane/core/adapters/record.py +468 -0
- package/lib/templates/swimlane/core/adapters/record_revision.py +43 -0
- package/lib/templates/swimlane/core/adapters/report.py +65 -0
- package/lib/templates/swimlane/core/adapters/task.py +58 -0
- package/lib/templates/swimlane/core/adapters/usergroup.py +183 -0
- package/lib/templates/swimlane/core/bulk.py +48 -0
- package/lib/templates/swimlane/core/cache.py +165 -0
- package/lib/templates/swimlane/core/client.py +466 -0
- package/lib/templates/swimlane/core/cursor.py +100 -0
- package/lib/templates/swimlane/core/fields/__init__.py +46 -0
- package/lib/templates/swimlane/core/fields/attachment.py +82 -0
- package/lib/templates/swimlane/core/fields/base/__init__.py +15 -0
- package/lib/templates/swimlane/core/fields/base/cursor.py +90 -0
- package/lib/templates/swimlane/core/fields/base/field.py +149 -0
- package/lib/templates/swimlane/core/fields/base/multiselect.py +116 -0
- package/lib/templates/swimlane/core/fields/comment.py +48 -0
- package/lib/templates/swimlane/core/fields/datetime.py +112 -0
- package/lib/templates/swimlane/core/fields/history.py +28 -0
- package/lib/templates/swimlane/core/fields/list.py +266 -0
- package/lib/templates/swimlane/core/fields/number.py +38 -0
- package/lib/templates/swimlane/core/fields/reference.py +169 -0
- package/lib/templates/swimlane/core/fields/text.py +30 -0
- package/lib/templates/swimlane/core/fields/tracking.py +10 -0
- package/lib/templates/swimlane/core/fields/usergroup.py +137 -0
- package/lib/templates/swimlane/core/fields/valueslist.py +70 -0
- package/lib/templates/swimlane/core/resolver.py +46 -0
- package/lib/templates/swimlane/core/resources/__init__.py +0 -0
- package/lib/templates/swimlane/core/resources/app.py +136 -0
- package/lib/templates/swimlane/core/resources/app_revision.py +43 -0
- package/lib/templates/swimlane/core/resources/attachment.py +64 -0
- package/lib/templates/swimlane/core/resources/base.py +55 -0
- package/lib/templates/swimlane/core/resources/comment.py +33 -0
- package/lib/templates/swimlane/core/resources/record.py +499 -0
- package/lib/templates/swimlane/core/resources/record_revision.py +44 -0
- package/lib/templates/swimlane/core/resources/report.py +259 -0
- package/lib/templates/swimlane/core/resources/revision_base.py +69 -0
- package/lib/templates/swimlane/core/resources/task.py +16 -0
- package/lib/templates/swimlane/core/resources/usergroup.py +166 -0
- package/lib/templates/swimlane/core/search.py +31 -0
- package/lib/templates/swimlane/core/wrappedsession.py +12 -0
- package/lib/templates/swimlane/exceptions.py +191 -0
- package/lib/templates/swimlane/utils/__init__.py +132 -0
- package/lib/templates/swimlane/utils/date_validator.py +4 -0
- package/lib/templates/swimlane/utils/list_validator.py +7 -0
- package/lib/templates/swimlane/utils/str_validator.py +10 -0
- package/lib/templates/swimlane/utils/version.py +101 -0
- package/lib/transformers/base-transformer.js +61 -14
- package/lib/transformers/base-transformer.js.map +1 -1
- package/lib/transformers/connector-generator.d.ts +104 -2
- package/lib/transformers/connector-generator.js +1234 -51
- package/lib/transformers/connector-generator.js.map +1 -1
- package/lib/types/migrator-types.d.ts +22 -0
- package/lib/types/migrator-types.js.map +1 -1
- package/oclif.manifest.json +1 -1
- 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
|