Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion kbcstorage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
"""

from kbcstorage.buckets import Buckets
from kbcstorage.components import Components
from kbcstorage.configurations import Configurations
from kbcstorage.workspaces import Workspaces
from kbcstorage.jobs import Jobs
from kbcstorage.tables import Tables
Expand All @@ -14,24 +16,32 @@ class Client:
Storage API Client.
"""

def __init__(self, api_domain, token):
def __init__(self, api_domain, token, branch_id='default'):
"""
Initialise a client.

Args:
api_domain (str): The domain on which the API sits. eg.
"https://connection.keboola.com".
token (str): A storage API key.
branch_id (str): The ID of branch to use, use 'default' to work without branch (in main).
"""
self.root_url = api_domain.rstrip("/")
self._token = token
self._branch_id = branch_id

self.buckets = Buckets(self.root_url, self.token)
self.files = Files(self.root_url, self.token)
self.jobs = Jobs(self.root_url, self.token)
self.tables = Tables(self.root_url, self.token)
self.workspaces = Workspaces(self.root_url, self.token)
self.components = Components(self.root_url, self.token, self.branch_id)
self.configurations = Configurations(self.root_url, self.token, self.branch_id)

@property
def token(self):
return self._token

@property
def branch_id(self):
return self._branch_id
37 changes: 37 additions & 0 deletions kbcstorage/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
Manages calls to the Storage API relating to components

Full documentation https://keboola.docs.apiary.io/#reference/components-and-configurations
"""
from kbcstorage.base import Endpoint


class Components(Endpoint):
"""
Components Endpoint
"""
def __init__(self, root_url, token, branch_id):
"""
Create a Configuration endpoint.

Args:
root_url (:obj:`str`): The base url for the API.
token (:obj:`str`): A storage API key.
branch_id (str): The ID of branch to use, use 'default' to work without branch (in main).
"""
super().__init__(root_url, f"branch/{branch_id}/components", token)

def list(self, include=None):
"""
List all components (and optionally configurations) in a project.

Args:
include (list): Properties to list (configuration, rows, state)
Returns:
response_body: The parsed json from the HTTP response.

Raises:
requests.HTTPError: If the API request fails.
"""
params = {'include': ',' . join(include)} if include else {}
return self._get(self.base_url, params=params)
116 changes: 116 additions & 0 deletions kbcstorage/configurations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""
Manages calls to the Storage API relating to configurations

Full documentation https://keboola.docs.apiary.io/#reference/components-and-configurations
"""
from kbcstorage.base import Endpoint


class Configurations(Endpoint):
"""
Configurations Endpoint
"""

def __init__(self, root_url, token, branch_id):
"""
Create a Component endpoint.

Args:
root_url (:obj:`str`): The base url for the API.
token (:obj:`str`): A storage API key.
branch_id (str): The ID of branch to use, use 'default' to work without branch (in main).
"""
super().__init__(root_url, f"branch/{branch_id}/components", token)

def detail(self, component_id, configuration_id):
"""
Retrieves information about a given configuration.

Args:
component_id (str): The id of the component.
configuration_id (str): The id of the configuration.

Returns:
response_body: The parsed json from the HTTP response.

Raises:
requests.HTTPError: If the API request fails.
"""
if not isinstance(component_id, str) or component_id == '':
raise ValueError("Invalid component_id '{}'.".format(component_id))
if not isinstance(configuration_id, str) or configuration_id == '':
raise ValueError("Invalid component_id '{}'.".format(configuration_id))
url = '{}/{}/configs/{}'.format(self.base_url, component_id, configuration_id)
Comment on lines +39 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to accept empty strings? Doesn't the detail call require values?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If component_id is an empty string, it raises an error, doesn't it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omg, sorry, I don't know how to read a simple conditional statement 🤦

return self._get(url)

def delete(self, component_id, configuration_id):
"""
Deletes the configuration.

Args:
component_id (str): The id of the component.
configuration_id (str): The id of the configuration.

Raises:
requests.HTTPError: If the API request fails.
"""
if not isinstance(component_id, str) or component_id == '':
raise ValueError("Invalid component_id '{}'.".format(component_id))
if not isinstance(configuration_id, str) or configuration_id == '':
raise ValueError("Invalid component_id '{}'.".format(configuration_id))
Comment on lines +57 to +60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

url = '{}/{}/configs/{}'.format(self.base_url, component_id, configuration_id)
self._delete(url)

def list(self, component_id):
"""
Lists configurations of the given component.

Args:
component_id (str): The id of the component.

Raises:
requests.HTTPError: If the API request fails.
"""
if not isinstance(component_id, str) or component_id == '':
raise ValueError("Invalid component_id '{}'.".format(component_id))
url = '{}/{}/configs'.format(self.base_url, component_id)
return self._get(url)

def create(self, component_id, name, description='', configuration=None, state=None, change_description='',
is_disabled=False, configuration_id=None):
"""
Create a new configuration.

Args:
component_id (str): ID of the component to create configuration for.
name (str): Name of the configuration visible to end-user.
description (str): Optional configuration description
configuration (dict): Actual configuration parameters
state (dict): Optional state parameters
changeDescription (str): Optional change description
is_disabled (bool): Optional flag to disable the configuration, default False
configuration_id (str): Optional configuration ID, if not specified, new ID is generated
Returns:
response_body: The parsed json from the HTTP response.

Raises:
requests.HTTPError: If the API request fails.
"""
if not isinstance(component_id, str) or component_id == '':
raise ValueError("Invalid component_id '{}'.".format(component_id))
if state is None:
state = {}
if configuration is None:
configuration = {}
body = {
'name': name,
'description': description,
'configuration': configuration,
'state': state,
'changeDescription': change_description,
'isDisabled': is_disabled
}
if configuration_id:
body['id'] = configuration_id
url = '{}/{}/configs'.format(self.base_url, component_id)
return self._post(url, data=body)
1 change: 1 addition & 0 deletions tests/base_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@


