From 5bc427fee0e919901129bec25c5ee96311077d04 Mon Sep 17 00:00:00 2001 From: Christian Boettcher <c.boettcher@fz-juelich.de> Date: Tue, 7 Dec 2021 09:57:53 +0100 Subject: [PATCH] add basic client for datacat + first tests --- .gitignore | 5 +- datacat_integration/connection.py | 106 ++++++++++++++++++++++++++++++ tests/test_connection.py | 46 +++++++++++++ 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 datacat_integration/connection.py create mode 100644 tests/test_connection.py diff --git a/.gitignore b/.gitignore index e14035c..7e667c3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ __pycache__/ .coverage coverage.xml -# vs-code specific, individual files -settings.json \ No newline at end of file +# local env files +settings.json +testing-authentication.env \ No newline at end of file diff --git a/datacat_integration/connection.py b/datacat_integration/connection.py new file mode 100644 index 0000000..ddaa6bf --- /dev/null +++ b/datacat_integration/connection.py @@ -0,0 +1,106 @@ +from typing import Dict +import uuid +import json + +from urllib.parse import urljoin + +import requests + +class DataCatalogEntry: + """A datatype representing an entry in the datacatalog.""" + name: str = "" + url: str = "" + metadata: Dict[str, str] = {} + + def __init__(self, name: str, url: str, metadata: Dict[str, str]): + self.name = name + self.url = url + self.metadata = metadata + + def json(self): + """returns a json-compatible representation of the object.""" + return json.dumps( + { + "name" : self.name, + "url" : self.url, + "metadata" : self.metadata + } + ) + + def from_json(data: json): + """returns a DataCatalogEntry object from the given json string""" + dict_data = json.loads(data) + return DataCatalogEntry(dict_data['name'], dict_data['url'], dict_data['metadata']) + + + +class DataCatConnection: + """An API to the DataCatalog. An instance of this class contains connection data for a single DataCatalog-server with a single user/pass login.""" + def __init__( + self, + catalog_url: str = "", + username: str = "", + password: str = "" + ): + self.url = catalog_url + self.user = username + self._password = password + self.refresh_token() + + + def refresh_token(self): # POST /token + """Refresh the stored token by retrieving a new one from the server.""" + data = { + "username" : self.user, + "password" : self._password + } + headers = { + 'accept' : 'application/json' + } + response = requests.post(urljoin(self.url, 'token'), data=data, headers=headers) + if response.ok: + self._auth_token = response.json()['access_token'] + return self._auth_token + else: + raise ConnectionError('Could not authenticate with the DataCatalog.') + + def get_token(self): # GET /me with auth and refresh if error + """Checks if the current token is valid. If yes, return it, else refresh it and return a new one.""" + headers = { + 'accept' : 'application/json', + 'Authorization' : 'Bearer {}'.format(self._auth_token) + } + if requests.get(urljoin(self.url, 'me'), headers=headers).ok: + return self._auth_token + else: + return self.refresh_token() + + + def get_object(self, datacat_type: str, oid: uuid): # GET /<type>/<oid> + """Returns a json of the given object from the server.""" + headers = { + 'accept' : 'application/json' + } + url = urljoin(self.url, "{}/{}".format(datacat_type, oid)) + return requests.get(url, headers=headers).json() + + def create_object(self, datacat_type: str, object: DataCatalogEntry): # POST /<type> + """Creates a new object in the datacatalog. Returns the oid of successful.""" + headers = { + 'accept' : 'application/json', + 'Content-Type' : 'application/json', + 'Authorization' : 'Bearer {}'.format(self._auth_token) + } + response = requests.post(urljoin(self.url, datacat_type), headers=headers, data=object.json()) + if response.ok: + return response.json()[0] + else: + raise ConnectionError(response.text) + + def list_type(self, datacat_type: str): + """lists all elements of the given type""" + headers = { + 'accept' : 'application/json' + } + url = urljoin(self.url, datacat_type) + return requests.get(url, headers=headers).json() \ No newline at end of file diff --git a/tests/test_connection.py b/tests/test_connection.py new file mode 100644 index 0000000..49e239a --- /dev/null +++ b/tests/test_connection.py @@ -0,0 +1,46 @@ +from unittest import TestCase +import os + +from datacat_integration.connection import DataCatalogEntry, DataCatConnection + + +class EntryTest(TestCase): + def setUp(self) -> None: + self.json_string = '{ "name" : "foo", "url" : "bar", "metadata" : { "key1" : "val1", "key2" : "val2" } }' + self.entry : DataCatalogEntry = DataCatalogEntry("foo", "bar", {"key1" : "val1", "key2" : "val2"}) + + def test_create_entry_from_json(self): + entry_from_json = DataCatalogEntry.from_json(self.json_string) + self.assertDictEqual(self.entry.metadata, entry_from_json.metadata) + self.assertEqual(self.entry.name, entry_from_json.name) + self.assertEqual(self.entry.url, entry_from_json.url) + + def test_create_json_from_entry(self): + json_from_entry = self.entry.json() + self.assertEqual(json_from_entry.replace(" ", ""), self.json_string.replace(" ", "")) + +class ConnectionTest(TestCase): + + def setUp(self) -> None: + self.url = os.getenv('DATACAT_URL') + self.user = os.getenv('DATACAT_LOGIN') + self.password = os.getenv('DATACAT_PASSWORD') + # if these are not set, connection can not be properly tested + self.assertIsNotNone(self.url) + self.assertIsNotNone(self.user) + self.assertIsNotNone(self.password) + + def test_create_token(self): + pass + + def test_update_token(self): + pass + + def test_get_object(self): + pass + + def test_create_object(self): + pass # TODO + + def test_list_objects(self): + pass \ No newline at end of file -- GitLab