Skip to content

13. The generic classes [BaseEntity] and [MyException]

We will now define two classes that we will use regularly going forward.

Image

13.1. The MyException class

The [MyException] (MyException.py) class provides a custom exception class:


# a proprietary exception class derived from [BaseException]
class MyException(BaseException):
    # manufacturer
    def __init__(self: object, code: int, message: str):
        # parent
        BaseException.__init__(self, message)
        # error code
        self.code = code
 
    # toString
    def __str__(self):
        return f"MyException[{self.code}, {super().__str__()}]"
 
    # getter
    @property
    def code(self) -> int:
        return self.__code
 
    # setter
    @code.setter
    def code(self, code: int):
        # the error code must be a positive integer
        if isinstance(code, int) and code > 0:
            self.__code = code
        else:
            # exception
            raise BaseException(f"code erreur {code} incorrect")

Notes

  • line 2: the class [MyException] derives from the predefined class [BaseException];
  • line 4: the constructor accepts two parameters:
    • [code]: an integer error code;
    • [message]: an error message;
  • line 6: the error message is passed to the parent class;
  • lines 14–27: the attribute [code] is manipulated via a getter/setter;
  • lines 23–24: the validity of the [code] attribute is checked: it must be an integer > 0;

13.2. The [BaseEntity] class

The [BaseEntity] class will be the parent class of most of the classes we will create to encapsulate information about an object. Going forward, we will primarily use two types of classes:

  • classes whose sole purpose is to encapsulate information about a single object in one place. These will have no behaviors (methods) other than getters/setters and a display function (__str__). If there are N objects to manage, these classes are instantiated N times. [BaseEntity] will be the parent class of this type of class;
  • classes whose primary role is to encapsulate methods and very little information. These classes will be instantiated only once (singleton). Their role is to implement an application’s algorithms;

The [BaseEntity] class is as follows:


# imports
import json
import re
 
from MyException import MyException
 
 
class BaseEntity(object):
    # properties excluded from class state
    excluded_keys = []
 
    # class properties
    @staticmethod
    def get_allowed_keys() -> list:
        # id: object identifier
        return ["id"]
 
    # toString
    def __str__(self) -> str:
        return self.asjson()
 
    # getter
    @property
    def id(self) -> int:
        return self.__id
 
    #  setter
    @id.setter
    def id(self, id):
        # the id must be an integer >=0
        try:
            id = int(id)
            erreur = id < 0
        except:
            erreur = True
        # mistake?
        if erreur:
            raise MyException(1, f"L'identifiant d'une entité {self.__class__} doit être un entier >=0")
        else:
            self.__id = id
 
    def fromdict(self, state: dict, silent=False):
        
 
    def set_value(self, key: str, value, new_attributes) -> dict:
        
 
    def asdict(self, included_keys: list = None, excluded_keys: list = []) -> dict:
        
 
    def asjson(self, excluded_keys: list = []) -> str:
        
 
    def fromjson(self, json_state: str):
        

Comments

  • The purpose of the [BaseEntity] class is to facilitate Object/Dictionary and Object/jSON conversions. The following methods are provided:
    • [asdict]: returns the dictionary of the object’s properties;
    • [fromdict]: creates an object from a dictionary;
    • [asjson]: returns the jSON string of the object, as does the [__str__] function;
    • [fromjson]: constructs an object from its string jSON;
  • The class [BaseEntity] is intended to be derived from and not used as-is;
  • lines 22–25: the class [BaseEntity] has only one property, the integer [id]. This property is the object’s identifier. In practice, it is often useful to be able to distinguish between instances of the same class. We will do this using this property, which is unique to each instance. Furthermore, objects often come from databases where they are identified by a primary key, typically an integer. In such cases, [id] will be the primary key;
  • lines 27–40: the setter for the [id] property. We verify that it is an integer >= 0. If this is not the case, a [MyException] exception is thrown (line 39);
  • line 10: [excluded_keys] is a class attribute, not an instance attribute. Therefore, we will write [BaseEntity.excluded_keys]. This class attribute is a list containing the class properties that do not participate in the Object/Dictionary and Object/jSON conversions;
  • Lines 12–16: [get_allowed_keys] returns the list of class properties. In a Dictionary → Object or jSON → Object conversion, only keys present in this list will be accepted. Each class deriving from class [BaseEntity] will need to redefine this list;

It is important to understand here that the properties and functions of the [BaseEntity] class are accessible to classes derived from [BaseEntity]. This is the key point to grasp.

