Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • caosdb/src/caosdb-pylib
1 result
Show changes
Commits on Source (19)
...@@ -61,16 +61,6 @@ mypy: ...@@ -61,16 +61,6 @@ mypy:
- make mypy - make mypy
# run unit tests # run unit tests
unittest_py3.8:
tags: [ docker ]
stage: test
needs: [ ]
image: python:3.8
script: &python_test_script
# Python docker has problems with tox and pip so use plain pytest here
- touch ~/.pylinkahead.ini
- pip install .[test]
- python -m pytest unittests
# This needs to be changed once Python 3.9 isn't the standard Python in Debian # This needs to be changed once Python 3.9 isn't the standard Python in Debian
# anymore. # anymore.
...@@ -90,7 +80,11 @@ unittest_py3.10: ...@@ -90,7 +80,11 @@ unittest_py3.10:
stage: test stage: test
needs: [ ] needs: [ ]
image: python:3.10 image: python:3.10
script: *python_test_script script: &python_test_script
# Python docker has problems with tox and pip so use plain pytest here
- touch ~/.pylinkahead.ini
- pip install .[test]
- python -m pytest unittests
unittest_py3.11: unittest_py3.11:
tags: [ docker ] tags: [ docker ]
...@@ -158,7 +152,7 @@ build-testenv: ...@@ -158,7 +152,7 @@ build-testenv:
pages_prepare: &pages_prepare pages_prepare: &pages_prepare
tags: [ cached-dind ] tags: [ cached-dind ]
stage: deploy stage: deploy
needs: [ code_style, pylint, unittest_py3.8, unittest_py3.9, unittest_py3.10 ] needs: [ code_style, pylint, unittest_py3.9, unittest_py3.10 ]
only: only:
refs: refs:
- /^release-.*$/i - /^release-.*$/i
......
...@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* New setup extra `test` which installs the dependencies for testing. * New setup extra `test` which installs the dependencies for testing.
* The Container class has a new member function `filter` which is based o `_filter_entity_list`. * The Container class has a new member function `filter` which is based o `_filter_entity_list`.
* The `Entity` properties `_cuid` and `_flags` are now available for read-only access
as `cuid` and `flags`, respectively.
### Changed ### ### Changed ###
...@@ -18,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -18,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed ### ### Removed ###
* Support for Python 3.8
### Fixed ### ### Fixed ###
* [#73](https://gitlab.com/linkahead/linkahead-pylib/-/issues/73) * [#73](https://gitlab.com/linkahead/linkahead-pylib/-/issues/73)
...@@ -33,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -33,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#87](https://gitlab.com/linkahead/linkahead-pylib/-/issues/87) * [#87](https://gitlab.com/linkahead/linkahead-pylib/-/issues/87)
`XMLSyntaxError` messages when parsing (incomplete) responses in `XMLSyntaxError` messages when parsing (incomplete) responses in
case of certain connection timeouts. case of certain connection timeouts.
The diff returned by compare_entities now uses id instead of name as key if either property does not have a name
* [#127](https://gitlab.com/linkahead/linkahead-pylib/-/issues/127)
pylinkahead.ini now supports None and tuples as values for the `timeout` keyword
### Security ### ### Security ###
......
* caosdb-server >= 0.12.0 * caosdb-server >= 0.12.0
* Python >= 3.8 * Python >= 3.9
* pip >= 20.0.2 * pip >= 20.0.2
Any other dependencies are defined in the setup.py and are being installed via pip Any other dependencies are defined in the setup.py and are being installed via pip
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
### How to install ### ### How to install ###
First ensure that python with at least version 3.8 is installed. Should this not be First ensure that python with at least version 3.9 is installed. Should this not be
the case, you can use the [Installing python](#installing-python-) guide for your OS. the case, you can use the [Installing python](#installing-python-) guide for your OS.
#### Generic installation #### #### Generic installation ####
...@@ -39,7 +39,7 @@ entries `install_requires` and `extras_require`. ...@@ -39,7 +39,7 @@ entries `install_requires` and `extras_require`.
#### Linux #### #### Linux ####
Make sure that Python (at least version 3.8) and pip is installed, using your system tools and Make sure that Python (at least version 3.9) and pip is installed, using your system tools and
documentation. documentation.
Then open a terminal and continue in the [Generic installation](#generic-installation) section. Then open a terminal and continue in the [Generic installation](#generic-installation) section.
......
...@@ -179,7 +179,7 @@ def setup_package(): ...@@ -179,7 +179,7 @@ def setup_package():
"Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Information Analysis",
], ],
packages=find_packages('src'), packages=find_packages('src'),
python_requires='>=3.8', python_requires='>=3.9',
package_dir={'': 'src'}, package_dir={'': 'src'},
install_requires=['lxml>=4.6.3', install_requires=['lxml>=4.6.3',
"requests[socks]>=2.26", "requests[socks]>=2.26",
......
...@@ -349,6 +349,15 @@ class Entity: ...@@ -349,6 +349,15 @@ class Entity:
def pickup(self, new_pickup): def pickup(self, new_pickup):
self.__pickup = new_pickup self.__pickup = new_pickup
@property # getter for _cuid
def cuid(self):
# Set if None?
return self._cuid
@property # getter for _flags
def flags(self):
return self._flags.copy() # for dict[str, str] shallow copy is enough
def grant( def grant(
self, self,
realm: Optional[str] = None, realm: Optional[str] = None,
...@@ -1342,7 +1351,8 @@ class Entity: ...@@ -1342,7 +1351,8 @@ class Entity:
else: else:
dt_str = xml2str(self.datatype.to_xml(visited_entities=visited_entities.copy())) dt_str = xml2str(self.datatype.to_xml(visited_entities=visited_entities.copy()))
# Todo: Use for pretty-printing with calls from _repr_ only? # Todo: Use for pretty-printing with calls from _repr_ only?
# dt_str = dt_str.replace('<', 'ᐸ').replace('>', 'ᐳ').replace(' ', '⠀').replace('"', '\'').replace('\n', '') # dt_str = dt_str.replace('<', 'ᐸ').replace('>', 'ᐳ').replace(' ', '⠀').replace(
# '"', '\'').replace('\n', '')
xml.set("datatype", dt_str) xml.set("datatype", dt_str)
else: else:
xml.set("datatype", str(self.datatype)) xml.set("datatype", str(self.datatype))
...@@ -3758,6 +3768,7 @@ class Container(list): ...@@ -3758,6 +3768,7 @@ class Container(list):
""" """
return _filter_entity_list(self, pid=pid, name=name, entity=entity, return _filter_entity_list(self, pid=pid, name=name, entity=entity,
conjunction=conjunction) conjunction=conjunction)
@staticmethod @staticmethod
def _find_dependencies_in_container(container: Container): def _find_dependencies_in_container(container: Container):
"""Find elements in a container that are a dependency of another element of the same. """Find elements in a container that are a dependency of another element of the same.
......
...@@ -30,6 +30,15 @@ import yaml ...@@ -30,6 +30,15 @@ import yaml
try: try:
optional_jsonschema_validate: Optional[Callable] = None optional_jsonschema_validate: Optional[Callable] = None
from jsonschema import validate as optional_jsonschema_validate from jsonschema import validate as optional_jsonschema_validate
# Adapted from https://github.com/python-jsonschema/jsonschema/issues/148
# Defines Validator to allow parsing of all iterables as array in jsonschema
# CustomValidator can be removed if/once jsonschema allows tuples for arrays
from collections.abc import Iterable
from jsonschema import validators
default = validators.validator_for(True) # Returns latest supported draft
t_c = (default.TYPE_CHECKER.redefine('array', lambda x, y: isinstance(y, Iterable)))
CustomValidator = validators.extend(default, type_checker=t_c)
except ImportError: except ImportError:
pass pass
...@@ -72,14 +81,40 @@ def get_config() -> ConfigParser: ...@@ -72,14 +81,40 @@ def get_config() -> ConfigParser:
return _pycaosdbconf return _pycaosdbconf
def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, bool]]]: def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, bool, tuple, None]]]:
valobj: dict[str, dict[str, Union[int, str, bool]]] = {} """
Generates and returns a dict with all config options and their values
defined in the config.
The values of the options 'debug', 'timeout', and 'ssl_insecure' are
parsed, all other values are saved as string.
Parameters
----------
config : ConfigParser
The config to be converted to a dict
Returns
-------
valobj : dict
A dict with config options and their values as key value pairs
"""
valobj: dict[str, dict[str, Union[int, str, bool, tuple, None]]] = {}
for s in config.sections(): for s in config.sections():
valobj[s] = {} valobj[s] = {}
for key, value in config[s].items(): for key, value in config[s].items():
# TODO: Can the type be inferred from the config object? # TODO: Can the type be inferred from the config object?
if key in ["timeout", "debug"]: if key in ["debug"]:
valobj[s][key] = int(value) valobj[s][key] = int(value)
elif key in ["timeout"]:
value = "".join(value.split()) # Remove whitespace
if str(value).lower() in ["none", "null"]:
valobj[s][key] = None
elif value.startswith('(') and value.endswith(')'):
content = [None if str(s).lower() in ["none", "null"] else int(s)
for s in value[1:-1].split(',')]
valobj[s][key] = tuple(content)
else:
valobj[s][key] = int(value)
elif key in ["ssl_insecure"]: elif key in ["ssl_insecure"]:
valobj[s][key] = bool(value) valobj[s][key] = bool(value)
else: else:
...@@ -88,11 +123,12 @@ def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str, ...@@ -88,11 +123,12 @@ def config_to_yaml(config: ConfigParser) -> dict[str, dict[str, Union[int, str,
return valobj return valobj
def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool]]]): def validate_yaml_schema(valobj: dict[str, dict[str, Union[int, str, bool, tuple, None]]]):
if optional_jsonschema_validate: if optional_jsonschema_validate:
with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f: with open(os.path.join(os.path.dirname(__file__), "schema-pycaosdb-ini.yml")) as f:
schema = yaml.load(f, Loader=yaml.SafeLoader) schema = yaml.load(f, Loader=yaml.SafeLoader)
optional_jsonschema_validate(instance=valobj, schema=schema["schema-pycaosdb-ini"]) optional_jsonschema_validate(instance=valobj, schema=schema["schema-pycaosdb-ini"],
cls=CustomValidator)
else: else:
warnings.warn(""" warnings.warn("""
Warning: The validation could not be performed because `jsonschema` is not installed. Warning: The validation could not be performed because `jsonschema` is not installed.
......
...@@ -39,7 +39,7 @@ from requests.adapters import HTTPAdapter ...@@ -39,7 +39,7 @@ from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError as HTTPConnectionError from requests.exceptions import ConnectionError as HTTPConnectionError
from urllib3.poolmanager import PoolManager from urllib3.poolmanager import PoolManager
from ..configuration import get_config from ..configuration import get_config, config_to_yaml
from ..exceptions import (ConfigurationError, HTTPClientError, from ..exceptions import (ConfigurationError, HTTPClientError,
HTTPForbiddenError, HTTPResourceNotFoundError, HTTPForbiddenError, HTTPResourceNotFoundError,
HTTPServerError, HTTPURITooLongError, HTTPServerError, HTTPURITooLongError,
...@@ -422,8 +422,10 @@ def configure_connection(**kwargs): ...@@ -422,8 +422,10 @@ def configure_connection(**kwargs):
- "keyring" Uses the `keyring` library. - "keyring" Uses the `keyring` library.
- "auth_token" Uses only a given auth_token. - "auth_token" Uses only a given auth_token.
timeout : int timeout : int, tuple, or None
A connection timeout in seconds. (Default: 210) A connection timeout in seconds. (Default: 210)
If a tuple is given, they are used as connect and read timeouts
respectively, timeout None disables the timeout.
ssl_insecure : bool ssl_insecure : bool
Whether SSL certificate warnings should be ignored. Only use this for Whether SSL certificate warnings should be ignored. Only use this for
...@@ -465,21 +467,29 @@ def configure_connection(**kwargs): ...@@ -465,21 +467,29 @@ def configure_connection(**kwargs):
global_conf = {} global_conf = {}
conf = get_config() conf = get_config()
# Convert config to dict, with preserving types # Convert config to dict, with preserving types
int_opts = ["timeout"] int_opts = []
bool_opts = ["ssl_insecure"] bool_opts = ["ssl_insecure"]
other_opts = ["timeout"]
if conf.has_section("Connection"): if conf.has_section("Connection"):
global_conf = dict(conf.items("Connection")) global_conf = dict(conf.items("Connection"))
# Integer options
# Integer options
for opt in int_opts: for opt in int_opts:
if opt in global_conf: if opt in global_conf:
global_conf[opt] = conf.getint("Connection", opt) global_conf[opt] = conf.getint("Connection", opt)
# Boolean options
# Boolean options
for opt in bool_opts: for opt in bool_opts:
if opt in global_conf: if opt in global_conf:
global_conf[opt] = conf.getboolean("Connection", opt) global_conf[opt] = conf.getboolean("Connection", opt)
# Other options, defer parsing to configuration.config_to_yaml:
connection_config = config_to_yaml(conf)["Connection"]
for opt in other_opts:
if opt in global_conf:
global_conf[opt] = connection_config[opt]
local_conf = _make_conf(_DEFAULT_CONF, global_conf, kwargs) local_conf = _make_conf(_DEFAULT_CONF, global_conf, kwargs)
connection = _Connection.get_instance() connection = _Connection.get_instance()
......
...@@ -67,7 +67,13 @@ schema-pycaosdb-ini: ...@@ -67,7 +67,13 @@ schema-pycaosdb-ini:
description: This option is used internally and for testing. Do not override. description: This option is used internally and for testing. Do not override.
examples: [_DefaultCaosDBServerConnection] examples: [_DefaultCaosDBServerConnection]
timeout: timeout:
type: integer oneOf:
- type: [integer, "null"]
- type: array
items:
type: [integer, "null"]
minItems: 2
maxItems: 2
allOf: allOf:
- if: - if:
properties: properties:
......
[Connection]
url=https://localhost:10443/
password_method = unauthenticated
timeout = None
[Connection]
url=https://localhost:10443/
password_method = unauthenticated
timeout = (1,20)
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
from os import environ, getcwd, remove from os import environ, getcwd, remove
from os.path import expanduser, isfile, join from os.path import expanduser, isfile, join
from pathlib import Path
import linkahead as db import linkahead as db
import pytest import pytest
...@@ -66,3 +67,18 @@ def test_config_ini_via_envvar(temp_ini_files): ...@@ -66,3 +67,18 @@ def test_config_ini_via_envvar(temp_ini_files):
assert expanduser("~/.pylinkahead.ini") in db.configuration._read_config_files() assert expanduser("~/.pylinkahead.ini") in db.configuration._read_config_files()
# test configuration file in cwd # test configuration file in cwd
assert join(getcwd(), "pylinkahead.ini") in db.configuration._read_config_files() assert join(getcwd(), "pylinkahead.ini") in db.configuration._read_config_files()
def test_config_timeout_option():
expected_results = [None, (1, 20)]
# Iterate through timeout test configs
test_configs = Path(__file__).parent/'test_configs'
for test_config in test_configs.rglob('pylinkahead-timeout*.ini'):
# Test that test configs can be parsed
db.configure(str(test_config))
dct = db.configuration.config_to_yaml(db.get_config())
# Test that resulting dict has correct content for timeout
assert 'Connection' in dct
assert 'timeout' in dct['Connection']
assert dct['Connection']['timeout'] in expected_results
expected_results.remove(dct['Connection']['timeout'])
...@@ -70,7 +70,8 @@ def test_get_property_values(): ...@@ -70,7 +70,8 @@ def test_get_property_values():
) )
assert len(table) == 2 assert len(table) == 2
house_row = table[0] house_row = table[0]
assert house_row == (house.name, 40.2, "ft", window.id, None, None, None, 20.5, 20.5, "m", owner.name) assert house_row == (house.name, 40.2, "ft", window.id, None, None, None, 20.5, 20.5, "m",
owner.name)
owner_row = table[1] owner_row = table[1]
assert owner_row == (owner.name, None, None, None, None, None, None, None, None, None, None) assert owner_row == (owner.name, None, None, None, None, None, None, None, None, None, None)
...@@ -200,11 +201,12 @@ def test_container_slicing(): ...@@ -200,11 +201,12 @@ def test_container_slicing():
with pytest.raises(TypeError): with pytest.raises(TypeError):
cont[[0, 2, 3]] cont[[0, 2, 3]]
def test_container_filter(): def test_container_filter():
# this is a very rudimentary test since filter is based on _filter_entity_list which is tested # this is a very rudimentary test since filter is based on _filter_entity_list which is tested
# separately # separately
cont = db.Container() cont = db.Container()
cont.extend([db.Record(name=f"TestRec{ii+1}") for ii in range(5)]) cont.extend([db.Record(name=f"TestRec{ii+1}") for ii in range(5)])
recs = cont.filter(name="TestRec2") recs = cont.filter(name="TestRec2")
assert len(recs)==1 assert len(recs) == 1
recs[0].name =="TestRec2" recs[0].name == "TestRec2"