diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0f9a258de99ba559d280fc5ace74a3f111a9e30e..8845e4070c685230a99958fbebd9377238df32de 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -113,15 +113,8 @@ unittest_py3.13: tags: [ docker ] stage: test needs: [ ] - image: python:3.13-rc - script: - # TODO: Replace by '*python_test_script' as soon as 3.13 has been officially released. - # Python docker has problems with tox and pip so use plain pytest here - - apt update && apt install -y cargo - - touch ~/.pylinkahead.ini - - pip install pynose pytest pytest-cov jsonschema>=4.4.0 setuptools - - pip install . - - python -m pytest unittests + image: python:3.13 + script: *python_test_script # Trigger building of server image and integration tests trigger_build: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1592ac685731f0e132a2317559335f3fed924fa2..ba534987a7340185904c99ce5bd7ac03d823d4c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### +* Official support for Python 3.13 + ### Changed ### ### Deprecated ### -- `connection.get_username`. Use `la.Info().user_info.name` instead. ### Removed ### ### Fixed ### +* [gitlab.indiscale.com#200](https://gitlab.indiscale.com/caosdb/src/caosdb-pylib/-/merge_requests/153) + ``linkahead_admin.py`` prints reasonable error messages when users + or roles don't exist. + ### Security ### ### Documentation ### +## [0.15.1] - 2024-08-21 ## + +### Deprecated ### + +* `connection.get_username`. Use `la.Info().user_info.name` instead. + +### Fixed ### + +* [#128](https://gitlab.com/linkahead/linkahead-pylib/-/issues/128) + Assign `datetime.date` or `datetime.datetime` values to `DATETIME` + properties. + +### Documentation ### + * Added docstrings for `linkahead.models.Info` and `linkahead.models.UserInfo`. ## [0.15.0] - 2024-07-09 ## diff --git a/CITATION.cff b/CITATION.cff index 148cccb1804f7ae254224074dfef408e014f5438..3f51bdf839a5e0451f3d3aaf7f128f61b29927fc 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -20,6 +20,6 @@ authors: given-names: Stefan orcid: https://orcid.org/0000-0001-7214-8125 title: CaosDB - Pylib -version: 0.15.0 +version: 0.15.1 doi: 10.3390/data4020083 -date-released: 2024-07-09 +date-released: 2024-08-21 diff --git a/README_SETUP.md b/README_SETUP.md index b05eff87711b84682aa82bbd0aafd61f2e8c86eb..8a32fbfacb8fd5733c65998b35e52e1c7bbceab1 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -119,6 +119,7 @@ Build documentation in `build/` with `make doc`. - `sphinx` - `sphinx-autoapi` - `recommonmark` +- `sphinx_rtd_theme` ### How to contribute ### diff --git a/examples/set_permissions.py b/examples/set_permissions.py index a558bde73897cb6827c93373cc8327efc10e6e15..4657f2cca182b567c761a777df838825f8e89aef 100755 --- a/examples/set_permissions.py +++ b/examples/set_permissions.py @@ -37,13 +37,13 @@ from caosdb import administration as admin def assert_user_and_role(): """Make sure that users and roles exist. -After calling this function, there will be a user "jane" with the role "human" -and the user "xaxys" with the role "alien". These users and roles are returned. + After calling this function, there will be a user "jane" with the role "human" + and the user "xaxys" with the role "alien". These users and roles are returned. -Returns -------- -out : tuple - ((human_user, human_role), (alien_user, alien_role)) + Returns + ------- + out : tuple + ((human_user, human_role), (alien_user, alien_role)) """ try: @@ -81,15 +81,15 @@ out : tuple def get_entities(count=1): """Retrieve one or more entities. -Parameters ----------- -count : int, optional - How many entities to retrieve. + Parameters + ---------- + count : int, optional + How many entities to retrieve. -Returns -------- -out : Container - A container of retrieved entities, the length is given by the parameter count. + Returns + ------- + out : Container + A container of retrieved entities, the length is given by the parameter count. """ cont = db.execute_query("FIND RECORD 'Human Food'", flags={ "P": "0L{n}".format(n=count)}) @@ -102,20 +102,20 @@ out : Container def set_permission(role_grant, role_deny, cont=None, general=False): """Set the permissions of some entities. -Parameters ----------- -role_grant : str - Role which is granted permissions. + Parameters + ---------- + role_grant : str + Role which is granted permissions. -role_deny : str - Role which is denied permissions. + role_deny : str + Role which is denied permissions. -cont : Container - Entities for which permissions are set. + cont : Container + Entities for which permissions are set. -general : bool, optional - If True, the permissions for the roles will be set. If False (the default), - permissions for the entities in the container will be set. + general : bool, optional + If True, the permissions for the roles will be set. If False (the default), + permissions for the entities in the container will be set. """ # Set general permissions @@ -143,23 +143,23 @@ general : bool, optional def test_permission(granted_user, denied_user, cont): """Tests if the permissions are set correctly for two users. -Parameters ----------- -granted_user : (str, str) - The user which should have permissions to retrieve the entities in `cont`. - Given as (user, password). + Parameters + ---------- + granted_user : (str, str) + The user which should have permissions to retrieve the entities in `cont`. + Given as (user, password). -denied_user : (str, str) - The user which should have no permission to retrieve the entities in `cont`. - Given as (user, password). + denied_user : (str, str) + The user which should have no permission to retrieve the entities in `cont`. + Given as (user, password). -cont : Container - Entities for which permissions are tested. + cont : Container + Entities for which permissions are tested. -Returns -------- -None + Returns + ------- + None """ diff --git a/setup.py b/setup.py index 7b499d91cdd6e52856320a9e573f0742e1d54e81..4e136f6d8d915b2a4437c1d42a0af25b85f45c5b 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ from setuptools import find_packages, setup ISRELEASED = False MAJOR = 0 MINOR = 15 -MICRO = 1 +MICRO = 2 # Do not tag as pre-release until this commit # https://github.com/pypa/packaging/pull/515 # has made it into a release. Probably we should wait for pypa/packaging>=21.4 diff --git a/src/doc/conf.py b/src/doc/conf.py index 98e5b6c66e336c595c0feb6c1c47d5ac1db88a79..e5ead4a5015a606bf91f2e0009d7165f995206b5 100644 --- a/src/doc/conf.py +++ b/src/doc/conf.py @@ -29,10 +29,10 @@ copyright = '2023, IndiScale GmbH' author = 'Daniel Hornung' # The short X.Y version -version = '0.15.1' +version = '0.15.2' # The full version, including alpha/beta/rc tags # release = '0.5.2-rc2' -release = '0.15.1-dev' +release = '0.15.2-dev' # -- General configuration --------------------------------------------------- diff --git a/src/linkahead/cached.py b/src/linkahead/cached.py index cf1d1d34362335f87c5eca094b5aa9d6b750f68d..f9179e1a54997bf99f7158b9b46e2d1068a21e47 100644 --- a/src/linkahead/cached.py +++ b/src/linkahead/cached.py @@ -107,7 +107,7 @@ If a query phrase is given, the result must be unique. If this is not what you def cached_query(query_string: str) -> Container: """A cached version of :func:`linkahead.execute_query<linkahead.common.models.execute_query>`. -All additional arguments are at their default values. + All additional arguments are at their default values. """ result = _cached_access(AccessType.QUERY, query_string, unique=False) @@ -161,11 +161,12 @@ def cache_clear() -> None: def cache_info(): """Return info about the cache that is used by `cached_query` and `cached_get_entity_by`. -Returns -------- + Returns + ------- -out: named tuple - See the standard library :func:`functools.lru_cache` for details.""" + out: named tuple + See the standard library :func:`functools.lru_cache` for details. + """ return _cached_access.cache_info() @@ -188,21 +189,21 @@ def cache_fill(items: dict[Union[str, int], Any], This allows to fill the cache without actually submitting queries. Note that this does not overwrite existing entries with the same keys. -Parameters ----------- + Parameters + ---------- -items: dict - A dictionary with the entries to go into the cache. The keys must be compatible with the - AccessType given in ``kind`` + items: dict + A dictionary with the entries to go into the cache. The keys must be compatible with the + AccessType given in ``kind`` -kind: AccessType, optional - The AccessType, for example ID, name, path or query. + kind: AccessType, optional + The AccessType, for example ID, name, path or query. -unique: bool, optional - If True, fills the cache for :func:`cached_get_entity_by`, presumably with - :class:`linkahead.Entity<linkahead.common.models.Entity>` objects. If False, the cache should be - filled with :class:`linkahead.Container<linkahead.common.models.Container>` objects, for use with - :func:`cached_query`. + unique: bool, optional + If True, fills the cache for :func:`cached_get_entity_by`, presumably with + :class:`linkahead.Entity<linkahead.common.models.Entity>` objects. If False, the cache should be + filled with :class:`linkahead.Container<linkahead.common.models.Container>` objects, for use with + :func:`cached_query`. """ diff --git a/src/linkahead/common/administration.py b/src/linkahead/common/administration.py index dee341fa84dd85cbd41a77c0e2d510a96f2c4824..28ef107579fccb689b7337aed65e054cfbf36c05 100644 --- a/src/linkahead/common/administration.py +++ b/src/linkahead/common/administration.py @@ -345,20 +345,20 @@ def _get_roles(username, realm=None, **kwargs): def _set_permissions(role, permission_rules, **kwargs): """Set permissions for a role. -Parameters ----------- + Parameters + ---------- -role : str - The role for which the permissions are set. + role : str + The role for which the permissions are set. -permission_rules : iterable<PermissionRule> - An iterable with PermissionRule objects. + permission_rules : iterable<PermissionRule> + An iterable with PermissionRule objects. -**kwargs : - Additional arguments which are passed to the HTTP request. + **kwargs : + Additional arguments which are passed to the HTTP request. -Returns -------- + Returns + ------- None """ xml = etree.Element("PermissionRules") @@ -393,15 +393,15 @@ def _get_permissions(role, **kwargs): class PermissionRule(): """Permission rules. -Parameters ----------- -action : str - Either "grant" or "deny" + Parameters + ---------- + action : str + Either "grant" or "deny" -permission : str - For example ``RETRIEVE:*``. + permission : str + For example ``RETRIEVE:*``. -priority : bool, optional + priority : bool, optional Whether the priority shall be set, defaults is False. """ diff --git a/src/linkahead/common/models.py b/src/linkahead/common/models.py index 96851f34bd7558e03fb60d92ca001d8cd7c43171..5689e0799f51584a39c187fd106c4d1b09e9cfc7 100644 --- a/src/linkahead/common/models.py +++ b/src/linkahead/common/models.py @@ -39,6 +39,7 @@ import re import sys from builtins import str from copy import deepcopy +from datetime import date, datetime from functools import cmp_to_key from hashlib import sha512 from os import listdir @@ -50,7 +51,6 @@ from typing import TYPE_CHECKING from typing import Any, Final, Literal, Optional, TextIO, Union if TYPE_CHECKING: - from datetime import datetime from .datatype import DATATYPE from tempfile import _TemporaryFileWrapper from io import BufferedWriter @@ -873,29 +873,29 @@ class Entity: check. Note that, if checked, name or ID should not be None, lest the check fail. -Parameters ----------- + Parameters + ---------- -parent: Entity - Check for this parent. + parent: Entity + Check for this parent. -recursive: bool, optional - Whether to check recursively. + recursive: bool, optional + Whether to check recursively. -check_name: bool, optional - Whether to use the name for ancestry check. + check_name: bool, optional + Whether to use the name for ancestry check. -check_id: bool, optional - Whether to use the ID for ancestry check. + check_id: bool, optional + Whether to use the ID for ancestry check. -retrieve: bool, optional - If False, do not retrieve parents from the server. + retrieve: bool, optional + If False, do not retrieve parents from the server. -Returns -------- -out: bool - True if ``parent`` is a true parent, False otherwise. -""" + Returns + ------- + out: bool + True if ``parent`` is a true parent, False otherwise. + """ if recursive: parents = self.get_parents_recursively(retrieve=retrieve) @@ -930,17 +930,17 @@ out: bool def get_parents_recursively(self, retrieve: bool = True) -> list[Entity]: """Get all ancestors of this entity. -Parameters ----------- + Parameters + ---------- -retrieve: bool, optional - If False, do not retrieve parents from the server. + retrieve: bool, optional + If False, do not retrieve parents from the server. -Returns -------- -out: list[Entity] - The parents of this Entity -""" + Returns + ------- + out: list[Entity] + The parents of this Entity + """ all_parents: list[Entity] = [] self._get_parent_recursively(all_parents, retrieve=retrieve) @@ -1598,15 +1598,15 @@ out: list[Entity] unique=True, flags=None, sync=True): """Update this entity. -There are two possible work-flows to perform this update: -First: - 1) retrieve an entity - 2) do changes - 3) call update method + There are two possible work-flows to perform this update: + First: + 1) retrieve an entity + 2) do changes + 3) call update method -Second: - 1) construct entity with id - 2) call update method. + Second: + 1) construct entity with id + 2) call update method. For slight changes the second one it is more comfortable. Furthermore, it is possible to stay off-line until calling the update method. The name, description, unit, datatype, path, @@ -1687,6 +1687,9 @@ def _parse_value(datatype, value): if isinstance(value, str): return value + if datatype == DATETIME and (isinstance(value, date) or isinstance(value, datetime)): + return value + # deal with collections if isinstance(datatype, str): matcher = re.compile(r"^(?P<col>[^<]+)<(?P<dt>[^>]+)>$") diff --git a/src/linkahead/common/versioning.py b/src/linkahead/common/versioning.py index 2e292e6bb031725fbd6da618c4b888c05072c46b..11cf5f6904b02954eb0b2bddc16478590df167e7 100644 --- a/src/linkahead/common/versioning.py +++ b/src/linkahead/common/versioning.py @@ -105,7 +105,7 @@ class Version(): is_head: Union[bool, str, None] = False, is_complete_history: Union[bool, str, None] = False): """Typically the `predecessors` or `successors` should not "link back" to an existing Version -object.""" + object.""" self.id = id self.date = date self.username = username diff --git a/src/linkahead/utils/create_revision.py b/src/linkahead/utils/create_revision.py index 5f6ecc8148859d0ee0908412ff80d20d465cdb25..cde4bae5b0d919977d220b2c35896dcb20e933e7 100644 --- a/src/linkahead/utils/create_revision.py +++ b/src/linkahead/utils/create_revision.py @@ -34,15 +34,15 @@ def bend_references(from_id, to_id, except_for=None): and those references are changed to point to to_id. entities having an id listed in except_for are excluded. -Parameters ----------- + Parameters + ---------- -from_id : int - the old object to which references where pointing -to_id : int - the new object to which references will be pointing -except_for : list of int - entities with id of this list will not be changed + from_id : int + the old object to which references where pointing + to_id : int + the new object to which references will be pointing + except_for : list of int + entities with id of this list will not be changed """ if except_for is None: except_for = [to_id] @@ -73,16 +73,16 @@ def create_revision(old_id, prop, value): This function changes the record with id old_id. The value of the propertye prop is changed to value. -Parameters ----------- + Parameters + ---------- -old_id : int - id of the record to be changed -prop : string - name of the property to be changed -value : type of corresponding property - the new value of the corresponding property -""" + old_id : int + id of the record to be changed + prop : string + name of the property to be changed + value : type of corresponding property + the new value of the corresponding property + """ record = db.execute_query("FIND {}".format(old_id))[0] new_rec = record.copy() new_rec.get_property(prop).value = value diff --git a/src/linkahead/utils/get_entity.py b/src/linkahead/utils/get_entity.py index 0ffd89e4dc7f214bbc72d4508f6ca4481dad7d9c..dd91cdc27b3f6adb52ddef36a59d1a0965fb662e 100644 --- a/src/linkahead/utils/get_entity.py +++ b/src/linkahead/utils/get_entity.py @@ -30,13 +30,13 @@ from .escape import escape_squoted_text def get_entity_by_name(name: str, role: Optional[str] = None) -> Entity: """Return the result of a unique query that uses the name to find the correct entity. -Submits the query "FIND {role} WITH name='{name}'". + Submits the query "FIND {role} WITH name='{name}'". -Parameters ----------- + Parameters + ---------- -role: str, optional - The role for the query, defaults to ``ENTITY``. + role: str, optional + The role for the query, defaults to ``ENTITY``. """ name = escape_squoted_text(name) if role is None: @@ -48,13 +48,13 @@ role: str, optional def get_entity_by_id(eid: Union[str, int], role: Optional[str] = None) -> Entity: """Return the result of a unique query that uses the id to find the correct entity. -Submits the query "FIND {role} WITH id='{eid}'". + Submits the query "FIND {role} WITH id='{eid}'". -Parameters ----------- + Parameters + ---------- -role: str, optional - The role for the query, defaults to ``ENTITY``. + role: str, optional + The role for the query, defaults to ``ENTITY``. """ if role is None: role = "ENTITY" @@ -65,13 +65,13 @@ role: str, optional def get_entity_by_path(path: str) -> Entity: """Return the result of a unique query that uses the path to find the correct file. -Submits the query "FIND {role} WHICH IS STORED AT '{path}'". + Submits the query "FIND {role} WHICH IS STORED AT '{path}'". -Parameters ----------- + Parameters + ---------- -role: str, optional - The role for the query, defaults to ``ENTITY``. + role: str, optional + The role for the query, defaults to ``ENTITY``. """ # type hint can be ignored, it's a unique query return execute_query(f"FIND FILE WHICH IS STORED AT '{path}'", unique=True) # type: ignore diff --git a/src/linkahead/utils/linkahead_admin.py b/src/linkahead/utils/linkahead_admin.py index cbc5a80d13b79b778591d4595411c45cb77d415e..ca5f3c01e0bbe95fe712761ec7f443ec88d406fd 100755 --- a/src/linkahead/utils/linkahead_admin.py +++ b/src/linkahead/utils/linkahead_admin.py @@ -33,7 +33,7 @@ from argparse import ArgumentParser, RawDescriptionHelpFormatter import linkahead as db from linkahead import administration as admin -from linkahead.exceptions import HTTPClientError +from linkahead.exceptions import HTTPClientError, HTTPResourceNotFoundError, HTTPForbiddenError __all__ = [] __version__ = 0.3 @@ -49,19 +49,35 @@ def do_update_role(args): role_name: Name of the role to update role_description: New description of the role """ - admin._update_role(name=args.role_name, description=args.role_description) + try: + admin._update_role(name=args.role_name, description=args.role_description) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot update role '{args.role_name}', " + f"reason: '{e.msg}'") def do_create_role(args): - admin._insert_role(name=args.role_name, description=args.role_description) + try: + admin._insert_role(name=args.role_name, description=args.role_description) + except (HTTPClientError, HTTPForbiddenError) as e: + print(f"Error: Cannot create role '{args.role_name}', " + f"reason: '{e.msg}'") def do_retrieve_role(args): - print(admin._retrieve_role(name=args.role_name)) + try: + print(admin._retrieve_role(name=args.role_name)) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot retrieve role '{args.role_name}', " + f"reason: '{e.msg}'") def do_delete_role(args): - admin._delete_role(name=args.role_name) + try: + admin._delete_role(name=args.role_name) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot delete role '{args.role_name}', " + f"reason: '{e.msg}'") def do_retrieve(args): @@ -132,22 +148,25 @@ def do_create_user(args): email=args.user_email, password=password) if args.activate_user: do_activate_user(args) - except HTTPClientError as e: - print(e.msg) + except (HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot create user '{args.user_name}', " + f"reason: '{e.msg}'") def do_activate_user(args): try: admin._update_user(name=args.user_name, status="ACTIVE") - except HTTPClientError as e: - print(e.msg) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot activate user '{args.user_name}', " + f"reason: '{e.msg}'") def do_deactivate_user(args): try: admin._update_user(name=args.user_name, status="INACTIVE") - except HTTPClientError as e: - print(e.msg) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot deactivate user '{args.user_name}', " + f"reason: '{e.msg}'") def do_set_user_password(args): @@ -157,57 +176,109 @@ def do_set_user_password(args): password = args.user_password try: admin._update_user(name=args.user_name, password=password, realm=args.realm) - except HTTPClientError as e: - print(e.msg) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot set password for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_add_user_roles(args): - roles = admin._get_roles(username=args.user_name, realm=None) + try: + roles = admin._get_roles(username=args.user_name, realm=None) + except (HTTPForbiddenError, HTTPResourceNotFoundError) as e: + print(f"Error: Cannot access roles for user '{args.user_name}', " + f"reason: '{e.msg}'") + return for r in args.user_roles: roles.add(r) - admin._set_roles(username=args.user_name, roles=roles) + try: + admin._set_roles(username=args.user_name, roles=roles) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot add new roles for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_remove_user_roles(args): - roles = admin._get_roles(username=args.user_name, realm=None) + try: + roles = admin._get_roles(username=args.user_name, realm=None) + except (HTTPForbiddenError, HTTPResourceNotFoundError) as e: + print(f"Error: Cannot access roles for user '{args.user_name}', " + f"reason: '{e.msg}'") + return for r in args.user_roles: if r in roles: roles.remove(r) - admin._set_roles(username=args.user_name, roles=roles) + try: + admin._set_roles(username=args.user_name, roles=roles) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot remove roles from user '{args.user_name}', " + f"reason: '{e.msg}'") def do_set_user_entity(args): - admin._update_user(name=args.user_name, entity=args.user_entity) + try: + admin._update_user(name=args.user_name, entity=args.user_entity) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot set entity for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_reset_user_entity(args): - admin._update_user(name=args.user_name, entity="") + try: + admin._update_user(name=args.user_name, entity="") + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot remove entity for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_set_user_email(args): - admin._update_user(name=args.user_name, email=args.user_email) + try: + admin._update_user(name=args.user_name, email=args.user_email) + except (HTTPResourceNotFoundError, HTTPForbiddenError, HTTPClientError) as e: + print(f"Error: Cannot set email for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_retrieve_user(args): - print(admin._retrieve_user(name=args.user_name)) + try: + print(admin._retrieve_user(name=args.user_name)) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot retrieve user '{args.user_name}', " + f"reason: '{e.msg}'") def do_delete_user(args): - admin._delete_user(name=args.user_name) + try: + admin._delete_user(name=args.user_name) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot delete user '{args.user_name}', " + f"reason: '{e.msg}'") def do_retrieve_user_roles(args): - print(admin._get_roles(username=args.user_name)) + try: + print(admin._get_roles(username=args.user_name)) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot retrieve roles for user '{args.user_name}', " + f"reason: '{e.msg}'") def do_retrieve_role_permissions(args): - print(admin._get_permissions(role=args.role_name)) + try: + print(admin._get_permissions(role=args.role_name)) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot retrieve permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") def do_grant_role_permissions(args): - perms = admin._get_permissions(args.role_name) + try: + perms = admin._get_permissions(role=args.role_name) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot access permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") + return for p in args.role_permissions: g = admin.PermissionRule( @@ -221,11 +292,20 @@ def do_grant_role_permissions(args): if d in perms: perms.remove(d) perms.add(g) - admin._set_permissions(role=args.role_name, permission_rules=perms) + try: + admin._set_permissions(role=args.role_name, permission_rules=perms) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot set permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") def do_revoke_role_permissions(args): - perms = admin._get_permissions(args.role_name) + try: + perms = admin._get_permissions(role=args.role_name) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot access permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") + return for p in args.role_permissions: g = admin.PermissionRule( @@ -238,11 +318,20 @@ def do_revoke_role_permissions(args): if d in perms: perms.remove(d) - admin._set_permissions(role=args.role_name, permission_rules=perms) + try: + admin._set_permissions(role=args.role_name, permission_rules=perms) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot revoke permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") def do_deny_role_permissions(args): - perms = admin._get_permissions(args.role_name) + try: + perms = admin._get_permissions(role=args.role_name) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot access permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") + return for p in args.role_permissions: g = admin.PermissionRule( @@ -256,7 +345,11 @@ def do_deny_role_permissions(args): if d in perms: perms.remove(d) perms.add(d) - admin._set_permissions(role=args.role_name, permission_rules=perms) + try: + admin._set_permissions(role=args.role_name, permission_rules=perms) + except (HTTPResourceNotFoundError, HTTPForbiddenError) as e: + print(f"Error: Cannot deny permissions for role '{args.role_name}', " + f"reason: '{e.msg}'") def do_retrieve_entity_acl(args): diff --git a/src/linkahead/utils/server_side_scripting.py b/src/linkahead/utils/server_side_scripting.py index 06caa3d94a629e368dc99f83dc2957c756b7b487..867155cf1f93bf1936e9b19f14926726f362edaf 100644 --- a/src/linkahead/utils/server_side_scripting.py +++ b/src/linkahead/utils/server_side_scripting.py @@ -99,6 +99,19 @@ def _make_request(call, pos_args, opts, files=None): def run_server_side_script(call, *args, files=None, **kwargs): """ + Parameters + ---------- + call : str + name of the script to be called, potentially with path prefix (e.g. ``management/update.py``) + *args : list(str) + list of positional arguments + files : dict + dictionary with where keys are the argument names with prefix (e.g. ``-p1`` or ``-Ofile``) and + the values are the paths to the files to be uploaded. Note, that only the base name will be + used when uploaded. Files will be placed in the ``.upload_files`` folder. Thus, the script + will be called with the argument ``<key>=.upload_files/<basename>``. + **kwargs : dict + kwargs will be passed to ``_make_request`` Return ------ diff --git a/tox.ini b/tox.ini index bbaaa1fc9eec2aba87c247d783818d215d8a7d5e..592c660c5bbbf5805a3ecbb3e60c41f597182a55 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps = . mypy jsonschema>=4.4.0 setuptools -commands=py.test --cov=caosdb -vv {posargs} +commands=py.test --cov=linkahead -vv {posargs} [flake8] max-line-length=100 diff --git a/unittests/test_issues.py b/unittests/test_issues.py index 7472f710cea32c1d76f11e52fe7c3c3617804c3c..e24afbe8b7be8d9a87d85819eccd3a4bf0d453e8 100644 --- a/unittests/test_issues.py +++ b/unittests/test_issues.py @@ -24,6 +24,7 @@ import os import lxml import linkahead as db +from datetime import date, datetime from pytest import raises @@ -64,3 +65,28 @@ def test_issue_156(): # </ParentList> assert value is project assert parents[0].name == "RTName" + + +def test_issue_128(): + """Test assigning datetime.date(time) values to DATETIME + properties: + https://gitlab.com/linkahead/linkahead-pylib/-/issues/128. + + """ + # Test assignement correct assignment for both datatype=DATETIME + # and datatype=LIST<DATETIME>, just to be sure. + prop = db.Property(name="TestDatetime", datatype=db.DATETIME) + prop_list = db.Property(name="TestListDatetime", datatype=db.LIST(db.DATETIME)) + + today = date.today() + now = datetime.now() + + prop.value = today + assert prop.value == today + prop.value = now + assert prop.value == now + + prop_list.value = [today, today] + assert prop_list.value == [today, today] + prop_list.value = [now, now] + assert prop_list.value == [now, now]