We will now examine the code for the [BaseEntity] class in detail. It is quite advanced. Beginner readers may simply read the description of each function’s role without delving into the code itself.

13.2.1. The [BaseEntity.fromdict] method

13.2.1.1. Definition

The [fromdict] method allows you to initialize a [BaseEntity] object or a derived object from a dictionary:


def fromdict(self, state: dict, silent=False):
        # object is updated
        # authorized keys
        allowed_keys = self.__class__.get_allowed_keys()
        # state key traversal
        for key, value in state.items():
            # is the key authorized?
            if key not in allowed_keys:
                if not silent:
                    raise MyException(2, f"la clé {key} n'est pas autorisée")
            else:
                # we try to assign the value to the key
                # we let any exception go up
                setattr(self, key, value)
        # we return the object
        return self

Comments

  • line 1: the function receives the dictionary [state] as a parameter, from which the current object will be initialized;
  • line 4: we call the static function [get_allowed_keys] of the class that called the function [fromdict]. If we are dealing with a class derived from [BaseEntity] and that derived class has redefined the static function [get_allowed_keys], then the function [get_allowed_keys] is called. Each derived class redefines this static function to declare its properties;
  • line 6: the keys and values of the [state] dictionary are iterated over;
  • line 8: if the key [key] is not among the class’s properties, then either:
    • it is ignored;
    • an exception is thrown (line 10). The developer specifies their intent by passing the correct parameter [silent] (line 1). The default value of [silent] causes an exception to be thrown if an attempt is made to initialize the object with a property it does not have;
  • line 14: if the key is among the object’s properties, then it is assigned to the object [self] using the predefined function [setattr];
  • line 16: the function returns the initialized object;

13.2.1.2. Examples

Image

13.2.1.2.1. The class [Utils]

The class [Utils] (Utils.py) is as follows:


class Utils:
    # static method
    @staticmethod
    def is_string_ok(string: str) -> bool:
        # is string a string?
        erreur = not isinstance(string, str)
        if not erreur:
            # is the chain empty?
            erreur = string.strip() == ''
        # result
        return not erreur

In lines 3–11, it defines a static method that returns true if its parameter [str] is a non-empty string;

13.2.1.2.2. The class [Personne]

The class [Personne] (Personne.py) derives from the class [BaseEntity]:


# imports
from BaseEntity import BaseEntity
from MyException import MyException
from Utils import Utils
 
 
# person class
class Personne(BaseEntity):
    # properties excluded from class state
    excluded_keys = []
 
    # class properties
    # id: person's identifier
    # first name: person's first name
    # name: person's name
    # age: age of the person
    @staticmethod
    def get_allowed_keys() -> list:
        # id: object identifier
        return BaseEntity.get_allowed_keys() + ["nom", "prénom", "âge"]
 
    # getters
    @property
    def prénom(self) -> str:
        return self.__prénom
 
    @property
    def nom(self) -> str:
        return self.__nom
 
    @property
    def âge(self) -> int:
        return self.__âge
 
    # setters
    @prénom.setter
    def prénom(self, prénom: str):
        # first name must be non-empty
        if Utils.is_string_ok(prénom):
            self.__prénom = prénom.strip()
        else:
            raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
 
    @nom.setter
    def nom(self, nom: str):
        # first name must be non-empty
        if Utils.is_string_ok(nom):
            self.__nom = nom.strip()
        else:
            raise MyException(12, "Le nom doit être une chaîne de caractères non vide")
 
    @âge.setter
    def âge(self, âge: int):
        # age must be an integer >=0
        erreur = False
        if isinstance(âge, int):
            if âge >= 0:
                self.__âge = âge
            else:
                erreur = True
        else:
            erreur = True
        # mistake?
        if erreur:
            raise MyException(13, "L'âge doit être un entier >=0")
  • line 8: the class [Personne] derives from the class [BaseEntity];
  • Lines 8–65: We have retained most of the [Personne] class discussed earlier. The differences are as follows:
    • the class no longer has a constructor;
    • the class uses the [MyException] exception, see line 65;
    • it has a static method, [get_allowed_keys], lines 17–20, which defines the list of its properties. The properties specific to the [Personne] class are added to those of the parent class [BaseEntity];
    • it has a static list, [excluded_keys], which we will return to later;
13.2.1.2.3. The [Enseignant] class

The class [Enseignant] (Enseignant.py) derives from the class [Personne]:


