diff --git a/kbcstorage/base.py b/kbcstorage/base.py index 4f8afd8..ab36a00 100644 --- a/kbcstorage/base.py +++ b/kbcstorage/base.py @@ -35,8 +35,6 @@ def __init__(self, root_url, path_component, token): """ if not root_url: raise ValueError("Root URL is required.") - if not path_component: - raise ValueError("Path component is required.") if not token: raise ValueError("Token is required.") self.root_url = root_url diff --git a/kbcstorage/branches.py b/kbcstorage/branches.py new file mode 100644 index 0000000..1e4ecd9 --- /dev/null +++ b/kbcstorage/branches.py @@ -0,0 +1,43 @@ +""" +Manages calls to the Storage API relating to development branches + +Full documentation https://keboola.docs.apiary.io/#reference/development-branches + +""" +from kbcstorage.base import Endpoint + + +class Branches(Endpoint): + """ + Tokens Endpoint + """ + def __init__(self, root_url, token): + """ + Create a Tokens endpoint. + + Args: + root_url (:obj:`str`): The base url for the API. + token (:obj:`str`): A storage API key. + """ + # Branches have inconsistent endpoint naming - it's either dev-branches or branch, so it need to be resolved + # endpoint by endpoint. + super().__init__(root_url, "", token) + + def metadata(self, branch_id="default"): + """ + Get branch metadata + + Args: + branch_id (str): The id of the branch or "default" to get metadata for the main branch (production). + + Returns: + response_body: The parsed json from the HTTP response. + + Raises: + requests.HTTPError: If the API request fails. + """ + if not isinstance(branch_id, str) or branch_id == "": + raise ValueError(f"Invalid branch_id '{branch_id}'") + + url = f"{self.base_url}branch/{branch_id}/metadata" + return self._get(url) diff --git a/kbcstorage/client.py b/kbcstorage/client.py index 00ac102..bedc0b3 100644 --- a/kbcstorage/client.py +++ b/kbcstorage/client.py @@ -1,10 +1,11 @@ """" Entry point for the Storage API client. """ - +from kbcstorage.branches import Branches from kbcstorage.buckets import Buckets from kbcstorage.components import Components from kbcstorage.configurations import Configurations +from kbcstorage.tokens import Tokens from kbcstorage.workspaces import Workspaces from kbcstorage.jobs import Jobs from kbcstorage.tables import Tables @@ -37,6 +38,8 @@ def __init__(self, api_domain, token, branch_id='default'): 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) + self.tokens = Tokens(self.root_url, self.token) + self.branches = Branches(self.root_url, self.token) @property def token(self): diff --git a/kbcstorage/tokens.py b/kbcstorage/tokens.py new file mode 100644 index 0000000..0e07285 --- /dev/null +++ b/kbcstorage/tokens.py @@ -0,0 +1,35 @@ +""" +Manages calls to the Storage API relating to tokens + +Full documentation https://keboola.docs.apiary.io/#reference/tokens-and-permissions/. + +""" +from kbcstorage.base import Endpoint + + +class Tokens(Endpoint): + """ + Tokens Endpoint + """ + def __init__(self, root_url, token): + """ + Create a Tokens endpoint. + + Args: + root_url (:obj:`str`): The base url for the API. + token (:obj:`str`): A storage API key. + """ + super().__init__(root_url, 'tokens', token) + + def verify(self): + """ + Verify token. + + Returns: + response_body: The parsed json from the HTTP response. + + Raises: + requests.HTTPError: If the API request fails. + """ + url = '{}/verify'.format(self.base_url) + return self._get(url) diff --git a/tests/functional/test_base.py b/tests/functional/test_base.py index 3836795..d671352 100644 --- a/tests/functional/test_base.py +++ b/tests/functional/test_base.py @@ -97,11 +97,6 @@ def test_missing_url(self): with self.assertRaisesRegex(ValueError, "Root URL is required."): Endpoint(None, '', None) - def test_missing_part(self): - with self.assertRaisesRegex(ValueError, - "Path component is required."): - Endpoint('https://connection.keboola.com/', '', None) - def test_missing_token(self): with self.assertRaisesRegex(ValueError, "Token is required."): Endpoint('https://connection.keboola.com/', 'tables', None) diff --git a/tests/functional/test_branches.py b/tests/functional/test_branches.py new file mode 100644 index 0000000..a17dad0 --- /dev/null +++ b/tests/functional/test_branches.py @@ -0,0 +1,13 @@ +import os + +from kbcstorage.branches import Branches +from tests.base_test_case import BaseTestCase + + +class TestEndpoint(BaseTestCase): + def setUp(self): + self.branches = Branches(os.getenv('KBC_TEST_API_URL'), os.getenv('KBC_TEST_TOKEN')) + + def test_metadata(self): + metadata = self.branches.metadata('default') + self.assertTrue(isinstance(metadata, list)) diff --git a/tests/functional/test_tokens.py b/tests/functional/test_tokens.py new file mode 100644 index 0000000..b32d931 --- /dev/null +++ b/tests/functional/test_tokens.py @@ -0,0 +1,16 @@ +import os +from kbcstorage.tokens import Tokens +from tests.base_test_case import BaseTestCase + + +class TestEndpoint(BaseTestCase): + def setUp(self): + self.tokens = Tokens(os.getenv('KBC_TEST_API_URL'), + os.getenv('KBC_TEST_TOKEN')) + + def test_verify(self): + token_info = self.tokens.verify() + self.assertTrue('id' in token_info) + self.assertTrue('description' in token_info) + self.assertTrue('canManageBuckets' in token_info) + self.assertTrue('owner' in token_info) diff --git a/tests/mocks/branches_responses.py b/tests/mocks/branches_responses.py new file mode 100644 index 0000000..5b763ec --- /dev/null +++ b/tests/mocks/branches_responses.py @@ -0,0 +1,8 @@ +branches_metadata_response = [ + { + "id": "93670", + "key": "KBC.projectDescription", + "value": "Testing project one", + "timestamp": "2023-05-31T17:52:18+0200" + } +] diff --git a/tests/mocks/test_branches.py b/tests/mocks/test_branches.py new file mode 100644 index 0000000..33f9426 --- /dev/null +++ b/tests/mocks/test_branches.py @@ -0,0 +1,49 @@ +""" +Test basic functionality of the Branches endpoint +""" +import unittest + +import responses + +from kbcstorage.branches import Branches +from tests.mocks.branches_responses import branches_metadata_response + + +class TestBranchesEndpointWithMocks(unittest.TestCase): + """ + Test the methods of a Branches endpoint instance with mock HTTP responses + """ + def setUp(self): + token = 'dummy_token' + base_url = 'https://connection.keboola.com/' + self.branches = Branches(base_url, token) + + @responses.activate + def test_metadata_no_branch(self): + """ + Branches lists metadata correctly + """ + responses.add( + responses.Response( + method='GET', + url='https://connection.keboola.com/v2/storage/branch/default/metadata', + json=branches_metadata_response + ) + ) + branch_metadata = self.branches.metadata() + self.assertEqual(branches_metadata_response, branch_metadata) + + @responses.activate + def test_metadata_some_branch(self): + """ + Branches lists metadata correctly + """ + responses.add( + responses.Response( + method='GET', + url='https://connection.keboola.com/v2/storage/branch/1234/metadata', + json=branches_metadata_response + ) + ) + branch_metadata = self.branches.metadata('1234') + self.assertEqual(branches_metadata_response, branch_metadata) diff --git a/tests/mocks/test_tokens.py b/tests/mocks/test_tokens.py new file mode 100644 index 0000000..43ac2dc --- /dev/null +++ b/tests/mocks/test_tokens.py @@ -0,0 +1,34 @@ +""" +Test basic functionality of the Tokens endpoint +""" +import unittest + +import responses + +from kbcstorage.tokens import Tokens +from tests.mocks.token_responses import verify_token_response + + +class TestTokensEndpointWithMocks(unittest.TestCase): + """ + Test the methods of a Tokens endpoint instance with mock HTTP responses + """ + def setUp(self): + token = 'dummy_token' + base_url = 'https://connection.keboola.com/' + self.tokens = Tokens(base_url, token) + + @responses.activate + def test_verify(self): + """ + Verify token returns correctly + """ + responses.add( + responses.Response( + method='GET', + url='https://connection.keboola.com/v2/storage/tokens/verify', + json=verify_token_response + ) + ) + token_info = self.tokens.verify() + self.assertEqual(verify_token_response, token_info) diff --git a/tests/mocks/token_responses.py b/tests/mocks/token_responses.py new file mode 100644 index 0000000..153c0d9 --- /dev/null +++ b/tests/mocks/token_responses.py @@ -0,0 +1,141 @@ +verify_token_response = { + "id": "373115", + "created": "2021-06-23T09:33:36+0200", + "refreshed": "2021-06-23T09:33:36+0200", + "description": "devel@keboola.com", + "uri": "https://connection.keboola.com/v2/storage/tokens/373115", + "isMasterToken": True, + "canManageBuckets": True, + "canManageTokens": True, + "canReadAllFileUploads": True, + "canPurgeTrash": True, + "expires": None, + "isExpired": False, + "isDisabled": False, + "dailyCapacity": 0, + "token": "8625-373115-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "bucketPermissions": { + "out.c-test2": "manage", + "in.c-test3": "manage", + "out.c-test": "manage", + "out.c-some-other": "manage" + }, + "owner": { + "id": 8625, + "name": "Test Project", + "type": "production", + "region": "us-east-1", + "created": "2021-06-23T09:33:32+0200", + "expires": None, + "features": [ + "syrup-jobs-limit-10", + "oauth-v3", + "queuev2", + "storage-types", + "allow-ai", + "alternat", + ], + "dataSizeBytes": 31474176, + "rowsCount": 797, + "hasMysql": False, + "hasSynapse": False, + "hasRedshift": True, + "hasSnowflake": True, + "hasExasol": False, + "hasTeradata": False, + "hasBigquery": False, + "defaultBackend": "snowflake", + "hasTryModeOn": "0", + "limits": { + "components.jobsParallelism": { + "name": "components.jobsParallelism", + "value": 10 + }, + "goodData.dataSizeBytes": { + "name": "goodData.dataSizeBytes", + "value": 1000000000 + }, + "goodData.demoTokenEnabled": { + "name": "goodData.demoTokenEnabled", + "value": 1 + }, + "goodData.prodTokenEnabled": { + "name": "goodData.prodTokenEnabled", + "value": 0 + }, + "goodData.usersCount": { + "name": "goodData.usersCount", + "value": 30 + }, + "kbc.adminsCount": { + "name": "kbc.adminsCount", + "value": 10 + }, + "kbc.extractorsCount": { + "name": "kbc.extractorsCount", + "value": 0 + }, + "kbc.monthlyProjectPowerLimit": { + "name": "kbc.monthlyProjectPowerLimit", + "value": 50 + }, + "kbc.writersCount": { + "name": "kbc.writersCount", + "value": 0 + }, + "orchestrations.count": { + "name": "orchestrations.count", + "value": 10 + }, + "storage.dataSizeBytes": { + "name": "storage.dataSizeBytes", + "value": 50000000000 + }, + "storage.jobsParallelism": { + "name": "storage.jobsParallelism", + "value": 10 + } + }, + "metrics": { + "kbc.adminsCount": { + "name": "kbc.adminsCount", + "value": 1 + }, + "orchestrations.count": { + "name": "orchestrations.count", + "value": 0 + }, + "storage.dataSizeBytes": { + "name": "storage.dataSizeBytes", + "value": 31474176 + }, + "storage.rowsCount": { + "name": "storage.rowsCount", + "value": 797 + } + }, + "isDisabled": False, + "billedMonthlyPrice": None, + "dataRetentionTimeInDays": 7, + "fileStorageProvider": "aws", + "redshift": { + "connectionId": 365, + "databaseName": "sapi_8625" + } + }, + "organization": { + "id": "111111" + }, + "admin": { + "name": "Devel", + "id": 59, + "features": [ + "ui-devel-preview", + "manage-try-mode", + "validate-sql", + "early-adopter-preview" + ], + "isOrganizationMember": True, + "role": "admin" + } +}