Skip to content

Commit e30f83a

Browse files
authored
Merge pull request #81 from keboola/triggers
Add Triggers endpoint
2 parents 40001aa + 2b04f70 commit e30f83a

File tree

6 files changed

+427
-3
lines changed

6 files changed

+427
-3
lines changed

kbcstorage/base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,32 @@ def _post(self, *args, **kwargs):
120120
else:
121121
return r.json()
122122

123+
def _put(self, *args, **kwargs):
124+
"""
125+
Construct a requests PUT call with args and kwargs and process the
126+
results.
127+
128+
Args:
129+
*args: Positional arguments to pass to the post request.
130+
**kwargs: Key word arguments to pass to the post request.
131+
132+
Returns:
133+
body: Response body parsed from json.
134+
135+
Raises:
136+
requests.HTTPError: If the API request fails.
137+
"""
138+
headers = kwargs.pop('headers', {})
139+
headers.update(self._auth_header)
140+
r = requests.put(headers=headers, *args, **kwargs)
141+
try:
142+
r.raise_for_status()
143+
except requests.HTTPError:
144+
# Handle different error codes
145+
raise
146+
else:
147+
return r.json()
148+
123149
def _delete(self, *args, **kwargs):
124150
"""
125151
Construct a requests DELETE call with args and kwargs and process the

kbcstorage/client.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from kbcstorage.buckets import Buckets
66
from kbcstorage.components import Components
77
from kbcstorage.configurations import Configurations
8-
from kbcstorage.tokens import Tokens
9-
from kbcstorage.workspaces import Workspaces
8+
from kbcstorage.files import Files
109
from kbcstorage.jobs import Jobs
1110
from kbcstorage.tables import Tables
12-
from kbcstorage.files import Files
11+
from kbcstorage.tokens import Tokens
12+
from kbcstorage.triggers import Triggers
13+
from kbcstorage.workspaces import Workspaces
1314

1415

1516
class Client:
@@ -40,6 +41,7 @@ def __init__(self, api_domain, token, branch_id='default'):
4041
self.configurations = Configurations(self.root_url, self.token, self.branch_id)
4142
self.tokens = Tokens(self.root_url, self.token)
4243
self.branches = Branches(self.root_url, self.token)
44+
self.triggers = Triggers(self.root_url, self.token)
4345

4446
@property
4547
def token(self):

kbcstorage/triggers.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
"""
2+
Manages calls to the Storage API relating to triggers
3+
4+
Full documentation `here`.
5+
6+
.. _here:
7+
http://docs.keboola.apiary.io/#reference/triggers/
8+
"""
9+
from kbcstorage.base import Endpoint
10+
11+
12+
class Triggers(Endpoint):
13+
"""
14+
Triggers Endpoint
15+
"""
16+
17+
def __init__(self, root_url, token):
18+
"""
19+
Create a Triggers endpoint.
20+
21+
Args:
22+
root_url (:obj:`str`): The base url for the API.
23+
token (:obj:`str`): A storage API key.
24+
"""
25+
super().__init__(root_url, 'triggers', token)
26+
27+
def list(self):
28+
"""
29+
List all triggers in project.
30+
31+
Returns:
32+
response_body: The parsed json from the HTTP response.
33+
34+
Raises:
35+
requests.HTTPError: If the API request fails.
36+
"""
37+
38+
return self._get(self.base_url)
39+
40+
def detail(self, trigger_id):
41+
"""
42+
Retrieves information about a given trigger.
43+
44+
Args:
45+
trigger_id (str): The id of the trigger.
46+
47+
Raises:
48+
requests.HTTPError: If the API request fails.
49+
"""
50+
url = '{}/{}'.format(self.base_url, trigger_id)
51+
52+
return self._get(url)
53+
54+
def create(self, runWithTokenId, component, configurationId, coolDownPeriodMinutes, tableIds):
55+
"""
56+
Create a new trigger.
57+
58+
Args:
59+
runWithTokenId (int): ID of token used for running configured component.
60+
component (str): For now we support only 'orchestration'.
61+
configurationId (int): Id of component configuration.
62+
coolDownPeriodMinutes (int): Minimal cool down period before
63+
firing action again in minutes (min is 1 minute).
64+
tableIds (list[str]) IDs of tables.
65+
Returns:
66+
response_body: The parsed json from the HTTP response.
67+
68+
Raises:
69+
requests.HTTPError: If the API request fails.
70+
"""
71+
# Separating create and link into two distinct functions...
72+
# Need to check args...
73+
body = {
74+
"runWithTokenId": runWithTokenId,
75+
"component": component,
76+
"configurationId": configurationId,
77+
"coolDownPeriodMinutes": coolDownPeriodMinutes,
78+
"tableIds": tableIds
79+
}
80+
81+
return self._post(self.base_url, json=body)
82+
83+
def delete(self, trigger_id):
84+
"""
85+
Delete a trigger referenced by ``trigger_id``.
86+
87+
Args:
88+
trigger_id (int): The id of the trigger to be deleted.
89+
90+
"""
91+
url = '{}/{}'.format(self.base_url, trigger_id)
92+
self._delete(url)
93+
94+
def update(self, trigger_id, runWithTokenId=None, component=None, configurationId=None,
95+
coolDownPeriodMinutes=None, tableIds=None):
96+
"""
97+
Update a trigger referenced by ``trigger_id``.
98+
99+
Args:
100+
runWithTokenId (int): ID of token used for running configured component.
101+
component (str): For now we support only 'orchestration'.
102+
configurationId (int): Id of component configuration.
103+
coolDownPeriodMinutes (int): Minimal cool down period before
104+
firing action again in minutes (min is 1 minute).
105+
tableIds (list[str]) IDs of tables.
106+
Returns:
107+
response_body: The parsed json from the HTTP response.
108+
109+
Raises:
110+
requests.HTTPError: If the API request fails.
111+
"""
112+
url = '{}/{}'.format(self.base_url, trigger_id)
113+
body = {
114+
k: v for k, v in {
115+
"runWithTokenId": runWithTokenId,
116+
"component": component,
117+
"configurationId": configurationId,
118+
"coolDownPeriodMinutes": coolDownPeriodMinutes,
119+
"tableIds": tableIds
120+
}.items()
121+
if v is not None
122+
}
123+
return self._put(url, data=body)

tests/functional/test_triggers.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import os
2+
import tempfile
3+
import warnings
4+
5+
from requests import exceptions
6+
7+
from kbcstorage.buckets import Buckets
8+
from kbcstorage.configurations import Configurations
9+
from kbcstorage.jobs import Jobs
10+
from kbcstorage.tables import Tables
11+
from kbcstorage.tokens import Tokens
12+
from kbcstorage.triggers import Triggers
13+
from tests.base_test_case import BaseTestCase
14+
15+
16+
class TestEndpoint(BaseTestCase):
17+
TEST_BUCKET_NAME = "trigger_test_bucket"
18+
TEST_TABLE_NAME = "trigger_test_table"
19+
20+
def setUp(self):
21+
self.root_url = os.getenv('KBC_TEST_API_URL')
22+
self.token = os.getenv('KBC_TEST_TOKEN')
23+
24+
self.triggers = Triggers(self.root_url, self.token)
25+
self.tables = Tables(self.root_url, self.token)
26+
self.buckets = Buckets(self.root_url, self.token)
27+
self.jobs = Jobs(self.root_url, self.token)
28+
self.configurations = Configurations(self.root_url, self.token, 'default')
29+
self.tokens = Tokens(self.root_url, self.token)
30+
31+
self.created_trigger_ids = []
32+
# https://github.com/boto/boto3/issues/454
33+
warnings.simplefilter("ignore", ResourceWarning)
34+
35+
self.clean()
36+
self.token_id = self.tokens.verify()["id"]
37+
self.test_bucket_id = self.buckets.create(self.TEST_BUCKET_NAME)['id']
38+
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
39+
tmp_file.write(b"a,b,c\n1,2,3\n")
40+
self.table_id = self.tables.create(self.test_bucket_id, self.TEST_TABLE_NAME, tmp_file.name)
41+
self.component = self.TEST_COMPONENT_NAME
42+
self.configuration_id = self.configurations.create(self.TEST_COMPONENT_NAME, 'trigger_test_config')["id"]
43+
44+
def clean(self):
45+
try:
46+
if hasattr(self, "test_bucket_id"):
47+
self.buckets.delete(self.test_bucket_id, True)
48+
except exceptions.HTTPError as e:
49+
if e.response.status_code != 404:
50+
raise
51+
52+
def tearDown(self):
53+
self.clean()
54+
55+
def test_steps(self):
56+
self.create_trigger()
57+
self.list_triggers()
58+
self.trigger_detail()
59+
self.update_trigger()
60+
self.delete_triggers()
61+
62+
def create_trigger(self):
63+
trigger_id = self.triggers.create(
64+
runWithTokenId=self.token_id,
65+
component=self.component,
66+
configurationId=self.configuration_id,
67+
coolDownPeriodMinutes=10,
68+
tableIds=[self.table_id]
69+
)['id']
70+
self.created_trigger_ids.append(trigger_id)
71+
self.assertEqual(trigger_id, self.triggers.detail(trigger_id)['id'])
72+
73+
def trigger_detail(self):
74+
self.assertGreater(len(self.created_trigger_ids), 0)
75+
first_id = self.created_trigger_ids[0]
76+
detail = self.triggers.detail(first_id)
77+
self.assertEqual(detail["runWithTokenId"], int(self.token_id))
78+
self.assertEqual(detail["component"], self.component)
79+
self.assertEqual(detail["configurationId"], self.configuration_id)
80+
self.assertEqual(detail["coolDownPeriodMinutes"], 10)
81+
self.assertEqual([t['tableId'] for t in detail["tables"]], [self.table_id])
82+
self.assertEqual(detail["id"], first_id)
83+
84+
def list_triggers(self):
85+
self.assertGreater(len(self.created_trigger_ids), 0)
86+
all_triggers = self.triggers.list()
87+
api_trigger_ids = {x["id"] for x in all_triggers}
88+
created_trigger_ids = {x for x in self.created_trigger_ids}
89+
self.assertTrue(created_trigger_ids.issubset(api_trigger_ids))
90+
91+
def update_trigger(self):
92+
self.assertGreater(len(self.created_trigger_ids), 0)
93+
first_id = self.created_trigger_ids[0]
94+
self.triggers.update(first_id, coolDownPeriodMinutes=100)
95+
detail = self.triggers.detail(first_id)
96+
self.assertEqual(detail["coolDownPeriodMinutes"], 100)
97+
98+
def delete_triggers(self):
99+
for t_id in self.created_trigger_ids:
100+
self.triggers.delete(t_id)

tests/mocks/test_triggers.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import unittest
2+
import responses
3+
from kbcstorage.triggers import Triggers
4+
from .triggers_responses import list_response, detail_response, create_response, update_response
5+
6+
7+
class TestTriggersWithMocks(unittest.TestCase):
8+
def setUp(self):
9+
token = 'dummy_token'
10+
base_url = 'https://connection.keboola.com/'
11+
self.triggers = Triggers(base_url, token)
12+
13+
@responses.activate
14+
def test_list(self):
15+
"""
16+
triggers mocks list correctly.
17+
"""
18+
responses.add(
19+
responses.Response(
20+
method='GET',
21+
url='https://connection.keboola.com/v2/storage/triggers',
22+
json=list_response
23+
)
24+
)
25+
triggers_list = self.triggers.list()
26+
assert isinstance(triggers_list, list)
27+
28+
@responses.activate
29+
def test_detail_by_id(self):
30+
"""
31+
triggers mocks detail by integer id correctly.
32+
"""
33+
responses.add(
34+
responses.Response(
35+
method='GET',
36+
url='https://connection.keboola.com/v2/storage/triggers/3',
37+
json=detail_response
38+
)
39+
)
40+
trigger_id = '3'
41+
trigger_detail = self.triggers.detail(trigger_id)
42+
assert trigger_detail['id'] == '3'
43+
44+
@responses.activate
45+
def test_delete(self):
46+
"""
47+
Triggers mock deletes trigger by id.
48+
"""
49+
responses.add(
50+
responses.Response(
51+
method='DELETE',
52+
url='https://connection.keboola.com/v2/storage/triggers/1',
53+
json={}
54+
)
55+
)
56+
trigger_id = 1
57+
deleted_detail = self.triggers.delete(trigger_id)
58+
assert deleted_detail is None
59+
60+
@responses.activate
61+
def test_update(self):
62+
"""
63+
Triggers mock update trigger by id.
64+
"""
65+
responses.add(
66+
responses.Response(
67+
method='PUT',
68+
url='https://connection.keboola.com/v2/storage/triggers/1',
69+
json=update_response
70+
)
71+
)
72+
trigger_id = 1
73+
updated_detail = self.triggers.update(trigger_id, runWithTokenId=100)
74+
assert updated_detail['id'] == '3'
75+
76+
@responses.activate
77+
def test_create(self):
78+
"""
79+
Triggers mock creates new trigger.
80+
"""
81+
responses.add(
82+
responses.Response(
83+
method='POST',
84+
url='https://connection.keboola.com/v2/storage/triggers',
85+
json=create_response
86+
)
87+
)
88+
created_detail = self.triggers.create(
89+
runWithTokenId=123,
90+
component="orchestration",
91+
configurationId=123,
92+
coolDownPeriodMinutes=20,
93+
tableIds=[
94+
"in.c-test.watched-1",
95+
"in.c-prod.watched-5"
96+
]
97+
)
98+
assert created_detail['runWithTokenId'] == 123

0 commit comments

Comments
 (0)