# imports
from MyException import MyException
from Personne import Personne
from Utils import Utils
 
 
# class Teacher
class Enseignant(Personne):
    # properties excluded from class state
    excluded_keys = []
 
    # class properties
    # id: person's identifier
    # first name: person's first name
    # name: person's name
    # age: age of the person
    # discipline: discipline taught
    @staticmethod
    def get_allowed_keys() -> list:
        # id: object identifier
        return Personne.get_allowed_keys() + ["discipline"]
 
    # properties
    @property
    def discipline(self) -> str:
        return self.__discipline
 
    @discipline.setter
    def discipline(self, discipline: str):
        # the discipline must be a non-empty string
        if Utils.is_string_ok(discipline):
            self.__discipline = discipline
        else:
            raise MyException(21, "La discipline doit être une chaîne de caractères non vide")
 
    # show method
    def show(self):
        print(f"Enseignant[{self.id}, {self.prénom}, {self.nom}, {self.âge}]")
  • line 8: the class [Enseignant] extends (or derives from) the class [Personne];
  • lines 18–21: define the list of class properties;
  • lines 37–38: the method [show] displays the teacher’s identity;
13.2.1.2.4. The [config] configuration

The example scripts use the following configuration [config]:


def configure():
    import os
 
    # configuration file folder
    script_dir = os.path.dirname(os.path.abspath(__file__))
 
    # absolute paths of folders to put in the syspath
    absolute_dependencies = [
        # the BaseEntity class
        f"{script_dir}/entities",
    ]
 
    # update syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)
 
    # we return the configuration
    return {}
  • lines 8–10: the directories containing the project dependencies;
  • lines 14-15: Python Path is built;
  • line 18: we return an empty dictionary (there are no other configurations besides the syspath);
13.2.1.2.5. The [fromdict_01] script

The [fromdict_01] script is as follows:


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
 
# a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1.show()
  • Line 10: A [Enseignant] object is created from a dictionary. To do this, the class’s default constructor is used to create a [Enseignant] object, to which the [fromdict] method is then applied. It is important to understand that here, the [fromdict] method being executed is that of the parent class [BaseEntity]. In fact:
    • The method [fromdict] is first looked for in the class [Enseignant]. It does not exist;
    • it is then searched for in the parent class [Personne]. It does not exist;
    • it is then searched for in the parent class [BaseEntity]. It exists;
  • line 11: the object [Enseignant] is displayed;

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_01.py
Enseignant[1, paul, lourou, 56]
 
Process finished with exit code 0
13.2.1.2.6. The script [fromdict_02]

The script [fromdict_02] is as follows:


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
 
# a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
enseignant1.show()
  • line 10: we create a teacher with an empty first name. This should raise an exception because the [Personne] class does not accept empty first names. This example demonstrates the difference between a dictionary and an object. The latter can validate its properties, whereas the dictionary cannot;

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 55, in fromdict
    setattr(self, key, value)
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\Personne.py", line 42, in prénom
    raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
MyException.MyException: MyException[11, Le prénom doit être une chaîne de caractères non vide]
 
Process finished with exit code 1
13.2.1.2.7. The script [fromdict_03]

The [fromdict_03] script is as follows:


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
 
# a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
enseignant1.show()
  • line 10: we create a teacher from a dictionary containing a key (gender) that does not belong to the [Enseignant] class. An exception should then be raised;

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py", line 10, in <module>
    enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
  File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line 51, in fromdict
    raise MyException(2, f"la clé [{key}] n'est pas autorisée")
