diff --git a/README.md b/README.md index 0e79121608b641514b7a928e43b64f8ae159a064..851405f567dc0adea442152c483eef17c285c95e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +<!--THIS FILE HAS BEEN GENERATED BY A SCRIPT. PLEASE DON'T CHANGE IT MANUALLY.--> + # Welcome This is the **CaosDB Python Client Library** repository and a part of the diff --git a/examples/server_side_script.py b/examples/server_side_script.py new file mode 100755 index 0000000000000000000000000000000000000000..71bd9c05b4e86133cc356e1c15359701642a9486 --- /dev/null +++ b/examples/server_side_script.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +# -*- coding: 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 +# +"""server_side_script.py. + +An example which implements a minimal server-side script. + +1) This script expects to find a *.txt file in the .upload_files dir which is +printed to stdout. + +2) It executes a "Count stars" query and prints the result to stdout. + +3) It will return with code 0 if everything is ok, or with any code that is +specified with the commandline option --exit +""" + +import sys +from os import listdir +from caosdb import configure_connection, execute_query + + +# parse --auth-token option and configure connection +CODE = 0 +QUERY = "COUNT stars" +for arg in sys.argv: + if arg.startswith("--auth-token="): + auth_token = arg[13:] + configure_connection(auth_token=auth_token) + if arg.startswith("--exit="): + CODE = int(arg[7:]) + if arg.startswith("--query="): + QUERY = arg[8:] + + +############################################################ +# 1 # find and print *.txt file ############################ +############################################################ + +try: + for fname in listdir(".upload_files"): + if fname.endswith(".txt"): + with open(".upload_files/{}".format(fname)) as f: + print(f.read()) +except FileNotFoundError: + pass + + +############################################################ +# 2 # query "COUNT stars" ################################## +############################################################ + +RESULT = execute_query(QUERY) +print(RESULT) + +############################################################ +# 3 ######################################################## +############################################################ + +sys.exit(CODE) diff --git a/pytest.ini b/pytest.ini index abdd410e5f9e9835a3233b22687ad49cc1109235..ca6aad829a3e0607292cf69b8b1d4b7f7758993e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -testpaths = unittests -addopts = -vv --cov=caosdb +testpaths=unittests +addopts=-x -vv --cov=caosdb diff --git a/setup.cfg b/setup.cfg index 74c5620b86c6fd5ee96abea95dab4010c9fb87a3..c46089e4d24843d7d4cc4f83dad6ec1351e4cc3f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,4 @@ +[aliases] +test=pytest [pycodestyle] ignore=E501,E121,E123,E126,E226,E24,E704,W503,W504 diff --git a/setup.py b/setup.py index 6adefd59183ba8f9ddbee3fd097858977e053119..28384d3450f99f82c575b2142c396b2aa9e8ad6e 100755 --- a/setup.py +++ b/setup.py @@ -32,8 +32,9 @@ 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"], + setup_requires=["pytest-runner>=2.0,<3dev"], + 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..98687e3c25286121decb499e233aa0743eff47a8 100644 --- a/src/caosdb/common/administration.py +++ b/src/caosdb/common/administration.py @@ -31,6 +31,71 @@ 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: @@ -154,6 +219,7 @@ def _delete_role(name, **kwargs): def _set_roles(username, roles, realm=None, **kwargs): xml = etree.Element("Roles") + print(roles) for r in roles: xml.append(etree.Element("Role", name=r)) @@ -189,8 +255,9 @@ def _get_roles(username, realm=None, **kwargs): e.msg = "User does not exist." raise ret = set() - for r in etree.fromstring(body)[0]: - ret.add(r.get("name")) + for r in etree.fromstring(body).xpath('/Response/Roles')[0]: + if r.tag == "Role": + ret.add(r.get("name")) return ret @@ -251,11 +318,12 @@ class PermissionRule(): xml = etree.fromstring(body) ret = set() for c in xml: - ret.add(PermissionRule._parse_element(c)) + if c.tag in ["Grant", "Deny"]: + ret.add(PermissionRule._parse_element(c)) return ret def __str__(self): - return self._action + "(" + self._permission + ")" + \ + return str(self._action) + "(" + str(self._permission) + ")" + \ ("P" if self._priority is True else "") def __repr__(self): diff --git a/src/caosdb/common/models.py b/src/caosdb/common/models.py index e07f97134035b79b66f1bfb9c33ee3b6d33ac97d..71c5c2b633222f3050944aecf212ea82979fda5a 100644 --- a/src/caosdb/common/models.py +++ b/src/caosdb/common/models.py @@ -1822,11 +1822,13 @@ class Container(list): insertion, update, and deletion which are a applied to all entities in the container or the whole container respectively. """ + _debug = staticmethod( lambda: ( get_config().getint( "Container", - "debug") if get_config().get( + "debug") if get_config().has_section("Container") and + get_config().get( "Container", "debug") is not None else 0)) diff --git a/src/caosdb/configuration.py b/src/caosdb/configuration.py index 9f6b558abd7bd5e291eaa8bd89ca41ebe0c73bf8..3933845c448da8d6164447aeec76b3aa2c65a929 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,10 @@ 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 d7a98b6f27dd036fa85b97d1a46b90f38b3bb563..0d4e310767bb57a009230c57a4a2e45429923985 100644 --- a/src/caosdb/connection/connection.py +++ b/src/caosdb/connection/connection.py @@ -72,6 +72,14 @@ class _WrappedHTTPResponse(CaosDBHTTPResponse): class _DefaultCaosDBServerConnection(CaosDBServerConnection): + """_DefaultCaosDBServerConnection. + + Methods + ------- + configure + request + """ + def __init__(self): self._useragent = ("PyCaosDB - " "DefaultCaosDBServerConnection") @@ -79,6 +87,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 +121,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"): @@ -146,10 +197,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) @@ -161,29 +238,37 @@ def _get_authenticator(**config): return auth_provider except ImportError: - raise RuntimeError("Password method \"{}\" not implemented. " - "Valid methods: plain, pass, or keyring." - .format(config["password_method"])) + 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()) @@ -242,7 +327,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/src/caosdb/exceptions.py b/src/caosdb/exceptions.py index a5d6e99662c38e6f74e2e974a68ce8152997e271..45c4570b4227031a35a486b86ea65535ff105852 100644 --- a/src/caosdb/exceptions.py +++ b/src/caosdb/exceptions.py @@ -70,9 +70,10 @@ class ClientErrorException(CaosDBException): class ServerErrorException(CaosDBException): def __init__(self, body): xml = etree.fromstring(body) - msg = xml[0].get("description") - if xml[0].text is not None: - msg = msg + "\n\n" + xml[0].text + error = xml.xpath('/Response/Error')[0] + msg = error.get("description") + if error.text is not None: + msg = msg + "\n\n" + error.text CaosDBException.__init__(self, msg) diff --git a/unittests/test_administraction.py b/unittests/test_administraction.py new file mode 100644 index 0000000000000000000000000000000000000000..dc05be2ac7c19ae066b9c8829677626796cea5fa --- /dev/null +++ b/unittests/test_administraction.py @@ -0,0 +1,69 @@ +# -*- 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") diff --git a/unittests/test_connection.py b/unittests/test_connection.py index 06838d1c15b70787be159b4ad2d7f1bb70abe6c8..26e3388dfa4dd7d360b8a373d631f5a362990f66 100644 --- a/unittests/test_connection.py +++ b/unittests/test_connection.py @@ -92,6 +92,8 @@ def test_make_uri_path(): def test_configure_connection(): + if not get_config().has_section("Connection"): + get_config().add_section("Connection") get_config().set("Connection", "url", "https://host.de") get_config().set("Connection", "username", "test_username") get_config().set("Connection", "password", "test_password")