diff --git a/setup.py b/setup.py index f80c619591a55190d70ee105b0cff2e3d79894c1..c0ca6bbbae810642ec47a2ea2781a134baa75207 100755 --- a/setup.py +++ b/setup.py @@ -32,8 +32,8 @@ setup(name='PyCaosDB', author_email='timm.fitschen@ds.mpg.de', packages=find_packages('src'), package_dir={'': 'src'}, - install_requires=['lxml>=3.6.4', 'coverage>=4.4.2', + install_requires=['lxml>=3.6.4', 'PyYaml>=3.12', 'future'], extras_require={'keyring': ['keyring>=13.0.0']}, - tests_require=["pytest"], + tests_require=["pytest", "pytest-cov", "coverage>=4.4.2"], ) diff --git a/src/caosdb/__init__.py b/src/caosdb/__init__.py index 91b429e602245a8cea6b5c939260e754a6de5f53..cc5b3f9be545021c8a884d2fc52080c725047a8d 100644 --- a/src/caosdb/__init__.py +++ b/src/caosdb/__init__.py @@ -42,5 +42,7 @@ from caosdb.common.models import (delete, execute_query, raise_errors, get_global_acl, get_known_permissions) # Import of convenience methods: import caosdb.apiutils + +# read configuration these files configure(expanduser('~/.pycaosdb.ini')) configure(join(getcwd(), "pycaosdb.ini")) diff --git a/src/caosdb/common/administration.py b/src/caosdb/common/administration.py index ee57a370db58c3f1b604014a08b8de457724005f..eff03f8d97b889ea9d934c2edb4d9ba2fe50da9d 100644 --- a/src/caosdb/common/administration.py +++ b/src/caosdb/common/administration.py @@ -31,6 +31,69 @@ from caosdb.connection.connection import get_connection from caosdb.common.utils import xml2str +def set_server_property(key, value): + """set_server_property + + Set a server property. + + Parameters + ---------- + key : str + The name of the server property. + value : str + The value of the server property. + + + Returns + ------- + None + """ + con = get_connection() + + con._form_data_request(method="POST", path="_server_properties", + params={key: value}).read() + +def get_server_properties(): + """get_server_properties + + Get all server properties as a dict. + + Returns + ------- + dict + The server properties. + """ + con = get_connection() + body = con._http_request(method="GET", path="_server_properties").response + xml = etree.parse(body) + props = dict() + for elem in xml.getroot(): + props[elem.tag] = elem.text + return props + +def get_server_property(key): + """get_server_property + + Get a server property. + + Parameters + ---------- + key : str + The name of the server property + + Returns + ------- + value : str + The string value of the server property. + + Raises + ------ + KeyError + If the server property is no defined. + """ + return get_server_properties()[key] + + def _retrieve_user(name, realm=None, **kwargs): con = get_connection() try: diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py index 9f6b558abd7bd5e291eaa8bd89ca41ebe0c73bf8..c8895f90dde1a4a8076b5cc47d3fc265863a41ec 100644 --- a/src/caosdb/configuration.py +++ b/src/caosdb/configuration.py @@ -21,9 +21,6 @@ # # ** end header # - -"""Created on 20.09.2016.""" - try: # python2 from ConfigParser import ConfigParser @@ -31,29 +28,9 @@ except ImportError: # python3 from configparser import ConfigParser -_DEFAULTS = {"Connection": - {"url": None, - "timeout": "200", - "username": None, - "password_method": "plain", - "debug": "0", - "cacert": None}, - "Container": - {"debug": "0"}} - - def _reset_config(): global _pycaosdbconf - pycaosdbconf = None - _pycaosdbconf = ConfigParser(allow_no_value=True) - _init_defaults(_pycaosdbconf) - - -def _init_defaults(confpar): - for sec in _DEFAULTS.keys(): - confpar.add_section(sec) - for opt in _DEFAULTS[sec].keys(): - confpar.set(sec, opt, _DEFAULTS[sec][opt]) + _pycaosdbconf = ConfigParser(allow_no_value=False) def configure(inifile): diff --git a/src/caosdb/connection/connection.py b/src/caosdb/connection/connection.py index 10674e71fc1ae6a1d3a85e871d484593f58fd879..eceaff0bbf7d2c54ed059e46c6d0eafb61107f9d 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -72,6 +72,13 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse): class _DefaultCaosDBServerConnection(CaosDBServerConnection): + """_DefaultCaosDBServerConnection + + Methods + ------- + configure + request + """ def __init__(self): self._useragent = ("PyCaosDB - " "DefaultCaosDBServerConnection") @@ -79,6 +86,28 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection): self._base_path = None def request(self, method, path, headers=None, body=None, **kwargs): + """request + + Send a HTTP request to the server. + + Parameters + ---------- + method : str + The HTTP request method. + path : str + An URI path segment (without the 'scheme://host:port/' parts), + including query and frament segments. + headers : dict of str -> str, optional + HTTP request headers. (Defautl: None) + body : str or bytes or readable, opional + The body of the HTTP request. Bytes should be a utf-8 encoded + string. + **kwargs : + Any keyword arguments will be ignored. + + Returns + ------- + """ if headers is None: headers = {} try: @@ -91,6 +120,27 @@ class _DefaultCaosDBServerConnection(CaosDBServerConnection): return _WrappedHTTPResponse(self._http_con.getresponse()) def configure(self, **config): + """configure + + Configure the http connection. + + Parameters + ---------- + cacert : str + Path to the CA certificate which will be used to identify the + server. + url : str + The url of the CaosDB Server, e.g. + `https://example.com:443/rootpath`, including a possible root path. + **config : + Any further keyword arguments are being ignored. + + Raises + ------ + ConnectionException + If no url has been specified, or if the CA certificate cannot be + loaded. + """ context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) context.verify_mode = ssl.CERT_REQUIRED if hasattr(context, "check_hostname"): @@ -141,10 +191,36 @@ def _make_conf(*conf): _DEFAULT_CONF = {"password_method": "plain", "implementation": - _DefaultCaosDBServerConnection} + _DefaultCaosDBServerConnection, "timeout": 210} def _get_authenticator(**config): + """_get_authenticator + + Import and configure the password_method. + + Parameters + ---------- + password_method : str + The simple name of a submodule of caosdb.connection.authentication. + Currently, there are three valid values for this parameter: 'plain', + 'pass', and 'keyring'. + **config : + Any other keyword arguments are passed the configre method of the + password_method. + + Returns + ------- + AbstractAuthenticator + An object which implements the password_method and which already + configured. + + Raises + ------ + ConnectionException + If the password_method string cannot be resolved to a CaosAuthenticator + class. + """ auth_module = ("caosdb.connection.authentication." + config["password_method"]) _LOGGER.debug("import auth_module %s", auth_module) @@ -156,29 +232,37 @@ def _get_authenticator(**config): return auth_provider except ImportError: - raise RuntimeError("Password method \"{}\" not implemented. " + raise ConfigurationException("Password method \"{}\" not implemented. " "Valid methods: plain, pass, or keyring." .format(config["password_method"])) def configure_connection(**kwargs): - """Configures the caosdb connection. - - All paramters are optional. The default configuration is taken from the - `Connection` section of the configuration which can be retrieved by - `get_config` - - Returns the Connection object. - - Typical arguments are: - url The URL of the CaosDB server. E.g. https://dumiatis01:8433/playground - username The username - password Two options: - 1) The plain text password - 2) A function which returns the password. - timeout A connection timeout in seconds. - implementation A class which implements CaosDBServerConnection. (Default: + """Configures the caosdb connection and return the Connection object. + + The effective configuration is governed by the default values (see + 'Parameters'), the global configuration (see `caosdb.get_config()`) and the + parameters which are passed to this function, with ascending priority. + + The parameters which are listed here, are possibly not sufficient for a + working configuration of the connection. Check the `configure` method of + the implementation class and the password_method for more details. + + Parameters + ---------- + implementation : CaosDBServerConnection + The class which implements the connection. (Default: _DefaultCaosDBServerConnection) + password_method : str + The name of a submodule of caosdb.connection.authentication which + implements the AbstractAuthenticator interface. (Default: 'plain') + timeout : int + A connection timeout in seconds. (Default: 210) + + Returns + ------- + _Connection + The singleton instance of the _Connection class. """ global_conf = (dict(get_config().items("Connection")) if get_config().has_section("Connection") else dict()) @@ -237,7 +321,8 @@ def _handle_response_status(http_response): class _Connection(object): # pylint: disable=useless-object-inheritance """This connection class provides the interface to the database connection - allowing for retrieval, insertion, update, etc. of entries. + allowing for retrieval, insertion, update, etc. of entities, files, users, + roles and much more. It wrapps an instance of CaosDBServerConnection which actually does the work (how, depends on the instance). diff --git a/src/caosdb/connection/mockup.py b/src/caosdb/connection/mockup.py index abde5d03da4d553b523c3ecc63ff7552696cc865..6d1bb1f389823c2824bbaa3f3b1b41462e284692 100644 --- a/src/caosdb/connection/mockup.py +++ b/src/caosdb/connection/mockup.py @@ -47,7 +47,7 @@ class MockUpResponse(CaosDBHTTPResponse): def __init__(self, status, headers, body): self._status = status self.headers = headers - self.body = StringIO(body) + self.response = StringIO(body) @property def status(self): @@ -56,7 +56,7 @@ class MockUpResponse(CaosDBHTTPResponse): def read(self, size=-1): """Return the body of the response.""" - return self.body.read(size) + return self.response.read(size) def getheader(self, name, default=None): """Get the contents of the header `name`, or `default` if there is no diff --git a/unittests/test_administraction.py b/unittests/test_administraction.py new file mode 100644 index 0000000000000000000000000000000000000000..56bedee2d25bb0f1523fd05bd6ce0cb8724ba921 --- /dev/null +++ b/unittests/test_administraction.py @@ -0,0 +1,67 @@ +# -*- encoding: utf-8 -*- +# +# ** header v3.0 +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2018 Research Group Biomedical Physics, +# Max-Planck-Institute for Dynamics and Self-Organization Göttingen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# +# ** end header +# +"""Tests for the administration class.""" +# pylint: disable=missing-docstring +from __future__ import unicode_literals +from pytest import raises +from caosdb import administration, configure_connection, get_connection +from caosdb.connection.mockup import MockUpServerConnection, MockUpResponse + + +def setup(): + configure_connection(url="unittests", username="testuser", + password="testpassword", timeout=200, + implementation=MockUpServerConnection) + + +def test_get_server_properties_success(): + properties = "<Properties><TEST_PROP>TEST_VAL</TEST_PROP></Properties>" + get_connection()._delegate_connection.resources.append( + lambda **kwargs: MockUpResponse(200, {}, properties)) + props = administration.get_server_properties() + assert isinstance(props, dict) + + +def test_get_server_property_success(): + properties = "<Properties><TEST_PROP>TEST_VAL</TEST_PROP></Properties>" + get_connection()._delegate_connection.resources.append( + lambda **kwargs: MockUpResponse(200, {}, properties)) + assert "TEST_VAL" == administration.get_server_property("TEST_PROP") + +def test_get_server_property_key_error(): + properties = "<Properties><TEST_PROP>TEST_VAL</TEST_PROP></Properties>" + get_connection()._delegate_connection.resources.append( + lambda **kwargs: MockUpResponse(200, {}, properties)) + with raises(KeyError) as e: + assert administration.get_server_property("BLA") + +def test_set_server_property(): + def check_form(**kwargs): + assert kwargs["path"] == "_server_properties" + assert kwargs["method"] == "POST" + assert kwargs["body"] == "TEST_PROP=TEST_VAL".encode() + assert kwargs["headers"]["Content-Type"] == "application/x-www-form-urlencoded" + return MockUpResponse(200, {}, "<Properties><TEST_PROP>TEST_VAL</TEST_PROP></Properties>") + get_connection()._delegate_connection.resources.append(check_form) + administration.set_server_property("TEST_PROP", "TEST_VAL")