MyException.MyException: MyException[2, la clé [sexe] n'est pas autorisée]
 
Process finished with exit code 1
13.2.1.2.8. The script [fromdict_04]

The script [fromdict_04] is a copy of [fromdict_03] with one minor difference:


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
 
# a teacher
enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"}, silent=True)
enseignant1.show()
  • line 10: we used the [silent=True] parameter to indicate that if a dictionary key is not a property of the [Enseignant] class, it should simply be ignored. In this case, no exception will be raised;

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_04.py
Enseignant[1, albert, lourou, 56]
 
Process finished with exit code 0

13.2.2. The [BaseEntity.asdict] method

13.2.2.1. Definition

The [BaseEntity.asdict] method returns a dictionary whose keys are the object’s properties:


    def asdict(self, included_keys: list = None, excluded_keys: list =[]) -> dict:
        # object attributes
        attributes = self.__dict__
        # new attributes
        new_attributes = {}
        # browse attributes
        for key, value in attributes.items():
            # if the key is explicitly requested
            if included_keys and key in included_keys:
                self.set_value(key, value, new_attributes)
            # otherwise, if the key is not excluded
            elif not included_keys and key not in self.__class__.excluded_keys and key not in excluded_keys:
                self.set_value(key, value, new_attributes)
        # render the attribute dictionary
        return new_attributes

Comments

  • line 1: the function [asdict] returns the object’s property dictionary;
  • line 1: [included_keys]: the list of keys to include in the dictionary;
  • line 1: [excluded_keys]: the list of keys to exclude from the dictionary;
  • line 3: the property [self.__dict__] returns the object’s property dictionary. The property names are the keys, and their values are the dictionary’s values. An object may contain references to other objects. In that case, the property names are prefixed with the name of the class to which they belong. This is something we do not want. We want the properties without their prefix;
  • line 3: it is important to understand here that if the function [asdict] is executed within a class derived from [BaseEntity], the property [self.__dict__] returns the dictionary of properties for the derived object;
  • line 5: the dictionary we are going to construct;
  • line 7: we iterate through the values of [self.__dict__] in the form (key, value);
  • line 9: if the current key belongs to the list of keys to include, then it is added to the dictionary [new_attributes] by the function [set_value], which we will describe shortly;
  • line 12: if the parameter [included_keys] is not present, then the parameter [excluded_keys] is used. If the property is not among the properties to be excluded, then it is added to the dictionary [new_attributes];
  • Line 12: There are several ways to exclude a property from the dictionary:
    • it was defined at the class attribute level [excluded_keys];
    • it has been defined in the list [excluded_keys] passed to the function [asdict];
    • the parameter [included_keys] is present and does not include the property;
  • line 15: the dictionary [new_attributes] is returned

The function [set_value] in lines 10 and 13 is as follows:


    @staticmethod
    def set_value(key: str, value, new_attributes: dict):
        # keys can be of the form __Class__key
        match = re.match("^.*?__(.*?)$", key)
        if match:
            # note the new key
            newkey = match.groups()[0]
        else:
            # the key remains unchanged
            newkey = key
        # insert the new key into the [new_attributes] dictionary
        # type, transforming if necessary the associated value into one of the
        # dict, list, simple type
        new_attributes[newkey] = BaseEntity.check_value(value)

Comments

  • line 4: check if the key is in the form __Class_key. This is the form it takes if it belongs to an object included in the main object. In this case, we only want to keep the string [key];
  • line 7: we keep only the string following the last two underscored characters of the string;
  • lines 8–10: if the key is not in the form __Class_key, then we keep it as is;
  • lines 11–14: the value associated with the key [newkey] is calculated by the static method [BaseEntity.check_value];

The static method [BaseEntity.check_value] is as follows:


    @staticmethod
    def check_value(value):
        # the value can be of type BaseEntity, list, dict or a simple type
        # is value an instance of BaseEntity?
        if isinstance(value, BaseEntity):
            value2 = value.asdict()
        # is value of type list
        elif isinstance(value, list):
            value2 = BaseEntity.list2list(value)
        # is value a dict type?
        elif isinstance(value, dict):
            value2 = BaseEntity.dict2dict(value)
        # value is a simple type
        else:
            value2 = value
        # we return the result
        return value2
  • line 1: the method [check_value] is static (a class method, not an instance method). It takes as a parameter the value to be associated with a dictionary key:
    • line 17: if this value is a simple type, it remains unchanged;
    • lines 5-6: if this value is of type BaseEntity, the value is replaced by its dictionary. This results in a recursive call;
    • lines 8–9: if this value is a list, then it is replaced by the value [BaseEntity.list2list];
    • lines 11-12: if this value is a dictionary, then it is replaced by the value [BaseEntity.dict2dict];

The static method [BaseEntity.list2list] is as follows:


    @staticmethod
    def list2list(liste: list) -> list:
        # inspect list items
        newlist = []
        for value in liste:
            newlist.append(BaseEntity.check_value(value))
        # return the new list
        return newlist
  • line 2: the method receives a list and returns a list;
  • lines 5-6: each value in the list passed as a parameter is replaced with the value returned by the static method [BaseEntity.check_value]. This is therefore a recursive call. The static method [BaseEntity.check_value] is called until its parameter [value] is a simple type (not a BaseEntity type, list, or dict);

The static method [BaseEntity.dict2dict] is as follows:


    @staticmethod
    def dict2dict(dictionary: dict) -> dict:
        # inspect dictionary items
        newdict = {}
        for key, value in dictionary.items():
            newdict[key] = BaseEntity.check_value(value)
        # the new dictionary is returned
        return newdict
  • line 2: the method receives a dictionary and returns a dictionary;
  • lines 5-6: each value in the dictionary passed as a parameter is replaced with the value returned by the static method [BaseEntity.check_value]. This is therefore a recursive call. The static method [BaseEntity.check_value] is called until its parameter [value] is a simple type (not a BaseEntity type, list, or dict);

13.2.2.2. Examples

The script [asdict_01] demonstrates various uses of the method [asdict]:


# configure the application
import config
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity
 
# a teacher
enseignant1 = Enseignant().fromdict({"id"1"nom""lourou""prénom""paul""âge"56})
dict1 = enseignant1.asdict()
print(type(dict1))
print(enseignant1.__dict__)
print(dict1)
print(enseignant1.asdict(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1)
# another teacher
enseignant2 = Enseignant().fromdict({"id"2"nom""abélard""prénom""béatrice""âge"57})
print(enseignant2.asdict())
print(enseignant2.asdict(included_keys=["_Personne__nom"]))
# a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asdict())
# a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asdict())

The results of the execution are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asdict_01.py
<class 'dict'>
{'_BaseEntity__id': 1, '_Personne__nom': 'lourou', '_Personne__prénom': 'paul', '_Personne__âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}
{'id': 1, 'nom': 'lourou', 'prénom': 'paul'}
{"id": 1, "nom": "lourou", "âge": 56}
{'id': 2, 'nom': 'abélard', 'âge': 57}
{'nom': 'abélard'}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}]}
{'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}], 'matières': {'maths': {'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, 'français': {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}}}
 
