@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
|
@@ -5,7 +5,37 @@ class SwimlaneContext:
|
|
|
5
5
|
self.asset = asset
|
|
6
6
|
self.asset_schema = asset_schema
|
|
7
7
|
self.http_proxy = http_proxy
|
|
8
|
+
url = inputs.pop("SwimlaneUrl", "")
|
|
9
|
+
self.config = {
|
|
10
|
+
"ApplicationId": inputs.pop("ApplicationId", ""),
|
|
11
|
+
"RecordId": inputs.pop("RecordId", ""),
|
|
12
|
+
"InternalSwimlaneUrl": url,
|
|
13
|
+
"SwimlaneUrl": url
|
|
14
|
+
}
|
|
15
|
+
self.user = {
|
|
16
|
+
"first_name": "Swimlane",
|
|
17
|
+
"last_name": "Admin",
|
|
18
|
+
"middle_initial": "",
|
|
19
|
+
"display_name": "Swimlane Admin",
|
|
20
|
+
"last_login": "2026-02-09T18:40:10.386Z",
|
|
21
|
+
"domain": "",
|
|
22
|
+
"active": True,
|
|
23
|
+
"name": "swimlanesystem",
|
|
24
|
+
"last_password_change_date": "2026-01-22T00:31:49.783Z",
|
|
25
|
+
"password_reset_required": False,
|
|
26
|
+
"default_workspace_id": "",
|
|
27
|
+
"default_dashboard_id": "",
|
|
28
|
+
"groups": [],
|
|
29
|
+
"roles": [],
|
|
30
|
+
"primary_group": "",
|
|
31
|
+
"email": "",
|
|
32
|
+
"user_name": "swimlaneadmin",
|
|
33
|
+
"phone_number": "",
|
|
34
|
+
"disabled":False,
|
|
35
|
+
"id": ""
|
|
36
|
+
}
|
|
8
37
|
self.inputs = inputs
|
|
38
|
+
self.state = {}
|
|
9
39
|
# We purposefully do not include the sw_context.config object here as it is not supported
|
|
10
40
|
|
|
11
41
|
class Authenticate:
|
|
@@ -9,23 +9,95 @@ class RunnerOverride(RO):
|
|
|
9
9
|
super().__init__(asset, asset_schema, http_proxy)
|
|
10
10
|
|
|
11
11
|
def run(self, inputs, action_schema):
|
|
12
|
+
# Rebuild inputs from YAML-defined keys (empty per type), then merge asset, then overlay incoming inputs
|
|
13
|
+
# INPUTS_MERGE_HERE
|
|
14
|
+
|
|
12
15
|
sw_context = SwimlaneContext(asset=self.asset, asset_schema=self.asset_schema,
|
|
13
16
|
http_proxy=self.http_proxy, inputs=inputs)
|
|
14
|
-
sw_outputs =
|
|
17
|
+
sw_outputs = []
|
|
18
|
+
from swimlane import Swimlane
|
|
19
|
+
Swimlane.turbineAccountId = sw_context.inputs.pop('TurbineAccountId', '')
|
|
20
|
+
Swimlane.turbineTenantId = sw_context.inputs.pop('TurbineTenantId', '')
|
|
21
|
+
Swimlane._api_root = f'/api/account/{Swimlane.turbineAccountId}/tenant/{Swimlane.turbineTenantId}/'
|
|
22
|
+
Swimlane._execute_task_webhook_url = sw_context.inputs.pop('ExecuteTaskWebhookUrl', '')
|
|
15
23
|
|
|
16
24
|
# IMPORTED CODE BELOW
|
|
17
25
|
# HERE
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
# END OF IMPORTED CODE
|
|
29
|
+
import os
|
|
30
|
+
import base64
|
|
31
|
+
import requests
|
|
32
|
+
IPC_API_URI = os.getenv('IPC_API_URI', '')
|
|
33
|
+
IPC_API_TOKEN = os.getenv('IPC_API_TOKEN', '')
|
|
34
|
+
|
|
35
|
+
OUTPUT_DATE_CONVERSIONS = __OUTPUT_DATE_CONVERSIONS__
|
|
36
|
+
|
|
37
|
+
def convert_timestamp(value, timetype):
|
|
38
|
+
import pendulum
|
|
39
|
+
if timetype == "milliseconds":
|
|
40
|
+
value = value / 1000
|
|
41
|
+
if timetype in ('milliseconds', 'seconds'):
|
|
42
|
+
return pendulum.from_timestamp(value).to_iso8601_string()
|
|
43
|
+
return pendulum.from_format(value, timetype).to_iso8601_string()
|
|
44
|
+
|
|
45
|
+
def sanitize_filename(filename):
|
|
46
|
+
"""Removes or replaces illegal characters from file names."""
|
|
47
|
+
illegal_chars = r'[\/:*?"<>|[\]{}()\%]' # Define characters to be removed
|
|
48
|
+
sanitized = re.sub(illegal_chars, '', filename) # Remove illegal characters
|
|
49
|
+
return sanitized.strip() # Remove leading/trailing spaces
|
|
50
|
+
|
|
51
|
+
def handle_output_files(output):
|
|
52
|
+
post_url = f"{IPC_API_URI}/files"
|
|
53
|
+
params = {"token": IPC_API_TOKEN}
|
|
54
|
+
# There are potentially many dicts in output, but usually only one
|
|
55
|
+
for o in output:
|
|
56
|
+
for key in o:
|
|
57
|
+
file_output = o.get(key, None)
|
|
58
|
+
temp_files = []
|
|
59
|
+
if file_output and isinstance(file_output, list):
|
|
60
|
+
# value is a list, but we only want to proceed if it's a list of files
|
|
61
|
+
if all(isinstance(i, dict) and 'base64' in i and 'filename' in i for i in file_output):
|
|
62
|
+
# we need to convert the objects to turbine format, then upload
|
|
63
|
+
temp_files = [{'file_data': base64.b64decode(i['base64']), 'file_name': sanitize_filename(i['filename'])} for i in file_output]
|
|
64
|
+
attachments = []
|
|
65
|
+
for file in temp_files:
|
|
66
|
+
attachments.append(
|
|
67
|
+
(
|
|
68
|
+
"files",
|
|
69
|
+
(
|
|
70
|
+
file["file_name"],
|
|
71
|
+
file["file_data"],
|
|
72
|
+
"multipart/form-data",
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
res = requests.post(post_url, files=attachments, params=params)
|
|
77
|
+
res.raise_for_status()
|
|
78
|
+
o[key] = res.json()
|
|
79
|
+
for key, timetype in OUTPUT_DATE_CONVERSIONS.items():
|
|
80
|
+
if key in o and o[key] is not None:
|
|
81
|
+
try:
|
|
82
|
+
o[key] = convert_timestamp(o[key], timetype)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
return output
|
|
21
87
|
if isinstance(sw_outputs, dict):
|
|
22
|
-
|
|
88
|
+
sw_outputs["sw_task_status"] = "success"
|
|
89
|
+
return {"output": handle_output_files([sw_outputs])} # Already a dictionary
|
|
23
90
|
|
|
24
91
|
elif isinstance(sw_outputs, list):
|
|
25
|
-
|
|
92
|
+
# Add sw_task_status to each dictionary in the list
|
|
93
|
+
for item in sw_outputs:
|
|
94
|
+
if isinstance(item, dict):
|
|
95
|
+
item["sw_task_status"] = "success"
|
|
96
|
+
return {"output": handle_output_files(sw_outputs)} # Wrap in a dictionary
|
|
26
97
|
|
|
27
98
|
elif sw_outputs is None:
|
|
28
|
-
return {"output":
|
|
99
|
+
return {"output": [{"sw_task_status": "success"}]} # Handle None case explicitly
|
|
29
100
|
|
|
30
101
|
else:
|
|
31
|
-
|
|
102
|
+
# For other types, wrap in a dictionary with sw_task_status
|
|
103
|
+
return {"output": [{"result": sw_outputs, "sw_task_status": "success"}]} # Wrap other types in a dictionary
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Swimlane Python API driver"""
|
|
2
|
+
|
|
3
|
+
from __future__ import absolute_import
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .core.client import Swimlane
|
|
8
|
+
from .utils.version import get_package_version
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'Swimlane',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
__version__ = get_package_version()
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
logger.addHandler(logging.NullHandler())
|
|
18
|
+
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Contains various adapters encapsulating API logic for retrieving, searching, listing, or creating resource objects"""
|
|
2
|
+
|
|
3
|
+
from .app import AppAdapter
|
|
4
|
+
from .record import RecordAdapter
|
|
5
|
+
from .report import ReportAdapter
|
|
6
|
+
from .usergroup import UserAdapter, GroupAdapter
|
|
7
|
+
from .helper import HelperAdapter
|
|
8
|
+
from .app_revision import AppRevisionAdapter
|
|
9
|
+
from .record_revision import RecordRevisionAdapter
|
|
10
|
+
from .task import TaskAdapter
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from swimlane.core.cache import check_cache
|
|
2
|
+
from swimlane.core.resolver import SwimlaneResolver
|
|
3
|
+
from swimlane.core.resources.app import App
|
|
4
|
+
from swimlane.utils import one_of_keyword_only
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AppAdapter(SwimlaneResolver):
|
|
8
|
+
"""Handles retrieval of Swimlane App resources"""
|
|
9
|
+
|
|
10
|
+
@check_cache(App)
|
|
11
|
+
@one_of_keyword_only('id', 'name')
|
|
12
|
+
def get(self, key, value):
|
|
13
|
+
"""Get single app by one of id or name
|
|
14
|
+
|
|
15
|
+
Supports resource cache
|
|
16
|
+
|
|
17
|
+
Keyword Args:
|
|
18
|
+
id (str): Full app id
|
|
19
|
+
name (str): App name
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
App: Corresponding App resource instance
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
TypeError: No or multiple keyword arguments provided
|
|
26
|
+
ValueError: No matching app found on server
|
|
27
|
+
ValueError: The lookup value is empty or None
|
|
28
|
+
"""
|
|
29
|
+
if not value:
|
|
30
|
+
raise ValueError('The value provided for the key "{0}" cannot be empty or None'.format(key))
|
|
31
|
+
|
|
32
|
+
if key == 'id':
|
|
33
|
+
# Server returns 204 instead of 404 for a non-existent app id
|
|
34
|
+
response = self._swimlane.request('get', 'app/{}'.format(value))
|
|
35
|
+
if response.status_code == 204:
|
|
36
|
+
raise ValueError('No app with id "{}"'.format(value))
|
|
37
|
+
|
|
38
|
+
return App(
|
|
39
|
+
self._swimlane,
|
|
40
|
+
response.json()
|
|
41
|
+
)
|
|
42
|
+
else:
|
|
43
|
+
# Workaround for lack of support for get by name
|
|
44
|
+
# Holdover from previous driver support, to be fixed as part of 3.x
|
|
45
|
+
for app in self.list():
|
|
46
|
+
if value and value == app.name:
|
|
47
|
+
return app
|
|
48
|
+
|
|
49
|
+
# No matching app found
|
|
50
|
+
raise ValueError('No app with name "{}"'.format(value))
|
|
51
|
+
|
|
52
|
+
def list(self):
|
|
53
|
+
"""Retrieve list of all apps
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
:class:`list` of :class:`~swimlane.core.resources.app.App`: List of all retrieved apps
|
|
57
|
+
"""
|
|
58
|
+
response = self._swimlane.request('get', 'app')
|
|
59
|
+
return [App(self._swimlane, item) for item in response.json()]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
from swimlane.core.cache import check_cache
|
|
4
|
+
from swimlane.core.resolver import AppResolver
|
|
5
|
+
from swimlane.core.resources.app_revision import AppRevision
|
|
6
|
+
from swimlane.utils import one_of_keyword_only
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AppRevisionAdapter(AppResolver):
|
|
10
|
+
"""Handles retrieval of Swimlane App Revision resources"""
|
|
11
|
+
|
|
12
|
+
def get_all(self):
|
|
13
|
+
"""
|
|
14
|
+
Gets all app revisions.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
AppRevision[]: Returns all AppRevisions for this Adapter's app.
|
|
18
|
+
"""
|
|
19
|
+
raw_revisions = self._swimlane.request('get', 'app/{0}/history'.format(self._app.id)).json()
|
|
20
|
+
return [AppRevision(self._swimlane, raw) for raw in raw_revisions]
|
|
21
|
+
|
|
22
|
+
def get(self, revision_number):
|
|
23
|
+
"""
|
|
24
|
+
Gets a specific app revision.
|
|
25
|
+
|
|
26
|
+
Supports resource cache
|
|
27
|
+
|
|
28
|
+
Keyword Args:
|
|
29
|
+
revision_number (float): App revision number
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
AppRevision: The AppRevision for the given revision number.
|
|
33
|
+
Raises: When revision is not an integer, a float NOT ending in ".0", or is less than 1
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if isinstance(revision_number, (int, float)):
|
|
37
|
+
if revision_number > 0 and revision_number % math.floor(revision_number) == 0:
|
|
38
|
+
key_value = AppRevision.get_unique_id(self._app.id, revision_number)
|
|
39
|
+
return self.__get(app_id_revision=key_value)
|
|
40
|
+
|
|
41
|
+
raise ValueError('The revision number must be a positive whole number greater than 0')
|
|
42
|
+
|
|
43
|
+
@check_cache(AppRevision)
|
|
44
|
+
@one_of_keyword_only('app_id_revision')
|
|
45
|
+
def __get(self, key, value):
|
|
46
|
+
"""Underlying get method supporting resource cache."""
|
|
47
|
+
app_id, revision_number = AppRevision.parse_unique_id(value)
|
|
48
|
+
app_revision_raw = self._swimlane.request('get', 'app/{0}/history/{1}'.format(app_id, revision_number)).json()
|
|
49
|
+
return AppRevision(self._swimlane, app_revision_raw)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import pendulum
|
|
2
|
+
|
|
3
|
+
from swimlane.core.resolver import SwimlaneResolver
|
|
4
|
+
from swimlane.utils.version import requires_swimlane_version
|
|
5
|
+
from swimlane.utils.list_validator import validate_str_list
|
|
6
|
+
from swimlane.utils.str_validator import validate_str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HelperAdapter(SwimlaneResolver):
|
|
10
|
+
"""Adapter providing any miscellaneous API calls not better suited for another adapter"""
|
|
11
|
+
|
|
12
|
+
@requires_swimlane_version('2.15')
|
|
13
|
+
def add_record_references(self, app_id, record_id, field_id, target_record_ids):
|
|
14
|
+
"""Bulk operation to directly add record references without making any additional requests
|
|
15
|
+
|
|
16
|
+
Warnings:
|
|
17
|
+
Does not perform any app, record, or target app/record validation
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
app_id (str): Full App ID string
|
|
21
|
+
record_id (str): Full parent Record ID string
|
|
22
|
+
field_id (str): Full field ID to target reference field on parent Record string
|
|
23
|
+
target_record_ids (List(str)): List of full target reference Record ID strings
|
|
24
|
+
"""
|
|
25
|
+
validate_str_list(target_record_ids, "target_record_ids")
|
|
26
|
+
|
|
27
|
+
self._swimlane.request(
|
|
28
|
+
'post',
|
|
29
|
+
'app/{0}/record/{1}/add-references'.format(app_id, record_id),
|
|
30
|
+
json={
|
|
31
|
+
'fieldId': field_id,
|
|
32
|
+
'targetRecordIds': target_record_ids
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def add_comment(self, app_id, record_id, field_id, message, rich_text=False):
|
|
37
|
+
"""Directly add a comment to a record without retrieving the app or record first
|
|
38
|
+
|
|
39
|
+
Warnings:
|
|
40
|
+
Does not perform any app, record, or field ID validation
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
app_id (str): Full App ID string
|
|
44
|
+
record_id (str): Full parent Record ID string
|
|
45
|
+
field_id (str): Full field ID to target reference field on parent Record string
|
|
46
|
+
message (str): New comment message body
|
|
47
|
+
rich_text (bool): Declare the message as being rich text, default is False
|
|
48
|
+
"""
|
|
49
|
+
validate_str(app_id, 'app_id')
|
|
50
|
+
validate_str(record_id, 'record_id')
|
|
51
|
+
validate_str(field_id, 'field_id')
|
|
52
|
+
validate_str(message, 'message')
|
|
53
|
+
|
|
54
|
+
if not isinstance(rich_text, bool):
|
|
55
|
+
raise ValueError('rich_text must be a boolean value.')
|
|
56
|
+
|
|
57
|
+
self._swimlane.request(
|
|
58
|
+
'post',
|
|
59
|
+
'app/{0}/record/{1}/{2}/comment'.format(
|
|
60
|
+
app_id,
|
|
61
|
+
record_id,
|
|
62
|
+
field_id
|
|
63
|
+
),
|
|
64
|
+
json={
|
|
65
|
+
'message': message,
|
|
66
|
+
'isRichText': rich_text,
|
|
67
|
+
'createdDate': pendulum.now().to_rfc3339_string()
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def check_bulk_job_status(self, job_id):
|
|
72
|
+
"""Check status of bulk_delete or bulk_modify jobs
|
|
73
|
+
.. versionadded:: 2.17.0
|
|
74
|
+
Args:
|
|
75
|
+
job_id (str): Job ID
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
:class:`list` of :class:`dict`: List of dictionaries containing job history
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
validate_str(job_id, 'job_id')
|
|
83
|
+
|
|
84
|
+
return self._swimlane.request('get', "logging/job/{0}".format(job_id)).json()
|