class BaseTestCase(unittest.TestCase):
TEST_COMPONENT_NAME = 'keboola.runner-config-test'

@classmethod
def setUpClass(cls) -> None:
Expand Down
59 changes: 59 additions & 0 deletions tests/functional/test_components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
from requests import exceptions
from kbcstorage.components import Components
from kbcstorage.configurations import Configurations
from tests.base_test_case import BaseTestCase


class TestEndpoint(BaseTestCase):
def setUp(self):
self.components = Components(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'), 'default')
self.configurations = Configurations(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'), 'default')
self.configurations.create(self.TEST_COMPONENT_NAME, 'test_components')

def tearDown(self):
try:
for configuration in self.configurations.list(self.TEST_COMPONENT_NAME):
self.configurations.delete(self.TEST_COMPONENT_NAME, configuration['id'])
except exceptions.HTTPError as e:
if e.response.status_code != 404:
raise

def testListComponents(self):
components = self.components.list()
self.assertTrue(len(components) > 0)
for component in components:
with self.subTest():
self.assertTrue('id' in component)
self.assertTrue('name' in component)
self.assertTrue('type' in component)
self.assertTrue('uri' in component)

with self.subTest():
for configuration in component['configurations']:
self.assertTrue('id' in configuration)
self.assertTrue('name' in configuration)
self.assertTrue('description' in configuration)
self.assertFalse('configuration' in configuration)
self.assertFalse('rows' in configuration)
self.assertFalse('state' in configuration)

def testListComponentsIncludeConfigurations(self):
components = self.components.list(include=['configuration', 'rows', 'state'])
self.assertTrue(len(components) > 0)
for component in components:
with self.subTest():
self.assertTrue('id' in component)
self.assertTrue('name' in component)
self.assertTrue('type' in component)
self.assertTrue('uri' in component)

with self.subTest():
self.assertTrue('configurations' in component)
for configuration in component['configurations']:
self.assertTrue('id' in configuration)
self.assertTrue('name' in configuration)
self.assertTrue('description' in configuration)
self.assertTrue('configuration' in configuration)
self.assertTrue('rows' in configuration)
self.assertTrue('state' in configuration)
52 changes: 52 additions & 0 deletions tests/functional/test_configurations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
from requests import exceptions
from kbcstorage.configurations import Configurations
from tests.base_test_case import BaseTestCase


class TestEndpoint(BaseTestCase):
def setUp(self):
self.configurations = Configurations(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'), 'default')

def tearDown(self):
try:
for configuration in self.configurations.list(self.TEST_COMPONENT_NAME):
self.configurations.delete(self.TEST_COMPONENT_NAME, configuration['id'])
except exceptions.HTTPError as e:
if e.response.status_code != 404:
raise

def testCreateConfiguration(self):
configuration = self.configurations.create(self.TEST_COMPONENT_NAME, 'test_create_configuration')
self.assertTrue('id' in configuration)
self.assertTrue('name' in configuration)
self.assertTrue('description' in configuration)
self.assertTrue('configuration' in configuration)

def testDeleteConfiguration(self):
configuration = self.configurations.create(self.TEST_COMPONENT_NAME, 'test_delete_configuration')
configuration = self.configurations.detail(self.TEST_COMPONENT_NAME, configuration['id'])
self.assertTrue('id' in configuration)
self.assertTrue('name' in configuration)
self.assertEqual(configuration['name'], 'test_delete_configuration')
self.assertTrue('description' in configuration)
self.assertTrue('configuration' in configuration)

self.configurations.delete(self.TEST_COMPONENT_NAME, configuration['id'])
with self.assertRaises(exceptions.HTTPError):
self.configurations.detail(self.TEST_COMPONENT_NAME, configuration['id'])

def testListConfigurations(self):
self.configurations.create(self.TEST_COMPONENT_NAME, 'test_list_configurations')
configurations = self.configurations.list(self.TEST_COMPONENT_NAME)
self.assertTrue(len(configurations) > 0)
for configuration in configurations:
with self.subTest():
self.assertTrue('id' in configuration)
self.assertTrue('name' in configuration)
self.assertTrue('description' in configuration)
self.assertTrue('configuration' in configuration)

with self.subTest():
with self.assertRaises(exceptions.HTTPError):
configurations = self.configurations.list('non-existent-component')
13 changes: 0 additions & 13 deletions tests/functional/test_workspaces.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import csv
import os
import tempfile
import warnings
from requests import exceptions
from kbcstorage.buckets import Buckets
from kbcstorage.jobs import Jobs
from kbcstorage.files import Files
from kbcstorage.tables import Tables
from kbcstorage.workspaces import Workspaces
from tests.base_test_case import BaseTestCase
Expand All @@ -17,23 +15,12 @@ def setUp(self):
self.buckets = Buckets(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'))
self.jobs = Jobs(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'))
self.tables = Tables(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'))
self.files = Files(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN'))
try:
file_list = self.files.list(tags=['sapi-client-python-tests'])
for file in file_list:
self.files.delete(file['id'])
except exceptions.HTTPError as e:
if e.response.status_code != 404:
raise
try:
self.buckets.delete('in.c-py-test-buckets', force=True)
except exceptions.HTTPError as e:
if e.response.status_code != 404:
raise

# https://github.com/boto/boto3/issues/454
warnings.simplefilter("ignore", ResourceWarning)

def tearDown(self):
try:
if hasattr(self, 'workspace_id'):
Expand Down
Loading