Process finished with exit code 0
  • Line 4 demonstrates the advantage of the [asdict] method over using the [__dict__] property. The properties are stripped of their class prefix. This is better suited for display;
  • There are several ways to use the [asdict] method:
    • if you want all properties: use the [asdict] method without parameters;
    • if you only want certain properties:
      • there are more properties to include than to exclude: use the single parameter [excluded_keys];
      • there are fewer properties to include than to exclude: use the single parameter [included_keys];

13.2.3. The [BaseEntity.asjson] method

This method returns the string jSON from an object of type [BaseEntity] or a derived type. It displays the string jSON from the dictionary returned by the [asdict] method. Its code is as follows:


def asjson(self, included_keys: list = None, excluded_keys: list = []) -> str:
        # the json string
        return json.dumps(self.asdict(included_keys=included_keys, excluded_keys=excluded_keys), ensure_ascii=False)
  • Line 1: The parameters of the [asjson] method are those of the [asdict] method;

Here is an example (asjson_01) using this method:


# configure the application
import config
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
from BaseEntity import BaseEntity
 
# a teacher
enseignant1 = Enseignant().fromdict({"id"1"nom""lourou""prénom""paul""âge"56})
print(type(enseignant1.asjson()))
print(enseignant1.asjson(excluded_keys=["_Personne__âge"]))
Enseignant.excluded_keys = ["_Personne__prénom"]
print(enseignant1.asjson())
# another teacher
enseignant2 = Enseignant().fromdict({"id"2"nom""abélard""prénom""béatrice""âge"57})
print(enseignant2.asjson())
print(enseignant2.asjson(included_keys=["_Personne__nom"]))
# a list of entities within an entity
Enseignant.excluded_keys = []
entity1 = BaseEntity()
enseignants = [enseignant1, enseignant2]
setattr(entity1, "enseignants", enseignants)
print(entity1.asjson())
# a dictionary of entities within an entity
matières = {"maths": enseignant1, "français": enseignant2}
setattr(entity1, "matières", matières)
print(entity1.asjson())

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/asjson_01.py
<class 'str'>
{"id": 1, "nom": "lourou", "prénom": "paul"}
{"id": 1, "nom": "lourou", "âge": 56}
{"id": 2, "nom": "abélard", "âge": 57}
{"nom": "abélard"}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}]}
{"enseignants": [{"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}], "matières": {"maths": {"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56}, "français": {"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57}}}
 
Process finished with exit code 0

The method [BaseEntity.__str__] uses the method [asjson] to display the identity of the object [BaseEntity] or a derived object:


# toString
    def __str__(self) -> str:
        return self.asjson()

13.2.4. The [BaseEntity.fromjson] method

The [BaseEntity.fromjson] method allows you to initialize an object of type [BaseEntity] or a derived type from a jSON dictionary. Its code is as follows:


def fromjson(self, json_state: str, silent: bool = False):
        # update object status from jSON string
        return self.fromdict(json.loads(json_state), silent=silent)
  • line 1: the method takes two parameters:
    • [json_state]: the jSON dictionary that will be used to initialize the [BaseEntity] object;
    • [silent]: to indicate whether the presence in the jSON dictionary of a key that cannot be accepted as a property of the [BaseEntity] object causes an exception (silent=False) or is simply ignored (silent=True);
  • line 3: we start by constructing the Python dictionary image of the jSON dictionary, then use the [fromdict] method to initialize the [BaseEntity] object from this Python dictionary;

Here is an example (fromjson_01):


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from Enseignant import Enseignant
import json
 
# a teacher
json1 = json.dumps({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
enseignant1 = Enseignant().fromjson(json1)
enseignant1.show()
  • line 11: the string jSON is created from a dictionary;
  • line 12: an object [Enseignant] is initialized with this string;
  • line 13: the teacher is displayed;

The results are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromjson_01.py
Enseignant[1, paul, lourou, 56]
 
Process finished with exit code 0

13.2.5. The [main] script

The [main] script summarizes the various methods encountered:


# configure the application
import config
 
config = config.configure()
 
# syspath is configured - imports can be made
from BaseEntity import BaseEntity
from MyException import MyException
 
 
# a class
class ChildEntity(BaseEntity):
    # attributes excluded from class state
    excluded_keys = []
 
    @staticmethod
    def get_allowed_keys():
        return ["att1", "att2", "att3", "att4"]
 
    @property
    def att1(self) -> int:
        return self.__att1
 
    @att1.setter
    def att1(self, value: int):
        if 10 >= value >= 1:
            self.__att1 = value
        else:
            raise MyException(1, f"L'attribut [att1] attend une valeur dans l'intervalle [1,10] ({value})")
 
 
# configuration ChildEntity
ChildEntity.excluded_keys = []
# instance ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2})
# pay attention to property names
# these are the names used in [excluded_keys] and [included_keys]
print(child.__dict__)
# properties not prefixed by their class
print(child)
 
# instance ChildEntity
try:
    child = ChildEntity().fromdict({"att1": 1, "att5": 5})
    print(child)
except MyException as erreur:
    print(erreur)
 
# instance ChildEntity
child = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
print(child)
 
# exclusion of certain keys from instance status
ChildEntity.excluded_keys = ['att3']
print(child)
 
# a key is explicitly excluded from the display
# it is added to those excluded globally at class level
print(child.asdict(excluded_keys=["_ChildEntity__att1"]))
print(child.asjson(excluded_keys=["att2"]))
 
# class interest in the dictionary
# it can check the validity of its contents
try:
    child = ChildEntity().fromdict({"att1": 20})
except MyException as erreur:
    print(erreur)
 
# instance ChildEntity
child1 = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
# instance ChildEntity containing another instance ChildEntity
child2 = ChildEntity().fromdict({"att1": 10, "att2": 20, "att3": 30, "att4": child1})
print(child2)
 
# included_keys has priority over excluded_keys which are then ignored
ChildEntity.excluded_keys = ['_ChildEntity__att1', 'att2']
print(child.asdict(included_keys=["_ChildEntity__att1", "att3"], excluded_keys=["att3", "att4"]))

The results of the execution are as follows:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/main.py
{'_ChildEntity__att1': 1, 'att2': 2}
{"att1": 1, "att2": 2}
MyException[2, la clé [att5] n'est pas autorisée]
{"att1": 1, "att2": 2, "att3": 3, "att4": 4}
{"att1": 1, "att2": 2, "att4": 4}
{'att2': 2, 'att4': 4}
{"att1": 1, "att4": 4}
MyException[1, L'attribut [att1] attend une valeur dans l'intervalle [1,10] (20)]
{"att1": 10, "att2": 20, "att4": {"att1": 1, "att2": 2, "att4": 4}}
{'att1': 1, 'att3': 3}
 
Process finished with exit code 0

Note line 2 of the results: it is the property [ChildEntity.__dict__] (line 38 of the code) that allows us to determine the names of the properties to include in the lists [included_keys] and [excluded_keys]. Note, still on line 2 of the results, that depending on whether the property is defined within the class via a getter/setter or whether it was created as one would create a dictionary key, it may or may not be prefixed with the class name [ChildEntity].