diff --git a/src/common/orm/_Database.py b/src/common/orm/Database.py similarity index 58% rename from src/common/orm/_Database.py rename to src/common/orm/Database.py index c1a0a9065b33adf8309ae5394d7dbf1594abcfbf..9f29e9d617828bc63f9e2238257e4009aee2bfe9 100644 --- a/src/common/orm/_Database.py +++ b/src/common/orm/Database.py @@ -1,38 +1,19 @@ import logging from typing import List, Set from .backend._Backend import _Backend -from .model.Model import Model -from .Exceptions import MutexException LOGGER = logging.getLogger(__name__) -class _Database(Model): +class Database: def __init__(self, backend : _Backend): if not isinstance(backend, _Backend): str_class_path = '{}.{}'.format(_Backend.__module__, _Backend.__name__) raise AttributeError('backend must inherit from {}'.format(str_class_path)) self._backend = backend - super().__init__(self) - self._acquired = False - self._owner_key = None - - @property - def parent(self) -> '_Database': return self @property def backend(self) -> _Backend: return self._backend - @property - def backend_key(self) -> str: return '' - - #def __enter__(self) -> '_Database': - # self._acquired, self._owner_key = self._backend.lock() - # if not self._acquired: raise MutexException('Unable to acquire database lock') - # return self - - #def __exit__(self, exc_type, exc_val, exc_tb) -> None: - # self._backend.unlock(self._owner_key) - def clear_all(self, keep_keys : Set[str] = set()) -> None: for key in self._backend.keys(): if key in keep_keys: continue diff --git a/src/common/orm/Exceptions.py b/src/common/orm/Exceptions.py index 0d4cb33f91be9df3a9e237351312cb666c2a9654..eea0b564e1918cb6a2da0553641c9492a32b1425 100644 --- a/src/common/orm/Exceptions.py +++ b/src/common/orm/Exceptions.py @@ -1,2 +1,5 @@ +class ConstraintException(Exception): + pass + class MutexException(Exception): pass diff --git a/src/common/orm/backend/_Backend.py b/src/common/orm/backend/_Backend.py index a8dd7eac293e101605c461ef9131deaf72e54507..a143b411366c3636cd287bfa6f6a4922a7919d0d 100644 --- a/src/common/orm/backend/_Backend.py +++ b/src/common/orm/backend/_Backend.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Set, Tuple class _Backend: def __init__(self, **settings) -> None: @@ -22,7 +22,7 @@ class _Backend: def dict_get(self, key : List[str], fields : List[str] = []) -> Dict[str, str]: raise NotImplementedError() - def dict_update(self, key : List[str], fields : Dict[str,str] = {}) -> None: + def dict_update(self, key : List[str], fields : Dict[str, str] = {}) -> None: raise NotImplementedError() def dict_delete(self, key : List[str], fields : List[str] = []) -> None: @@ -43,8 +43,11 @@ class _Backend: def set_has(self, key : List[str], item : str) -> bool: raise NotImplementedError() + def set_get_all(self, key : List[str]) -> Set[str]: + raise NotImplementedError() + def set_remove(self, key : List[str], item : str) -> None: raise NotImplementedError() - def dump(self) -> List[str]: + def dump(self) -> List[Tuple[str, str, str]]: raise NotImplementedError() diff --git a/src/common/orm/backend/inmemory/InMemoryBackend.py b/src/common/orm/backend/inmemory/InMemoryBackend.py index 13a38027555799e23cabaf32c3d8ef55d826ef15..be1410b95921205b8230679f3cd5e4d3a7b0ff7c 100644 --- a/src/common/orm/backend/inmemory/InMemoryBackend.py +++ b/src/common/orm/backend/inmemory/InMemoryBackend.py @@ -9,7 +9,7 @@ import copy, logging, threading, uuid from typing import Dict, List, Optional, Set, Tuple, Union from .._Backend import _Backend from ..Tools import key_to_str -from .Tools import get_or_create_dict, get_or_create_list, get_or_create_set +from .Tools import get_dict, get_list, get_or_create_dict, get_or_create_list, get_or_create_set, get_set LOGGER = logging.getLogger(__name__) @@ -32,11 +32,11 @@ class InMemoryBackend(_Backend): if str_key not in self._keys: continue del self._keys[str_key] return False, None - else: - # lock available, temporarily acquire it; locks will be released if some of them for a requested - # key is not available - self._keys[str_key] = owner_key - acquired_lock_keys[str_key] = owner_key + + # lock available, temporarily acquire it; locks will be released if some of them for a requested + # key is not available + self._keys[str_key] = owner_key + acquired_lock_keys[str_key] = owner_key return True, owner_key def unlock(self, keys : List[List[str]], owner_key : str) -> bool: @@ -69,7 +69,8 @@ class InMemoryBackend(_Backend): def dict_get(self, key : List[str], fields : List[str] = []) -> Dict[str, str]: str_key = key_to_str(key) with self._lock: - container = get_or_create_dict(self._keys, str_key) + container = get_dict(self._keys, str_key) + if container is None: return None if len(fields) == 0: fields = container.keys() return copy.deepcopy({ field_name : field_value for field_name,field_value in container.items() if field_name in fields @@ -90,11 +91,13 @@ class InMemoryBackend(_Backend): else: container = get_or_create_dict(self._keys, str_key) for field in list(fields): container.pop(field, None) + if len(container) == 0: self._keys.pop(str_key) def list_get_all(self, key : List[str]) -> List[str]: str_key = key_to_str(key) with self._lock: - container = get_or_create_list(self._keys, str_key) + container = get_list(self._keys, str_key) + if container is None: return None return copy.deepcopy(container) def list_push_last(self, key : List[str], item : str) -> None: @@ -108,6 +111,7 @@ class InMemoryBackend(_Backend): with self._lock: container = get_or_create_list(self._keys, str_key) container.remove(item) + if len(container) == 0: self._keys.pop(str_key) def set_add(self, key : List[str], item : str) -> None: str_key = key_to_str(key) @@ -121,11 +125,19 @@ class InMemoryBackend(_Backend): container = get_or_create_set(self._keys, str_key) return item in container + def set_get_all(self, key : List[str]) -> Set[str]: + str_key = key_to_str(key) + with self._lock: + container = get_set(self._keys, str_key) + if container is None: return None + return copy.deepcopy(container) + def set_remove(self, key : List[str], item : str) -> None: str_key = key_to_str(key) with self._lock: container = get_or_create_set(self._keys, str_key) container.discard(item) + if len(container) == 0: self._keys.pop(str_key) def dump(self) -> List[Tuple[str, str, str]]: with self._lock: diff --git a/src/common/orm/backend/inmemory/Tools.py b/src/common/orm/backend/inmemory/Tools.py index f8fb573504bc6daa9622c79482fc0676a31058f2..fe10506556de36d6f40665c6f59119cbf540d8a4 100644 --- a/src/common/orm/backend/inmemory/Tools.py +++ b/src/common/orm/backend/inmemory/Tools.py @@ -1,5 +1,8 @@ from typing import Dict, List, Set, Union +def get_dict(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> Dict: + return keys.get(str_key, None) + def get_or_create_dict(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> Dict: container = keys.get(str_key, None) if container is None: container = keys.setdefault(str_key, dict()) @@ -7,6 +10,9 @@ def get_or_create_dict(keys : Dict[str, Union[Dict, List, Set]], str_key : str) raise Exception('Key({:s}, {:s}) is not a dict'.format(str(type(container).__name__), str(str_key))) return container +def get_list(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> List: + return keys.get(str_key, None) + def get_or_create_list(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> List: container = keys.get(str_key, None) if container is None: container = keys.setdefault(str_key, list()) @@ -14,6 +20,9 @@ def get_or_create_list(keys : Dict[str, Union[Dict, List, Set]], str_key : str) raise Exception('Key({:s}, {:s}) is not a list'.format(str(type(container).__name__), str(str_key))) return container +def get_set(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> Set: + return keys.get(str_key, None) + def get_or_create_set(keys : Dict[str, Union[Dict, List, Set]], str_key : str) -> Set: container = keys.get(str_key, None) if container is None: container = keys.setdefault(str_key, set()) diff --git a/src/common/orm/backend/redis/RedisBackend.py b/src/common/orm/backend/redis/RedisBackend.py index 028c735540950d2f2c58145f595be278934450b7..99ae304c91fa86f94c3674ef41c2f31bc7dda413 100644 --- a/src/common/orm/backend/redis/RedisBackend.py +++ b/src/common/orm/backend/redis/RedisBackend.py @@ -1,5 +1,5 @@ import os, uuid -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple from redis.client import Redis from .._Backend import _Backend from ..Tools import key_to_str @@ -41,30 +41,6 @@ class RedisBackend(_Backend): str_key = key_to_str(key) return self._client.delete(str_key) == 1 - def set_has(self, key : List[str], item : str) -> bool: - str_key = key_to_str(key) - return self._client.sismember(str_key, item) == 1 - - def set_add(self, key : List[str], item : str) -> None: - str_key = key_to_str(key) - self._client.sadd(str_key, item) - - def set_remove(self, key : List[str], item : str) -> None: - str_key = key_to_str(key) - self._client.srem(str_key, item) - - def list_push_last(self, key : List[str], item : str) -> None: - str_key = key_to_str(key) - self._client.rpush(str_key, item) - - def list_get_all(self, key : List[str]) -> List[str]: - str_key = key_to_str(key) - return list(map(lambda m: m.decode('UTF-8'), self._client.lrange(str_key, 0, -1))) - - def list_remove_first_occurrence(self, key : List[str], item: str) -> None: - str_key = key_to_str(key) - self._client.lrem(str_key, 1, item) - def dict_get(self, key : List[str], fields : List[str] = []) -> Dict[str, str]: str_key = key_to_str(key) if len(fields) == 0: @@ -91,6 +67,34 @@ class RedisBackend(_Backend): else: self._client.hdel(str_key, set(fields)) + def list_get_all(self, key : List[str]) -> List[str]: + str_key = key_to_str(key) + return list(map(lambda m: m.decode('UTF-8'), self._client.lrange(str_key, 0, -1))) + + def list_push_last(self, key : List[str], item : str) -> None: + str_key = key_to_str(key) + self._client.rpush(str_key, item) + + def list_remove_first_occurrence(self, key : List[str], item: str) -> None: + str_key = key_to_str(key) + self._client.lrem(str_key, 1, item) + + def set_add(self, key : List[str], item : str) -> None: + str_key = key_to_str(key) + self._client.sadd(str_key, item) + + def set_has(self, key : List[str], item : str) -> bool: + str_key = key_to_str(key) + return self._client.sismember(str_key, item) == 1 + + def set_get_all(self, key : List[str]) -> Set[str]: + str_key = key_to_str(key) + return set(map(lambda m: m.decode('UTF-8'), self._client.smembers(str_key))) + + def set_remove(self, key : List[str], item : str) -> None: + str_key = key_to_str(key) + self._client.srem(str_key, item) + def dump(self) -> List[Tuple[str, str, str]]: entries = [] for str_key in self._client.keys(): diff --git a/src/common/orm/fields/BooleanField.py b/src/common/orm/fields/BooleanField.py index e802b4e4bf1ed6b69b57a2d84b47010618f63bd7..f159ada45d39158eadf4c46150c4b80daa3263f8 100644 --- a/src/common/orm/fields/BooleanField.py +++ b/src/common/orm/fields/BooleanField.py @@ -1,6 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING -from common.type_checkers.Checkers import chk_boolean, chk_not_none +from common.type_checkers.Checkers import chk_boolean from .Field import Field if TYPE_CHECKING: @@ -13,12 +13,15 @@ class BooleanField(Field): super().__init__(*args, type_=bool, **kwargs) def __set__(self, instance : 'Model', value : bool) -> None: - if self.required: chk_not_none(self.name, value) - if value is None: super().__set__(instance, value) - super().__set__(instance, chk_boolean(self.name, value)) + super().__set__(instance, self.validate(value)) + + def validate(self, value): + value = super().validate(value) + return None if value is None else chk_boolean(self.name, value) def serialize(self, value : bool) -> str: - return str(value) + value = self.validate(value) + return None if value is None else str(value) def deserialize(self, value : str) -> bool: return (value.upper() in BOOL_TRUE_VALUES) diff --git a/src/common/orm/fields/Field.py b/src/common/orm/fields/Field.py index 80ba0a8c8229ce6c68335e1e63627c808f9ec364..995fa9342c6b82a57242cbd1941207a299cdbc13 100644 --- a/src/common/orm/fields/Field.py +++ b/src/common/orm/fields/Field.py @@ -1,26 +1,43 @@ from __future__ import annotations +from abc import ABC, abstractmethod +import logging from typing import TYPE_CHECKING, Any, List, Set, Tuple, Union -from common.type_checkers.Checkers import chk_boolean, chk_string, chk_type +from common.type_checkers.Checkers import chk_boolean, chk_not_none, chk_string, chk_type if TYPE_CHECKING: from ..model.Model import Model -class Field: +LOGGER = logging.getLogger(__name__) + +# Ref: https://docs.python.org/3.9/howto/descriptor.html + +class Field(ABC): def __init__( self, name : str = None, type_ : Union[type, Set[type], Tuple[type], List[type]] = object, required : bool = False) -> None: + self.name = None if name is None else chk_string('Field.name', name) self.type_ = chk_type('Field.type', type_, (type, set, tuple, list)) self.required = chk_boolean('Field.required', required) + def __get__(self, instance : 'Model', objtype=None): + if instance is None: return self + return instance.__dict__.get(self.name) + def __set__(self, instance : 'Model', value : Any) -> None: instance.__dict__[self.name] = value def __delete__(self, instance : 'Model'): raise AttributeError('Attribute "{:s}" cannot be deleted'.format(self.name)) + def validate(self, value): + if self.required: chk_not_none(self.name, value, reason='is required. It cannot be None.') + return value + + @abstractmethod def serialize(self, value : Any) -> str: raise NotImplementedError + @abstractmethod def deserialize(self, value : str) -> Any: raise NotImplementedError diff --git a/src/common/orm/fields/FloatField.py b/src/common/orm/fields/FloatField.py index b1485b33f07d71dd684aa0b1b1dd87a2e3ec7058..1b0cb2da3bf57887041d445ccb54c8684cbe2e70 100644 --- a/src/common/orm/fields/FloatField.py +++ b/src/common/orm/fields/FloatField.py @@ -1,6 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from common.type_checkers.Checkers import chk_float, chk_not_none +from common.type_checkers.Checkers import chk_float from .Field import Field if TYPE_CHECKING: @@ -17,12 +17,16 @@ class FloatField(Field): chk_float('{}.max_value'.format(self.name), max_value, min_value=self._min_value) def __set__(self, instance : 'Model', value : float) -> None: - if self.required: chk_not_none(self.name, value) - if value is None: super().__set__(instance, value) - super().__set__(instance, chk_float(self.name, value, min_value=self._min_value, max_value=self._max_value)) + super().__set__(instance, self.validate(value)) - def serialize(self, value : float) -> str: - return str(value) + def validate(self, value): + value = super().validate(value) + return None if value is None else chk_float( + self.name, value, min_value=self._min_value, max_value=self._max_value) + + def serialize(self, value : bool) -> str: + value = self.validate(value) + return None if value is None else str(value) def deserialize(self, value : str) -> float: return float(value) diff --git a/src/common/orm/fields/ForeignKeyField.py b/src/common/orm/fields/ForeignKeyField.py index ac34403d5bd7e6edef98a160e0c2d1d1b71dcb2b..c1cab6245711f01075e66e89c2e6fa81cf52bd74 100644 --- a/src/common/orm/fields/ForeignKeyField.py +++ b/src/common/orm/fields/ForeignKeyField.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING -from common.type_checkers.Checkers import chk_issubclass, chk_not_none, chk_type -from ..backend.Tools import key_to_str +from typing import TYPE_CHECKING, Union +from common.orm.Exceptions import ConstraintException +from common.type_checkers.Checkers import chk_issubclass, chk_type from .StringField import StringField if TYPE_CHECKING: @@ -13,8 +13,19 @@ class ForeignKeyField(StringField): self.foreign_model : Model = chk_issubclass('Field.foreign_model', foreign_model, Model) super().__init__(*args, required=required, allow_empty=not required, **kwargs) - def __set__(self, instance : 'Model', value : 'Model') -> None: - if self.required: chk_not_none(self.name, value) - if value is None: super().__set__(instance, value) + def __set__(self, instance : 'Model', value : Union['Model', str]) -> None: model_instance : 'Model' = chk_type('value', value, self.foreign_model) - super().__set__(instance, key_to_str(model_instance.backend_key)) + super().__set__(instance, model_instance.instance_key) + + def __delete__(self, instance: 'Model'): + if self.required: + msg = 'ForeignKey({}) is required. Unable to clear it.' + raise ConstraintException(msg.format(str(self.name))) + super().__set__(instance, None) + + def serialize(self, value: str) -> str: + if not self.required and value is None: return None + if self.required and (value is None or len(value) == 0): + msg = 'ForeignKey({}, {}) is required. It cannot be serialized as empty.' + raise ConstraintException(msg.format(str(self.name), str(value))) + return super().serialize(value) diff --git a/src/common/orm/fields/IntegerField.py b/src/common/orm/fields/IntegerField.py index 72cccdac7fd7bad734e9503434764be7c9e797ea..ae86f0c41b33e9a729525aebf9fa65410d8b35c5 100644 --- a/src/common/orm/fields/IntegerField.py +++ b/src/common/orm/fields/IntegerField.py @@ -1,6 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional -from common.type_checkers.Checkers import chk_integer, chk_not_none +from common.type_checkers.Checkers import chk_integer from .Field import Field if TYPE_CHECKING: @@ -17,12 +17,16 @@ class IntegerField(Field): chk_integer('{}.max_value'.format(self.name), max_value, min_value=self._min_value) def __set__(self, instance : 'Model', value : int) -> None: - if self.required: chk_not_none(self.name, value) - if value is None: super().__set__(instance, value) - super().__set__(instance, chk_integer(self.name, value, min_value=self._min_value, max_value=self._max_value)) + super().__set__(instance, self.validate(value)) - def serialize(self, value : int) -> str: - return str(value) + def validate(self, value): + value = super().validate(value) + return None if value is None else chk_integer( + self.name, value, min_value=self._min_value, max_value=self._max_value) + + def serialize(self, value : bool) -> str: + value = self.validate(value) + return None if value is None else str(value) def deserialize(self, value : str) -> int: return int(value) diff --git a/src/common/orm/fields/PrimaryKeyField.py b/src/common/orm/fields/PrimaryKeyField.py index 2111c1b65e9af6f7899a0ee9254ce103d3bb3882..d159071b6cdbf53700da8ff191ba216129c044f1 100644 --- a/src/common/orm/fields/PrimaryKeyField.py +++ b/src/common/orm/fields/PrimaryKeyField.py @@ -1,6 +1,5 @@ from __future__ import annotations from typing import TYPE_CHECKING -from common.type_checkers.Checkers import chk_not_none from .StringField import StringField if TYPE_CHECKING: @@ -11,7 +10,6 @@ class PrimaryKeyField(StringField): super().__init__(*args, required=True, allow_empty=False, min_length=1, **kwargs) def __set__(self, instance : 'Model', value : str) -> None: - chk_not_none(self.name, value) # Always required if (self.name in instance.__dict__) and (instance.__dict__[self.name] is not None): raise ValueError('PrimaryKeyField cannot be modified') super().__set__(instance, value) diff --git a/src/common/orm/fields/StringField.py b/src/common/orm/fields/StringField.py index 8a6e260a116aab29c140ff377b7a033248c4dcaa..898a7d5262a0fa9c8b36853806b2237908d819fb 100644 --- a/src/common/orm/fields/StringField.py +++ b/src/common/orm/fields/StringField.py @@ -1,7 +1,7 @@ from __future__ import annotations import re from typing import TYPE_CHECKING, Optional, Pattern, Union -from common.type_checkers.Checkers import chk_boolean, chk_integer, chk_not_none, chk_string +from common.type_checkers.Checkers import chk_boolean, chk_integer, chk_string from .Field import Field if TYPE_CHECKING: @@ -21,14 +21,17 @@ class StringField(Field): self._pattern = None if pattern is None else re.compile(pattern) def __set__(self, instance : 'Model', value : str) -> None: - if self.required: chk_not_none(self.name, value) - if value is None: super().__set__(instance, value) - super().__set__(instance, chk_string( + super().__set__(instance, self.validate(value)) + + def validate(self, value): + value = super().validate(value) + return None if value is None else chk_string( self.name, value, allow_empty=self._allow_empty, min_length=self._min_length, max_length=self._max_length, - pattern=self._pattern)) + pattern=self._pattern) - def serialize(self, value : str) -> str: - return value + def serialize(self, value : bool) -> str: + value = self.validate(value) + return None if value is None else str(value) def deserialize(self, value : str) -> str: return value diff --git a/src/common/orm/model/Model.py b/src/common/orm/model/Model.py index b4950d163208aec8981be13d56c3c6011d47c7c5..3666cffdf7ba79ed3b1ab511d134b9bc8305bbd6 100644 --- a/src/common/orm/model/Model.py +++ b/src/common/orm/model/Model.py @@ -1,73 +1,75 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple, Type +import logging, re +from typing import Any, Dict, List, Mapping, Optional, Set, Tuple +from common.orm.Database import Database from common.orm.backend.Tools import key_to_str from common.orm.fields.ForeignKeyField import ForeignKeyField -from common.type_checkers.Checkers import chk_none -from ..Exceptions import MutexException -from ..backend._Backend import _Backend +from ..Exceptions import ConstraintException, MutexException from ..fields.Field import Field from ..fields.PrimaryKeyField import PrimaryKeyField from .Tools import NoDupOrderedDict +LOGGER = logging.getLogger(__name__) +DEFAULT_PRIMARY_KEY_NAME = 'pk_auto' + class MetaModel(type): @classmethod - def __prepare__(metacls, name : str, bases : Tuple[type, ...], **attrs : Any) -> Mapping[str, Any]: + def __prepare__(cls, name : str, bases : Tuple[type, ...], **attrs : Any) -> Mapping[str, Any]: return NoDupOrderedDict() - def __new__(metacls, name : str, bases : Tuple[type, ...], attrs : NoDupOrderedDict[str, Any]): + def __new__(cls, name : str, bases : Tuple[type, ...], attrs : NoDupOrderedDict[str, Any]): field_names = list() - primary_key_field = None + pk_field_name = None for key, value in attrs.items(): if not isinstance(value, Field): continue - attrs[key].name = key + value.name = key field_names.append(key) if not isinstance(value, PrimaryKeyField): continue - if primary_key_field is None: - primary_key_field = value + if pk_field_name is None: + pk_field_name = key continue - raise AttributeError('PrimaryKey for Model({:s}) already set to attribute({:s})'.format( - str(name), str(primary_key_field.name))) - cls_obj = super().__new__(metacls, name, bases, dict(attrs)) - setattr(cls_obj, '_primary_key_field', primary_key_field) + raise AttributeError('PrimaryKeyField for Model({:s}) already set to attribute({:s})'.format( + str(name), str(pk_field_name))) + if pk_field_name is None: + if DEFAULT_PRIMARY_KEY_NAME in attrs.keys(): + msg = 'PrimaryKeyField for Model({:s}) not defined and attribute "{:s}" already used. '\ + 'Leave attribute name "{:s}" for automatic PrimaryKeyField, or set a PrimaryKeyField.' + raise AttributeError(msg.format(str(name), DEFAULT_PRIMARY_KEY_NAME, DEFAULT_PRIMARY_KEY_NAME)) + pk_field_name = DEFAULT_PRIMARY_KEY_NAME + attrs[pk_field_name] = PrimaryKeyField(name=pk_field_name) + field_names.append(pk_field_name) + cls_obj = super().__new__(cls, name, bases, dict(attrs)) + setattr(cls_obj, '_pk_field_name', pk_field_name) setattr(cls_obj, '_field_names_list', field_names) setattr(cls_obj, '_field_names_set', set(field_names)) return cls_obj class Model(metaclass=MetaModel): - def __init__(self, parent : 'Model', primary_key : Any = None) -> None: - if not isinstance(parent, Model): - str_class_path = '{}.{}'.format(Model.__module__, Model.__name__) - raise AttributeError('parent must inherit from {}'.format(str_class_path)) - self._parent = parent - self._backend = self._parent.backend - self._class_name = type(self).__name__ - backend_key = self._class_name - if self._primary_key_field is not None: # pylint: disable=no-member - primary_key_field_name = self._primary_key_field.name # pylint: disable=no-member - setattr(self, primary_key_field_name, primary_key) - backend_key += '[{:s}]'.format(getattr(self, primary_key_field_name)) - else: - try: - chk_none('primary_key', primary_key) - except: - msg = 'Unable to set primary_key({:s}) since no PrimaryKeyField is defined in the model' - raise AttributeError(msg.format(str(primary_key))) - self._backend_key : str = key_to_str([self.parent.backend_key, backend_key]) - self._backend_parent_key : str = self.parent.backend_key - self._backend_references_key : str = key_to_str([self.parent.backend_key, backend_key, 'references']) + def __init__(self, database : Database, primary_key : str, auto_load : bool = True) -> None: + if not isinstance(database, Database): + str_class_path = '{}.{}'.format(Database.__module__, Database.__name__) + raise AttributeError('database must inherit from {}'.format(str_class_path)) + self._model_class = type(self) + self._class_name = self._model_class.__name__ + pk_field_name = self._pk_field_name # pylint: disable=no-member + pk_field_instance : 'PrimaryKeyField' = getattr(self._model_class, pk_field_name) + primary_key = pk_field_instance.validate(primary_key) + if primary_key.startswith(self._class_name): + match = re.match(r'^{:s}\[([^\]]*)\]'.format(self._class_name), primary_key) + if match: primary_key = match.group(1) + setattr(self, pk_field_name, primary_key) + self._database = database + self._backend = database.backend + self._instance_key : str = '{:s}[{:s}]'.format(self._class_name, primary_key) + self._references_key : str = key_to_str([self._instance_key, 'references']) self._owner_key : Optional[str] = None + if auto_load: self.load() @property - def parent(self) -> 'Model': return self._parent - - @property - def backend(self) -> _Backend: return self._parent.backend - - @property - def backend_key(self) -> str: return self._backend_key + def instance_key(self) -> str: return self._instance_key def lock(self, extra_keys : List[List[str]] = []): - lock_keys = [self._backend_key, self.parent.backend_key, self._backend_references_key] + extra_keys + lock_keys = [self._instance_key, self._references_key] + extra_keys lock_keys = [key_to_str([lock_key, 'lock']) for lock_key in lock_keys] acquired,self._owner_key = self._backend.lock(lock_keys, owner_key=self._owner_key) if acquired: return @@ -75,43 +77,49 @@ class Model(metaclass=MetaModel): str(lock_keys), str(self._owner_key))) def unlock(self, extra_keys : List[List[str]] = []): - lock_keys = [self._backend_key, self.parent.backend_key, self._backend_references_key] + extra_keys + lock_keys = [self._instance_key, self._references_key] + extra_keys lock_keys = [key_to_str([lock_key, 'lock']) for lock_key in lock_keys] released = self._backend.unlock(lock_keys, self._owner_key) if released: return - raise Exception('Unable to unlock keys {:s} using owner_key {:s}'.format( + raise MutexException('Unable to unlock keys {:s} using owner_key {:s}'.format( str(lock_keys), str(self._owner_key))) def load(self) -> None: - self.lock() - - model_class = type(self) - attributes = self._backend.dict_get(self._backend_key) - pk_field = self._primary_key_field # pylint: disable=no-member - pk_field_name = None if pk_field is None else pk_field.name - for field_name in self._field_names_list: # pylint: disable=no-member - if field_name == pk_field_name: continue - if field_name not in attributes: continue - raw_field_value = attributes[field_name] - field_class : 'Field' = getattr(model_class, field_name) - field_value = field_class.deserialize(raw_field_value) - setattr(self, field_name, field_value) + pk_field_name = self._pk_field_name # pylint: disable=no-member - self.unlock() + try: + self.lock() + + attributes = self._backend.dict_get(self._instance_key) + if attributes is None: return + for field_name in self._field_names_list: # pylint: disable=no-member + if field_name == pk_field_name: continue + if field_name not in attributes: continue + raw_field_value = attributes[field_name] + field_instance : 'Field' = getattr(self._model_class, field_name) + field_value = field_instance.deserialize(raw_field_value) + if isinstance(field_instance, ForeignKeyField): + setattr(self, field_name + '_stored', field_value) + field_value = field_instance.foreign_model(self._database, field_value, auto_load=True) + setattr(self, field_name, field_value) + finally: + self.unlock() def save(self) -> None: - model_class = type(self) - attributes : Dict[str, Any] = dict() required_keys : Set[str] = set() foreign_additions : Dict[str, str] = dict() foreign_removals : Dict[str, str] = dict() for field_name in self._field_names_list: # pylint: disable=no-member - field_value = str(getattr(self, field_name)) - field_instance : 'Field' = getattr(model_class, field_name) + field_value = getattr(self, field_name) + field_instance : 'Field' = getattr(self._model_class, field_name) serialized_field_value = field_instance.serialize(field_value) + if serialized_field_value is None: + if not field_instance.required: continue + msg = 'Attribute({}) is required.' + raise AttributeError(msg.format(str(field_instance.name))) if isinstance(field_instance, ForeignKeyField): - foreign_reference = '{:s}:{:s}'.format(self._backend_key, field_name) + foreign_reference = '{:s}:{:s}'.format(self._instance_key, field_name) field_value_stored = getattr(self, field_name + '_stored', None) if field_value_stored is not None: foreign_removals[key_to_str([field_value_stored, 'references'])] = foreign_reference @@ -119,32 +127,21 @@ class Model(metaclass=MetaModel): required_keys.add(serialized_field_value) attributes[field_name] = serialized_field_value - if len(self._backend_parent_key) > 0: - required_keys.add(self._backend_parent_key) - extra_keys = [] extra_keys.extend(list(foreign_removals.keys())) extra_keys.extend(list(foreign_additions.keys())) - print('self._backend_key', self._backend_key) - print('extra_keys', extra_keys) - print('attributes', attributes) - print('foreign_removals', foreign_removals) - print('foreign_additions', foreign_additions) - try: - print('dump_before', self._backend.dump()) self.lock(extra_keys=extra_keys) - print('dump_after', self._backend.dump()) not_exists = [] for required_key in required_keys: if self._backend.exists(required_key): continue not_exists.append('{:s}'.format(str(required_key))) if len(not_exists) > 0: - raise ValueError('Required Keys ({:s}) does not exist'.format(', '.join(sorted(not_exists)))) + raise ConstraintException('Required Keys ({:s}) does not exist'.format(', '.join(sorted(not_exists)))) - self._backend.dict_update(self._backend_key, attributes) + self._backend.dict_update(self._instance_key, attributes) for serialized_field_value,foreign_reference in foreign_removals.items(): self._backend.set_remove(serialized_field_value, foreign_reference) @@ -157,9 +154,30 @@ class Model(metaclass=MetaModel): setattr(self, (foreign_reference.split(':')[-1]) + '_stored', field_value_stored) def delete(self) -> None: - self.lock() - self._backend.delete(self._backend_key) - self.unlock() + foreign_removals : Dict[str, str] = {} + for field_name in self._field_names_list: # pylint: disable=no-member + field_instance : 'Field' = getattr(self._model_class, field_name) + if not isinstance(field_instance, ForeignKeyField): continue + foreign_reference = '{:s}:{:s}'.format(self._instance_key, field_name) + field_value_stored = getattr(self, field_name + '_stored', None) + if field_value_stored is None: continue + foreign_removals[key_to_str([field_value_stored, 'references'])] = foreign_reference + + extra_keys = [] + extra_keys.extend(list(foreign_removals.keys())) + + try: + self.lock(extra_keys=extra_keys) + + if self._backend.exists(self._references_key): + references = self._backend.set_get_all(self._references_key) + raise ConstraintException('Instance is used by Keys ({:s})'.format(', '.join(sorted(references)))) + + self._backend.delete(self._instance_key) + for serialized_field_value,foreign_reference in foreign_removals.items(): + self._backend.set_remove(serialized_field_value, foreign_reference) + finally: + self.unlock(extra_keys=extra_keys) def dump_id(self) -> Dict: raise NotImplementedError() @@ -168,8 +186,7 @@ class Model(metaclass=MetaModel): raise NotImplementedError() def __repr__(self) -> str: - pk_field = self._primary_key_field # pylint: disable=no-member - pk_field_name = None if pk_field is None else pk_field.name # pylint: disable=no-member + pk_field_name = self._pk_field_name # pylint: disable=no-member arguments = ', '.join( '{:s}={:s}{:s}'.format( name, repr(getattr(self, name)), '(PK)' if name == pk_field_name else '') diff --git a/src/common/orm/tests/test_unitary_orm.py b/src/common/orm/tests/test_unitary_orm.py index 7ae5a1a79363aab2728a790cc35e32cbbcf23355..a03a1051206e1062e1c8f27d665e107bc968fdf8 100644 --- a/src/common/orm/tests/test_unitary_orm.py +++ b/src/common/orm/tests/test_unitary_orm.py @@ -1,43 +1,64 @@ import logging, pytest -from common.orm._Database import _Database +from common.orm.Exceptions import ConstraintException +from common.orm.Database import Database from common.orm.backend._Backend import _Backend -from common.orm.backend.inmemory.InMemoryBackend import LOGGER, InMemoryBackend +from common.orm.backend.inmemory.InMemoryBackend import InMemoryBackend from common.orm.fields.BooleanField import BooleanField from common.orm.fields.FloatField import FloatField from common.orm.fields.ForeignKeyField import ForeignKeyField from common.orm.fields.IntegerField import IntegerField from common.orm.fields.PrimaryKeyField import PrimaryKeyField from common.orm.fields.StringField import StringField -from common.orm.model.Model import Model +from common.orm.model.Model import DEFAULT_PRIMARY_KEY_NAME, Model logging.basicConfig(level=logging.INFO) +LOGGER = logging.getLogger(__name__) def test_database_instantiation(): with pytest.raises(AttributeError) as e: - _Database(None) + Database(None) str_class_path = '{}.{}'.format(_Backend.__module__, _Backend.__name__) assert str(e.value) == 'backend must inherit from {}'.format(str_class_path) - assert _Database(InMemoryBackend()) is not None + assert Database(InMemoryBackend()) is not None def test_model_without_attributes(): with pytest.raises(AttributeError) as e: Model(None, 'valid-uuid') - str_class_path = '{}.{}'.format(Model.__module__, Model.__name__) - assert str(e.value) == 'parent must inherit from {}'.format(str_class_path) + str_class_path = '{}.{}'.format(Database.__module__, Database.__name__) + assert str(e.value) == 'database must inherit from {}'.format(str_class_path) - database = _Database(InMemoryBackend()) + database = Database(InMemoryBackend()) - with pytest.raises(AttributeError) as e: + with pytest.raises(ValueError) as e: Model(database, '') - assert str(e.value) == 'Unable to set primary_key() since no PrimaryKeyField is defined in the model' + msg = '{:s}() is out of range: allow_empty(False).' + assert str(e.value) == msg.format(DEFAULT_PRIMARY_KEY_NAME) - with pytest.raises(AttributeError) as e: - Model(database, 'primary-key') - assert str(e.value) == 'Unable to set primary_key(primary-key) since no PrimaryKeyField is defined in the model' + with pytest.raises(TypeError) as e: + Model(database, 23) + msg = '{:s}(23) is of a wrong type(int). Accepted type_or_types(<class \'str\'>).' + assert str(e.value) == msg.format(DEFAULT_PRIMARY_KEY_NAME) + + with pytest.raises(TypeError) as e: + Model(database, 23.5) + msg = '{:s}(23.5) is of a wrong type(float). Accepted type_or_types(<class \'str\'>).' + assert str(e.value) == msg.format(DEFAULT_PRIMARY_KEY_NAME) + + with pytest.raises(TypeError) as e: + Model(database, True) + msg = '{:s}(True) is of a wrong type(bool). Accepted type_or_types(<class \'str\'>).' + assert str(e.value) == msg.format(DEFAULT_PRIMARY_KEY_NAME) + + with pytest.raises(TypeError) as e: + Model(database, ['a']) + msg = '{:s}([\'a\']) is of a wrong type(list). Accepted type_or_types(<class \'str\'>).' + assert str(e.value) == msg.format(DEFAULT_PRIMARY_KEY_NAME) + + Model(database, 'valid-primary-key') def test_model_with_primarykey(): - database = _Database(InMemoryBackend()) + database = Database(InMemoryBackend()) with pytest.raises(AttributeError) as e: class WrongTestModel(Model): # pylint: disable=unused-variable @@ -47,7 +68,7 @@ def test_model_with_primarykey(): salary = FloatField(min_value=0.0) active = BooleanField() pk2 = PrimaryKeyField() - assert str(e.value) == 'PrimaryKey for Model(WrongTestModel) already set to attribute(pk)' + assert str(e.value) == 'PrimaryKeyField for Model(WrongTestModel) already set to attribute(pk)' class TestModel(Model): pk = PrimaryKeyField() @@ -57,8 +78,8 @@ def test_model_with_primarykey(): active = BooleanField() with pytest.raises(ValueError) as e: - TestModel(database) - assert str(e.value) == 'pk(None) is None.' + TestModel(database, None) + assert str(e.value) == 'pk(None) is required. It cannot be None.' with pytest.raises(ValueError) as e: TestModel(database, '') @@ -72,7 +93,7 @@ def test_model_with_primarykey(): assert str(e.value) == 'PrimaryKeyField cannot be modified' def test_model_with_primarykey_and_attributes(): - database = _Database(InMemoryBackend()) + database = Database(InMemoryBackend()) class TestModel(Model): pk = PrimaryKeyField() @@ -144,7 +165,7 @@ def test_model_with_primarykey_and_attributes(): assert repr(obj) == "TestModel(pk='valid-pk'(PK), name='John Smith', age=37, salary=5023.52, active=True)" def test_model_database_operations(): - database = _Database(InMemoryBackend()) + database = Database(InMemoryBackend()) class TestModel(Model): pk = PrimaryKeyField() @@ -166,8 +187,8 @@ def test_model_database_operations(): database_dump = database.dump() assert len(database_dump) == 1 - db_entry_repr = '[dict] /TestModel[valid-pk]'\ - ' '\ + db_entry_repr = '[dict] TestModel[valid-pk]'\ + ' '\ ':: {\'pk\': \'valid-pk\', \'name\': \'John Smith\', \'age\': \'37\', \'salary\': \'5023.52\', '\ '\'active\': \'True\'}' assert database_dump[0] == db_entry_repr @@ -187,21 +208,22 @@ def test_model_database_operations(): database.clear_all() assert len(database.dump()) == 0 -def test_model_with_foreignkeys(): - database = _Database(InMemoryBackend()) +def test_model_foreignkeys(): + database = Database(InMemoryBackend()) class Team(Model): pk = PrimaryKeyField() - name = StringField(max_length=10) + name = StringField(max_length=10, required=True) class Workplace(Model): pk = PrimaryKeyField() - name = StringField(max_length=10) + name = StringField(max_length=10, required=True) class Member(Model): pk = PrimaryKeyField() - place = ForeignKeyField(Workplace) - name = StringField(max_length=10) + team = ForeignKeyField(Team) + place = ForeignKeyField(Workplace, required=False) + name = StringField(max_length=10, required=True) team_dev_ops = Team(database, 'dev-ops') team_dev_ops.name = 'Dev Ops' @@ -213,35 +235,200 @@ def test_model_with_foreignkeys(): assert workplace_bcn is not None assert repr(workplace_bcn) == "Workplace(pk='bcn'(PK), name='Barcelona')" - member_john = Member(team_dev_ops, 'john') + member_john = Member(database, 'john') member_john.name = 'John' + member_john.team = team_dev_ops member_john.place = workplace_bcn assert member_john is not None - assert repr(member_john) == "Member(pk='john'(PK), place='/Workplace[bcn]', name='John')" + assert repr(member_john) == "Member(pk='john'(PK), team='Team[dev-ops]', place='Workplace[bcn]', name='John')" - with pytest.raises(ValueError) as e: + database_dump = database.dump() + LOGGER.info('----- Database Dump: -----') + for database_entry in database_dump: + LOGGER.info(' ' + database_entry) + LOGGER.info('--------------------------') + + with pytest.raises(ConstraintException) as e: member_john.save() - assert str(e.value) == 'Required Keys (/Team[dev-ops], /Workplace[bcn]) does not exist' + assert str(e.value) == 'Required Keys (Team[dev-ops], Workplace[bcn]) does not exist' workplace_bcn.save() + assert repr(Workplace(database, workplace_bcn.pk)) == "Workplace(pk='bcn'(PK), name='Barcelona')" - with pytest.raises(ValueError) as e: + with pytest.raises(ConstraintException) as e: member_john.save() - assert str(e.value) == 'Required Keys (/Team[dev-ops]) does not exist' + assert str(e.value) == 'Required Keys (Team[dev-ops]) does not exist' team_dev_ops.save() + assert repr(Team(database, team_dev_ops.pk)) == "Team(pk='dev-ops'(PK), name='Dev Ops')" + member_john.save() + str_member = "Member(pk='john'(PK), team='Team[dev-ops]', place='Workplace[bcn]', name='John')" + assert repr(Member(database, member_john.pk)) == str_member + + with pytest.raises(ConstraintException) as e: + workplace_bcn.delete() + assert str(e.value) == 'Instance is used by Keys (Member[john]:place)' + + with pytest.raises(ConstraintException) as e: + team_dev_ops.delete() + assert str(e.value) == 'Instance is used by Keys (Member[john]:team)' + + workplace_mad = Workplace(database, 'mad') + workplace_mad.name = 'Madrid' + assert workplace_mad is not None + assert repr(workplace_mad) == "Workplace(pk='mad'(PK), name='Madrid')" + + member_john = Member(database, 'john') + member_john.name = 'John' + member_john.place = workplace_mad + assert member_john is not None + assert repr(member_john) == "Member(pk='john'(PK), team='Team[dev-ops]', place='Workplace[mad]', name='John')" + + member_tom = Member(database, 'tom') + member_tom.name = 'Tom' + member_tom.place = workplace_mad + assert member_tom is not None + assert repr(member_tom) == "Member(pk='tom'(PK), team=None, place='Workplace[mad]', name='Tom')" + + with pytest.raises(ConstraintException) as e: + member_tom.save() + assert str(e.value) == 'ForeignKey(team, None) is required. It cannot be serialized as empty.' + + member_tom.team = team_dev_ops + + with pytest.raises(ConstraintException) as e: + member_john.save() + assert str(e.value) == 'Required Keys (Workplace[mad]) does not exist' + + workplace_mad.save() + assert repr(Workplace(database, workplace_mad.pk)) == "Workplace(pk='mad'(PK), name='Madrid')" + + member_tom.save() + str_member = "Member(pk='tom'(PK), team='Team[dev-ops]', place='Workplace[mad]', name='Tom')" + assert repr(Member(database, member_tom.pk)) == str_member + + member_john = Member(database, 'john') + + with pytest.raises(ConstraintException) as e: + del member_john.place + del member_john.team + assert str(e.value) == 'ForeignKey(team) is required. Unable to clear it.' + + member_brad = Member(database, 'brad') + assert member_brad is not None + assert repr(member_brad) == "Member(pk='brad'(PK), team=None, place=None, name=None)" + + with pytest.raises(ConstraintException) as e: + member_brad.save() + assert str(e.value) == 'ForeignKey(team, None) is required. It cannot be serialized as empty.' + + member_brad.team = team_dev_ops + + with pytest.raises(ValueError) as e: + member_brad.save() + assert str(e.value) == 'name(None) is required. It cannot be None.' + + member_brad.name = 'Brad' + assert repr(member_brad) == "Member(pk='brad'(PK), team=\'Team[dev-ops]\', place=None, name='Brad')" + + member_brad.save() + str_member = "Member(pk='brad'(PK), team='Team[dev-ops]', place=None, name='Brad')" + assert repr(Member(database, member_brad.pk)) == str_member + + team_admin = Team(database, 'admin') + team_admin.name = 'Admin' + team_admin.save() + assert repr(Team(database, team_admin.pk)) == "Team(pk='admin'(PK), name='Admin')" + + member_brad = Member(database, member_brad.pk) + str_member = "Member(pk='brad'(PK), team='Team[dev-ops]', place=None, name='Brad')" + assert repr(member_brad) == str_member + member_brad.team = team_admin + str_member = "Member(pk='brad'(PK), team='Team[admin]', place=None, name='Brad')" + assert repr(member_brad) == str_member + member_brad.save() + str_member = "Member(pk='brad'(PK), team='Team[admin]', place=None, name='Brad')" + assert repr(Member(database, member_brad.pk)) == str_member database_dump = database.dump() LOGGER.info('----- Database Dump: -----') for database_entry in database_dump: LOGGER.info(' ' + database_entry) LOGGER.info('--------------------------') - #assert len(database_dump) == 1 - raise Exception() + assert len(database_dump) == 11 + spaces = ' ' + db_entry_repr_00 = "[dict] Member[brad] " + spaces + \ + ":: {'pk': 'brad', 'team': 'Team[admin]', 'name': 'Brad'}" + db_entry_repr_01 = "[dict] Member[john] " + spaces + \ + ":: {'pk': 'john', 'team': 'Team[dev-ops]', 'place': 'Workplace[bcn]', 'name': 'John'}" + db_entry_repr_02 = "[dict] Member[tom] " + spaces + \ + ":: {'pk': 'tom', 'team': 'Team[dev-ops]', 'place': 'Workplace[mad]', 'name': 'Tom'}" + db_entry_repr_03 = "[dict] Team[admin] " + spaces + \ + ":: {'pk': 'admin', 'name': 'Admin'}" + db_entry_repr_04 = "[ set] Team[admin]/references " + spaces + \ + ":: {'Member[brad]:team'}" + db_entry_repr_05 = "[dict] Team[dev-ops] " + spaces + \ + ":: {'pk': 'dev-ops', 'name': 'Dev Ops'}" + db_entry_repr_06a = "[ set] Team[dev-ops]/references " + spaces + \ + ":: {'Member[john]:team', 'Member[tom]:team'}" + db_entry_repr_06b = "[ set] Team[dev-ops]/references " + spaces + \ + ":: {'Member[tom]:team', 'Member[john]:team'}" + db_entry_repr_07 = "[dict] Workplace[bcn] " + spaces + \ + ":: {'pk': 'bcn', 'name': 'Barcelona'}" + db_entry_repr_08 = "[ set] Workplace[bcn]/references" + spaces + \ + ":: {'Member[john]:place'}" + db_entry_repr_09 = "[dict] Workplace[mad] " + spaces + \ + ":: {'pk': 'mad', 'name': 'Madrid'}" + db_entry_repr_10 = "[ set] Workplace[mad]/references" + spaces + \ + ":: {'Member[tom]:place'}" + assert database_dump[ 0] == db_entry_repr_00 + assert database_dump[ 1] == db_entry_repr_01 + assert database_dump[ 2] == db_entry_repr_02 + assert database_dump[ 3] == db_entry_repr_03 + assert database_dump[ 4] == db_entry_repr_04 + assert database_dump[ 5] == db_entry_repr_05 + assert (database_dump[ 6] == db_entry_repr_06a) or (database_dump[ 6] == db_entry_repr_06b) + assert database_dump[ 7] == db_entry_repr_07 + assert database_dump[ 8] == db_entry_repr_08 + assert database_dump[ 9] == db_entry_repr_09 + assert database_dump[10] == db_entry_repr_10 + + Member(database, member_tom.pk).delete() + + database_dump = database.dump() + LOGGER.info('----- Database Dump: -----') + for database_entry in database_dump: + LOGGER.info(' ' + database_entry) + LOGGER.info('--------------------------') -# TODO: Test remove foreign key -# TODO: Test change foreign key -# TODO: Test remove entity with foreign keys pointing to another entity -# TODO: Test remove entity with primary key being pointed by another entity + assert len(database_dump) == 9 + spaces = ' ' + db_entry_repr_00 = "[dict] Member[brad] " + spaces + \ + ":: {'pk': 'brad', 'team': 'Team[admin]', 'name': 'Brad'}" + db_entry_repr_01 = "[dict] Member[john] " + spaces + \ + ":: {'pk': 'john', 'team': 'Team[dev-ops]', 'place': 'Workplace[bcn]', 'name': 'John'}" + db_entry_repr_02 = "[dict] Team[admin] " + spaces + \ + ":: {'pk': 'admin', 'name': 'Admin'}" + db_entry_repr_03 = "[ set] Team[admin]/references " + spaces + \ + ":: {'Member[brad]:team'}" + db_entry_repr_04 = "[dict] Team[dev-ops] " + spaces + \ + ":: {'pk': 'dev-ops', 'name': 'Dev Ops'}" + db_entry_repr_05 = "[ set] Team[dev-ops]/references " + spaces + \ + ":: {'Member[john]:team'}" + db_entry_repr_06 = "[dict] Workplace[bcn] " + spaces + \ + ":: {'pk': 'bcn', 'name': 'Barcelona'}" + db_entry_repr_07 = "[ set] Workplace[bcn]/references" + spaces + \ + ":: {'Member[john]:place'}" + db_entry_repr_08 = "[dict] Workplace[mad] " + spaces + \ + ":: {'pk': 'mad', 'name': 'Madrid'}" + assert database_dump[ 0] == db_entry_repr_00 + assert database_dump[ 1] == db_entry_repr_01 + assert database_dump[ 2] == db_entry_repr_02 + assert database_dump[ 3] == db_entry_repr_03 + assert database_dump[ 4] == db_entry_repr_04 + assert database_dump[ 5] == db_entry_repr_05 + assert database_dump[ 6] == db_entry_repr_06 + assert database_dump[ 7] == db_entry_repr_07 + assert database_dump[ 8] == db_entry_repr_08 diff --git a/src/common/type_checkers/Checkers.py b/src/common/type_checkers/Checkers.py index b3693d87d29dc2a9c34343bfb8eb1cc69d1c2381..8c6231aaaca758b8cacea039878cac5fb2b93205 100644 --- a/src/common/type_checkers/Checkers.py +++ b/src/common/type_checkers/Checkers.py @@ -1,15 +1,15 @@ import re from typing import Any, Container, List, Optional, Pattern, Set, Sized, Tuple, Union -def chk_none(name : str, value : Any) -> Any: +def chk_none(name : str, value : Any, reason=None) -> Any: if value is None: return value - msg = '{}({}) is not None.' - raise ValueError(msg.format(str(name), str(value))) + if reason is None: reason = 'must be None.' + raise ValueError('{}({}) {}'.format(str(name), str(value), str(reason))) -def chk_not_none(name : str, value : Any) -> Any: +def chk_not_none(name : str, value : Any, reason=None) -> Any: if value is not None: return value - msg = '{}({}) is None.' - raise ValueError(msg.format(str(name), str(value))) + if reason is None: reason = 'must not be None.' + raise ValueError('{}({}) {}'.format(str(name), str(value), str(reason))) def chk_type(name : str, value : Any, type_or_types : Union[type, Set[type]] = set()) -> Any: if isinstance(value, type_or_types): return value