diff --git a/energyml-utils/docs/src/energyml/index.html b/energyml-utils/docs/src/energyml/index.html new file mode 100644 index 0000000..c188265 --- /dev/null +++ b/energyml-utils/docs/src/energyml/index.html @@ -0,0 +1,73 @@ + + + + + + +src.energyml API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+
+
+
+

Sub-modules

+
+
src.energyml.utils
+
+

The energyml.utils module. +It contains tools for energyml management …

+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/data/hdf.html b/energyml-utils/docs/src/energyml/utils/data/hdf.html new file mode 100644 index 0000000..08b2205 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/data/hdf.html @@ -0,0 +1,621 @@ + + + + + + +src.energyml.utils.data.hdf API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.data.hdf

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+from dataclasses import dataclass
+from io import BytesIO
+from typing import Optional, List, Tuple, Any, Union
+
+import h5py
+
+from ..epc import Epc, get_obj_identifier, ObjectNotFoundNotException, \
+    EPCRelsRelationshipType
+from ..introspection import search_attribute_matching_name_with_path, search_attribute_matching_name, \
+    get_obj_uuid, get_object_attribute
+
+
+@dataclass
+class DatasetReader:
+    def read_array(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+    def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+
+@dataclass
+class ETPReader(DatasetReader):
+    def read_array(self, obj_uri: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+    def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+
+@dataclass
+class HDF5FileReader(DatasetReader):
+    def read_array(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+        with h5py.File(source, "r") as f:
+            d_group = f[path_in_external_file]
+            return d_group[()].tolist()
+
+    def get_array_dimension(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+        with h5py.File(source, "r") as f:
+            return list(f[path_in_external_file].shape)
+
+    def extract_h5_datasets(
+            self, input_h5: Union[BytesIO, str], output_h5: Union[BytesIO, str], h5_datasets_paths: List[str]
+    ) -> None:
+        """
+        Copy all dataset from :param input_h5 matching with paths in :param h5_datasets_paths into the :param output
+        :param input_h5:
+        :param output_h5:
+        :param h5_datasets_paths:
+        :return:
+        """
+        if len(h5_datasets_paths) > 0:
+            with h5py.File(output_h5, "w") as f_dest:
+                with h5py.File(input_h5, "r") as f_src:
+                    for dataset in h5_datasets_paths:
+                        f_dest.create_dataset(dataset, data=f_src[dataset])
+
+
+def get_hdf_reference(obj) -> List[Any]:
+    """
+    See :func:`get_hdf_reference_with_path`. Only the value is returned, not the dot path into the object
+    :param obj:
+    :return:
+    """
+    return [
+        val
+        for path, val in get_hdf_reference_with_path(obj=obj)
+    ]
+
+
+def get_hdf_reference_with_path(obj: any) -> List[Tuple[str, Any]]:
+    """
+    See :func:`search_attribute_matching_name_with_path`. Search an attribute with type matching regex
+    "(PathInHdfFile|PathInExternalFile)".
+
+    :param obj:
+    :return: [ (Dot_Path_In_Obj, value), ...]
+    """
+    return search_attribute_matching_name_with_path(
+        obj,
+        "(PathInHdfFile|PathInExternalFile)"
+    )
+
+
+def get_crs_obj(
+        context_obj: Any,
+        path_in_root: Optional[str] = None,
+        root_obj: Optional[Any] = None,
+        epc: Optional[Epc] = None
+) -> Optional[Any]:
+    """
+    Search for the CRS object related to :param:`context_obj` into the :param:`epc`
+    :param context_obj:
+    :param path_in_root:
+    :param root_obj:
+    :param epc:
+    :return:
+    """
+    crs_list = search_attribute_matching_name(context_obj, r"\.*Crs", search_in_sub_obj=True, deep_search=False)
+    if crs_list is not None and len(crs_list) > 0:
+        crs = epc.get_object_by_identifier(get_obj_identifier(crs_list[0]))
+        if crs is None:
+            crs = epc.get_object_by_uuid(get_obj_uuid(crs_list[0]))
+        if crs is None:
+            raise ObjectNotFoundNotException(get_obj_identifier(crs_list[0]))
+        if crs is not None:
+            return crs
+
+    if context_obj != root_obj:
+        upper_path = path_in_root[:path_in_root.rindex(".")]
+        if len(upper_path) > 0:
+            return get_crs_obj(
+                context_obj=get_object_attribute(root_obj, upper_path),
+                path_in_root=upper_path,
+                root_obj=root_obj,
+                epc=epc,
+            )
+
+    return None
+
+
+def get_hdf5_path_from_external_path(
+        external_path_obj: Any,
+        path_in_root: Optional[str] = None,
+        root_obj: Optional[Any] = None,
+        epc: Optional[Epc] = None
+) -> Optional[str]:
+    """
+    Return the hdf5 file path (Searches for "uri" attribute or in :param:`epc` rels files).
+    :param external_path_obj: can be an attribute of an ExternalDataArrayPart
+    :param path_in_root:
+    :param root_obj:
+    :param epc:
+    :return:
+    """
+    if isinstance(external_path_obj, str):
+        # external_path_obj is maybe an attribute of an ExternalDataArrayPart, now search upper in the object
+        upper_path = path_in_root[:path_in_root.rindex(".")]
+        return get_hdf5_path_from_external_path(
+            external_path_obj=get_object_attribute(root_obj, upper_path),
+            path_in_root=upper_path,
+            root_obj=root_obj,
+            epc=epc,
+        )
+    elif type(external_path_obj).__name__ == "ExternalDataArrayPart":
+        epc_folder = epc.get_epc_file_folder()
+        h5_uri = search_attribute_matching_name(external_path_obj, "uri")
+        if h5_uri is not None and len(h5_uri) > 0:
+            return f"{epc_folder}/{h5_uri[0]}"
+    else:
+        epc_folder = epc.get_epc_file_folder()
+        hdf_proxy = search_attribute_matching_name(external_path_obj, "HdfProxy")[0]
+        if hdf_proxy is not None:
+            hdf_proxy_obj = epc.get_object_by_identifier(get_obj_identifier(hdf_proxy))
+            if hdf_proxy_obj is not None:
+                for rel in epc.additional_rels.get(get_obj_identifier(hdf_proxy_obj), []):
+                    # print(f"\trel : {rel}")
+                    if rel.type_value == EPCRelsRelationshipType.EXTERNAL_RESOURCE.get_type():
+                        return f"{epc_folder}/{rel.target}"
+    return None
+
+
+
+
+
+
+
+

Functions

+
+
+def get_crs_obj(context_obj: Any, path_in_root: Optional[str] = None, root_obj: Optional[Any] = None, epc: Optional[Epc] = None) ‑> Optional[Any] +
+
+

Search for the CRS object related to :param:context_obj into the :param:epc +:param context_obj: +:param path_in_root: +:param root_obj: +:param epc: +:return:

+
+ +Expand source code + +
def get_crs_obj(
+        context_obj: Any,
+        path_in_root: Optional[str] = None,
+        root_obj: Optional[Any] = None,
+        epc: Optional[Epc] = None
+) -> Optional[Any]:
+    """
+    Search for the CRS object related to :param:`context_obj` into the :param:`epc`
+    :param context_obj:
+    :param path_in_root:
+    :param root_obj:
+    :param epc:
+    :return:
+    """
+    crs_list = search_attribute_matching_name(context_obj, r"\.*Crs", search_in_sub_obj=True, deep_search=False)
+    if crs_list is not None and len(crs_list) > 0:
+        crs = epc.get_object_by_identifier(get_obj_identifier(crs_list[0]))
+        if crs is None:
+            crs = epc.get_object_by_uuid(get_obj_uuid(crs_list[0]))
+        if crs is None:
+            raise ObjectNotFoundNotException(get_obj_identifier(crs_list[0]))
+        if crs is not None:
+            return crs
+
+    if context_obj != root_obj:
+        upper_path = path_in_root[:path_in_root.rindex(".")]
+        if len(upper_path) > 0:
+            return get_crs_obj(
+                context_obj=get_object_attribute(root_obj, upper_path),
+                path_in_root=upper_path,
+                root_obj=root_obj,
+                epc=epc,
+            )
+
+    return None
+
+
+
+def get_hdf5_path_from_external_path(external_path_obj: Any, path_in_root: Optional[str] = None, root_obj: Optional[Any] = None, epc: Optional[Epc] = None) ‑> Optional[str] +
+
+

Return the hdf5 file path (Searches for "uri" attribute or in :param:epc rels files). +:param external_path_obj: can be an attribute of an ExternalDataArrayPart +:param path_in_root: +:param root_obj: +:param epc: +:return:

+
+ +Expand source code + +
def get_hdf5_path_from_external_path(
+        external_path_obj: Any,
+        path_in_root: Optional[str] = None,
+        root_obj: Optional[Any] = None,
+        epc: Optional[Epc] = None
+) -> Optional[str]:
+    """
+    Return the hdf5 file path (Searches for "uri" attribute or in :param:`epc` rels files).
+    :param external_path_obj: can be an attribute of an ExternalDataArrayPart
+    :param path_in_root:
+    :param root_obj:
+    :param epc:
+    :return:
+    """
+    if isinstance(external_path_obj, str):
+        # external_path_obj is maybe an attribute of an ExternalDataArrayPart, now search upper in the object
+        upper_path = path_in_root[:path_in_root.rindex(".")]
+        return get_hdf5_path_from_external_path(
+            external_path_obj=get_object_attribute(root_obj, upper_path),
+            path_in_root=upper_path,
+            root_obj=root_obj,
+            epc=epc,
+        )
+    elif type(external_path_obj).__name__ == "ExternalDataArrayPart":
+        epc_folder = epc.get_epc_file_folder()
+        h5_uri = search_attribute_matching_name(external_path_obj, "uri")
+        if h5_uri is not None and len(h5_uri) > 0:
+            return f"{epc_folder}/{h5_uri[0]}"
+    else:
+        epc_folder = epc.get_epc_file_folder()
+        hdf_proxy = search_attribute_matching_name(external_path_obj, "HdfProxy")[0]
+        if hdf_proxy is not None:
+            hdf_proxy_obj = epc.get_object_by_identifier(get_obj_identifier(hdf_proxy))
+            if hdf_proxy_obj is not None:
+                for rel in epc.additional_rels.get(get_obj_identifier(hdf_proxy_obj), []):
+                    # print(f"\trel : {rel}")
+                    if rel.type_value == EPCRelsRelationshipType.EXTERNAL_RESOURCE.get_type():
+                        return f"{epc_folder}/{rel.target}"
+    return None
+
+
+
+def get_hdf_reference(obj) ‑> List[Any] +
+
+

See :func:get_hdf_reference_with_path(). Only the value is returned, not the dot path into the object +:param obj: +:return:

+
+ +Expand source code + +
def get_hdf_reference(obj) -> List[Any]:
+    """
+    See :func:`get_hdf_reference_with_path`. Only the value is returned, not the dot path into the object
+    :param obj:
+    :return:
+    """
+    return [
+        val
+        for path, val in get_hdf_reference_with_path(obj=obj)
+    ]
+
+
+
+def get_hdf_reference_with_path(obj: ) ‑> List[Tuple[str, Any]] +
+
+

See :func:search_attribute_matching_name_with_path. Search an attribute with type matching regex +"(PathInHdfFile|PathInExternalFile)".

+

:param obj: +:return: [ (Dot_Path_In_Obj, value), …]

+
+ +Expand source code + +
def get_hdf_reference_with_path(obj: any) -> List[Tuple[str, Any]]:
+    """
+    See :func:`search_attribute_matching_name_with_path`. Search an attribute with type matching regex
+    "(PathInHdfFile|PathInExternalFile)".
+
+    :param obj:
+    :return: [ (Dot_Path_In_Obj, value), ...]
+    """
+    return search_attribute_matching_name_with_path(
+        obj,
+        "(PathInHdfFile|PathInExternalFile)"
+    )
+
+
+
+
+
+

Classes

+
+
+class DatasetReader +
+
+

DatasetReader()

+
+ +Expand source code + +
@dataclass
+class DatasetReader:
+    def read_array(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+    def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+

Subclasses

+ +

Methods

+
+
+def get_array_dimension(self, source: str, path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+    return None
+
+
+
+def read_array(self, source: str, path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def read_array(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+    return None
+
+
+
+
+
+class ETPReader +
+
+

ETPReader()

+
+ +Expand source code + +
@dataclass
+class ETPReader(DatasetReader):
+    def read_array(self, obj_uri: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+    def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+        return None
+
+

Ancestors

+ +

Methods

+
+
+def get_array_dimension(self, source: str, path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def get_array_dimension(self, source: str, path_in_external_file: str) -> Optional[List[Any]]:
+    return None
+
+
+
+def read_array(self, obj_uri: str, path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def read_array(self, obj_uri: str, path_in_external_file: str) -> Optional[List[Any]]:
+    return None
+
+
+
+
+
+class HDF5FileReader +
+
+

HDF5FileReader()

+
+ +Expand source code + +
@dataclass
+class HDF5FileReader(DatasetReader):
+    def read_array(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+        with h5py.File(source, "r") as f:
+            d_group = f[path_in_external_file]
+            return d_group[()].tolist()
+
+    def get_array_dimension(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+        with h5py.File(source, "r") as f:
+            return list(f[path_in_external_file].shape)
+
+    def extract_h5_datasets(
+            self, input_h5: Union[BytesIO, str], output_h5: Union[BytesIO, str], h5_datasets_paths: List[str]
+    ) -> None:
+        """
+        Copy all dataset from :param input_h5 matching with paths in :param h5_datasets_paths into the :param output
+        :param input_h5:
+        :param output_h5:
+        :param h5_datasets_paths:
+        :return:
+        """
+        if len(h5_datasets_paths) > 0:
+            with h5py.File(output_h5, "w") as f_dest:
+                with h5py.File(input_h5, "r") as f_src:
+                    for dataset in h5_datasets_paths:
+                        f_dest.create_dataset(dataset, data=f_src[dataset])
+
+

Ancestors

+ +

Methods

+
+
+def extract_h5_datasets(self, input_h5: Union[_io.BytesIO, str], output_h5: Union[_io.BytesIO, str], h5_datasets_paths: List[str]) ‑> None +
+
+

Copy all dataset from :param input_h5 matching with paths in :param h5_datasets_paths into the :param output +:param input_h5: +:param output_h5: +:param h5_datasets_paths: +:return:

+
+ +Expand source code + +
def extract_h5_datasets(
+        self, input_h5: Union[BytesIO, str], output_h5: Union[BytesIO, str], h5_datasets_paths: List[str]
+) -> None:
+    """
+    Copy all dataset from :param input_h5 matching with paths in :param h5_datasets_paths into the :param output
+    :param input_h5:
+    :param output_h5:
+    :param h5_datasets_paths:
+    :return:
+    """
+    if len(h5_datasets_paths) > 0:
+        with h5py.File(output_h5, "w") as f_dest:
+            with h5py.File(input_h5, "r") as f_src:
+                for dataset in h5_datasets_paths:
+                    f_dest.create_dataset(dataset, data=f_src[dataset])
+
+
+
+def get_array_dimension(self, source: Union[_io.BytesIO, str], path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def get_array_dimension(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+    with h5py.File(source, "r") as f:
+        return list(f[path_in_external_file].shape)
+
+
+
+def read_array(self, source: Union[_io.BytesIO, str], path_in_external_file: str) ‑> Optional[List[Any]] +
+
+
+
+ +Expand source code + +
def read_array(self, source: Union[BytesIO, str], path_in_external_file: str) -> Optional[List[Any]]:
+    with h5py.File(source, "r") as f:
+        d_group = f[path_in_external_file]
+        return d_group[()].tolist()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/data/helper.html b/energyml-utils/docs/src/energyml/utils/data/helper.html new file mode 100644 index 0000000..94a6a65 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/data/helper.html @@ -0,0 +1,1286 @@ + + + + + + +src.energyml.utils.data.helper API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.data.helper

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import inspect
+import sys
+from typing import Any, Optional, Callable, Literal, List, Union, Tuple
+
+from .hdf import get_hdf5_path_from_external_path, HDF5FileReader, get_hdf_reference, get_crs_obj
+from ..epc import Epc, get_obj_identifier
+from ..introspection import snake_case, get_object_attribute_no_verif, \
+    search_attribute_matching_name_with_path, search_attribute_matching_name, flatten_concatenation, \
+    search_attribute_in_upper_matching_name
+
+_ARRAY_NAMES_ = [
+    "BooleanArrayFromDiscretePropertyArray",
+    "BooleanArrayFromIndexArray",
+    "BooleanConstantArray",
+    "BooleanExternalArray",
+    "BooleanHdf5Array",
+    "BooleanXmlArray",
+    "CompoundExternalArray",
+    "DasTimeArray",
+    "DoubleConstantArray",
+    "DoubleHdf5Array",
+    "DoubleLatticeArray",
+    "ExternalDataArray",
+    "FloatingPointConstantArray",
+    "FloatingPointExternalArray",
+    "FloatingPointLatticeArray",
+    "FloatingPointXmlArray",
+    "IntegerArrayFromBooleanMaskArray",
+    "IntegerConstantArray",
+    "IntegerExternalArray",
+    "IntegerHdf5Array",
+    "IntegerLatticeArray",
+    "IntegerRangeArray",
+    "IntegerXmlArray",
+    "JaggedArray",
+    "ParametricLineArray",
+    "ParametricLineFromRepresentationLatticeArray",
+    "Point2DHdf5Array",
+    "Point3DFromRepresentationLatticeArray",
+    "Point3DHdf5Array",
+    "Point3DLatticeArray",
+    "Point3DParametricArray",
+    "Point3DZvalueArray",
+    "ResqmlJaggedArray",
+    "StringConstantArray",
+    "StringExternalArray",
+    "StringHdf5Array",
+    "StringXmlArray"
+]
+
+
+def get_array_reader_function(array_type_name: str) -> Optional[Callable]:
+    """
+    Returns the name of the potential appropriate function to read an object with type is named :param array_type_name
+    :param array_type_name: the initial type name
+    :return:
+    """
+    for name, obj in inspect.getmembers(sys.modules[__name__]):
+        if name == f"read_{snake_case(array_type_name)}":
+            return obj
+    return None
+
+
+def _array_name_mapping(array_type_name: str) -> str:
+    """
+    Transform the type name to match existing reader function
+    :param array_type_name:
+    :return:
+    """
+    array_type_name = array_type_name.replace("3D", "3d").replace("2D", "2d")
+    if array_type_name.endswith("ConstantArray"):
+        return "ConstantArray"
+    elif "External" in array_type_name or "Hdf5" in array_type_name:
+        return "ExternalArray"
+    elif array_type_name.endswith("XmlArray"):
+        return "XmlArray"
+    elif "Jagged" in array_type_name:
+        return "JaggedArray"
+    elif "Lattice" in array_type_name:
+        if "Integer" in array_type_name or "Double" in array_type_name:
+            return "int_double_lattice_array"
+    return array_type_name
+
+
+def read_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read an array and return a list. The array is read depending on its type. see. :py:func:`energyml.utils.data.helper.get_supported_array`
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    if isinstance(energyml_array, list):
+        return energyml_array
+    array_type_name = _array_name_mapping(type(energyml_array).__name__)
+
+    reader_func = get_array_reader_function(array_type_name)
+    if reader_func is not None:
+        return reader_func(
+            energyml_array=energyml_array,
+            root_obj=root_obj,
+            path_in_root=path_in_root,
+            epc=epc,
+        )
+    else:
+        print(f"Type {array_type_name} is not supported: function read_{snake_case(array_type_name)} not found")
+        raise Exception(f"Type {array_type_name} is not supported\n\t{energyml_array}: \n\tfunction read_{snake_case(array_type_name)} not found")
+
+
+def get_supported_array() -> List[str]:
+    """
+    Return a list of the supported arrays for the use of :py:func:`energyml.utils.data.helper.read_array` function.
+    :return:
+    """
+    return [x for x in _ARRAY_NAMES_ if get_array_reader_function(_array_name_mapping(x)) is not None]
+
+
+def get_not_supported_array():
+    """
+    Return a list of the NOT supported arrays for the use of :py:func:`energyml.utils.data.helper.read_array` function.
+    :return:
+    """
+    return [x for x in _ARRAY_NAMES_ if get_array_reader_function(_array_name_mapping(x)) is None]
+
+
+def read_constant_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a constant array ( BooleanConstantArray, DoubleConstantArray, FloatingPointConstantArray, IntegerConstantArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    # print(f"Reading constant array\n\t{energyml_array}")
+
+    value = get_object_attribute_no_verif(energyml_array, "value")
+    count = get_object_attribute_no_verif(energyml_array, "count")
+
+    # print(f"\tValue : {[value for i in range(0, count)]}")
+
+    return [value for i in range(0, count)]
+
+
+def read_xml_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a xml array ( BooleanXmlArray, FloatingPointXmlArray, IntegerXmlArray, StringXmlArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    values = get_object_attribute_no_verif(energyml_array, "values")
+    # count = get_object_attribute_no_verif(energyml_array, "count_per_value")
+    return values
+
+
+def read_jagged_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a jagged array
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    elements = read_array(
+        energyml_array=get_object_attribute_no_verif(energyml_array, "elements"),
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".elements",
+        epc=epc,
+    )
+    cumulative_length = read_array(
+        energyml_array=read_array(get_object_attribute_no_verif(energyml_array, "cumulative_length")),
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".cumulative_length",
+        epc=epc,
+    )
+
+    res = []
+    previous = 0
+    for cl in cumulative_length:
+        res.append(elements[previous: cl])
+        previous = cl
+    return res
+
+
+def read_external_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read an external array (BooleanExternalArray, BooleanHdf5Array, DoubleHdf5Array, IntegerHdf5Array, StringExternalArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    hdf5_path = get_hdf5_path_from_external_path(
+                external_path_obj=energyml_array,
+                path_in_root=path_in_root,
+                root_obj=root_obj,
+                epc=epc,
+    )
+    h5_reader = HDF5FileReader()
+    path_in_external = get_hdf_reference(energyml_array)[0]
+
+    result_array = h5_reader.read_array(hdf5_path, path_in_external)
+
+    if path_in_root.lower().endswith("points") and len(result_array) > 0 and len(result_array[0]) == 3:
+        crs = get_crs_obj(
+            context_obj=energyml_array,
+            path_in_root=path_in_root,
+            root_obj=root_obj,
+            epc=epc,
+        )
+        zincreasing_downward = is_z_reversed(crs)
+
+        if zincreasing_downward:
+            result_array = list(map(lambda p: [p[0], p[1], -p[2]], result_array))
+
+    return result_array
+
+
+def read_int_double_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read DoubleLatticeArray or IntegerLatticeArray.
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    start_value = get_object_attribute_no_verif(energyml_array, "start_value")
+    offset = get_object_attribute_no_verif(energyml_array, "offset")
+
+    result = []
+
+    # if len(offset) == 1:
+    #     pass
+    # elif len(offset) == 2:
+    #     pass
+    # else:
+    raise Exception(f"{type(energyml_array)} read with an offset of length {len(offset)} is not supported")
+
+    # return result
+
+
+def _point_as_array(point: Any) -> List:
+    """
+    Transform a point that has "coordinate1", "coordinate2", "coordinate3" as attributes into a list.
+    :param point:
+    :return:
+    """
+    return [
+        get_object_attribute_no_verif(point, "coordinate1"),
+        get_object_attribute_no_verif(point, "coordinate2"),
+        get_object_attribute_no_verif(point, "coordinate3"),
+    ]
+
+
+def prod_n_tab(val: Union[float, int, str], tab: List[Union[float, int, str]]):
+    """
+    Multiply every value of the list 'tab' by the constant 'val'
+    :param val:
+    :param tab:
+    :return:
+    """
+    return list(map(lambda x: x*val, tab))
+
+
+def sum_lists(l1: List, l2: List):
+    """
+    Sums 2 lists values.
+
+    Example:
+        [1,1,1] and [2,2,3,6] gives : [3,3,4,6]
+
+    :param l1:
+    :param l2:
+    :return:
+    """
+    return [l1[i] + l2[i] for i in range(min(len(l1), len(l2)))]+max(l1, l2, key=len)[min(len(l1), len(l2)):]
+
+
+def read_point3d_zvalue_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read a Point3D2ValueArray
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    supporting_geometry = get_object_attribute_no_verif(energyml_array, "supporting_geometry")
+    sup_geom_array = read_array(
+        energyml_array=supporting_geometry,
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".SupportingGeometry",
+        epc=epc,
+    )
+
+    zvalues = get_object_attribute_no_verif(energyml_array, "zvalues")
+    zvalues_array = flatten_concatenation(read_array(
+        energyml_array=zvalues,
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".ZValues",
+        epc=epc,
+    ))
+
+    count = 0
+
+    for i in range(len(sup_geom_array)):
+        try:
+            sup_geom_array[i][2] = zvalues_array[i]
+        except Exception as e:
+            if count == 0:
+                print(e, f": {i} is out of bound of {len(zvalues_array)}")
+                count = count + 1
+
+    return sup_geom_array
+
+
+def read_point3d_from_representation_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read a Point3DFromRepresentationLatticeArray.
+
+    Note: Only works for Grid2DRepresentation.
+
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    supporting_rep_identifier = get_obj_identifier(get_object_attribute_no_verif(energyml_array, "supporting_representation"))
+    print(f"energyml_array : {energyml_array}\n\t{supporting_rep_identifier}")
+    supporting_rep = epc.get_object_by_identifier(supporting_rep_identifier)
+
+    # TODO chercher un pattern \.*patch\.*.[d]+ pour trouver le numero du patch dans le path_in_root puis lire le patch
+    # print(f"path_in_root {path_in_root}")
+
+    result = []
+    if "grid2d" in str(type(supporting_rep)).lower():
+        patch_path, patch = search_attribute_matching_name_with_path(supporting_rep, "Grid2dPatch")[0]
+        points = read_grid2d_patch(
+            patch=patch,
+            grid2d=supporting_rep,
+            path_in_root=patch_path,
+            epc=epc,
+        )
+        # TODO: take the points by there indices from the NodeIndicesOnSupportingRepresentation
+        result = points
+
+    else:
+        raise Exception(f"Not supported type {type(energyml_array)} for object {type(root_obj)}")
+    # pour trouver les infos qu'il faut
+    return result
+
+
+def read_grid2d_patch(
+        patch: Any,
+        grid2d: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List:
+    points_path, points_obj = search_attribute_matching_name_with_path(patch, "Geometry.Points")[0]
+
+    return read_array(
+        energyml_array=points_obj,
+        root_obj=grid2d,
+        path_in_root=path_in_root + points_path,
+        epc=epc,
+    )
+
+
+def read_point3d_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List:
+    """
+    Read a Point3DLatticeArray.
+
+    Note: If a CRS is found and its 'ZIncreasingDownward' is set to true or its
+
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    result = []
+    origin = _point_as_array(get_object_attribute_no_verif(energyml_array, "origin"))
+    offset = get_object_attribute_no_verif(energyml_array, "offset")
+
+    if len(offset) == 2:
+        slowest = offset[0]
+        fastest = offset[1]
+
+        crs_sa_count = search_attribute_in_upper_matching_name(
+            obj=energyml_array,
+            name_rgx="SlowestAxisCount",
+            root_obj=root_obj,
+            current_path=path_in_root,
+        )
+
+        crs_fa_count = search_attribute_in_upper_matching_name(
+            obj=energyml_array,
+            name_rgx="FastestAxisCount",
+            root_obj=root_obj,
+            current_path=path_in_root,
+        )
+
+        crs = get_crs_obj(
+            context_obj=energyml_array,
+            path_in_root=path_in_root,
+            root_obj=root_obj,
+            epc=epc,
+        )
+        zincreasing_downward = is_z_reversed(crs)
+
+        slowest_vec = _point_as_array(get_object_attribute_no_verif(slowest, "offset"))
+        slowest_spacing = read_array(get_object_attribute_no_verif(slowest, "spacing"))
+        slowest_table = list(map(lambda x: prod_n_tab(x, slowest_vec), slowest_spacing))
+
+        fastest_vec = _point_as_array(get_object_attribute_no_verif(fastest, "offset"))
+        fastest_spacing = read_array(get_object_attribute_no_verif(fastest, "spacing"))
+        fastest_table = list(map(lambda x: prod_n_tab(x, fastest_vec), fastest_spacing))
+
+        slowest_size = len(slowest_table)
+        fastest_size = len(fastest_table)
+
+        if len(crs_sa_count) > 0 and len(crs_fa_count) and crs_sa_count[0] == fastest_size:
+            print("reversing order")
+            # if offset were given in the wrong order
+            tmp_table = slowest_table
+            slowest_table = fastest_table
+            fastest_table = tmp_table
+
+            tmp_size = slowest_size
+            slowest_size = fastest_size
+            fastest_size = tmp_size
+
+        for i in range(slowest_size):
+            for j in range(fastest_size):
+                previous_value = origin
+                # to avoid a sum of the parts of the array at each iteration, I take the previous value in the same line
+                # number i and add the fastest_table[j] value
+
+                if j > 0:
+                    if i > 0:
+                        line_idx = i * fastest_size  # numero de ligne
+                        previous_value = result[line_idx + j - 1]
+                    else:
+                        previous_value = result[j - 1]
+                    if zincreasing_downward:
+                        result.append(sum_lists(previous_value, slowest_table[i - 1]))
+                    else:
+                        result.append(sum_lists(previous_value, fastest_table[j - 1]))
+                else:
+                    if i > 0:
+                        prev_line_idx = (i - 1) * fastest_size  # numero de ligne precedent
+                        previous_value = result[prev_line_idx]
+                        if zincreasing_downward:
+                            result.append(sum_lists(previous_value, fastest_table[j - 1]))
+                        else:
+                            result.append(sum_lists(previous_value, slowest_table[i - 1]))
+                    else:
+                        result.append(previous_value)
+    else:
+        raise Exception(f"{type(energyml_array)} read with an offset of length {len(offset)} is not supported")
+
+    return result
+
+
+def is_z_reversed(crs: Optional[Any]) -> bool:
+    """
+    Returns True if the Z axe is reverse (ZIncreasingDownward=='True' or VerticalAxis.Direction=='down')
+    :param crs:
+    :return: By default, False is returned (if 'crs' is None)
+    """
+    reverse_z_values = False
+    if crs is not None:
+        # resqml 201
+        zincreasing_downward = search_attribute_matching_name(crs, "ZIncreasingDownward")
+        if len(zincreasing_downward) > 0:
+            reverse_z_values = zincreasing_downward[0]
+
+        # resqml >= 22
+        vert_axis = search_attribute_matching_name(crs, "VerticalAxis.Direction")
+        if len(vert_axis) > 0:
+            reverse_z_values = vert_axis[0].lower() == "down"
+
+    return reverse_z_values
+
+
+# def read_boolean_constant_array(
+#         energyml_array: Any,
+#         root_obj: Optional[Any] = None,
+#         path_in_root: Optional[str] = None,
+#         epc: Optional[Epc] = None
+# ):
+#     print(energyml_array)
+
+
+
+
+
+
+
+

Functions

+
+
+def get_array_reader_function(array_type_name: str) ‑> Optional[Callable] +
+
+

Returns the name of the potential appropriate function to read an object with type is named :param array_type_name +:param array_type_name: the initial type name +:return:

+
+ +Expand source code + +
def get_array_reader_function(array_type_name: str) -> Optional[Callable]:
+    """
+    Returns the name of the potential appropriate function to read an object with type is named :param array_type_name
+    :param array_type_name: the initial type name
+    :return:
+    """
+    for name, obj in inspect.getmembers(sys.modules[__name__]):
+        if name == f"read_{snake_case(array_type_name)}":
+            return obj
+    return None
+
+
+
+def get_not_supported_array() +
+
+

Return a list of the NOT supported arrays for the use of :py:func:energyml.utils.data.helper.read_array function. +:return:

+
+ +Expand source code + +
def get_not_supported_array():
+    """
+    Return a list of the NOT supported arrays for the use of :py:func:`energyml.utils.data.helper.read_array` function.
+    :return:
+    """
+    return [x for x in _ARRAY_NAMES_ if get_array_reader_function(_array_name_mapping(x)) is None]
+
+
+
+def get_supported_array() ‑> List[str] +
+
+

Return a list of the supported arrays for the use of :py:func:energyml.utils.data.helper.read_array function. +:return:

+
+ +Expand source code + +
def get_supported_array() -> List[str]:
+    """
+    Return a list of the supported arrays for the use of :py:func:`energyml.utils.data.helper.read_array` function.
+    :return:
+    """
+    return [x for x in _ARRAY_NAMES_ if get_array_reader_function(_array_name_mapping(x)) is not None]
+
+
+
+def is_z_reversed(crs: Optional[Any]) ‑> bool +
+
+

Returns True if the Z axe is reverse (ZIncreasingDownward=='True' or VerticalAxis.Direction=='down') +:param crs: +:return: By default, False is returned (if 'crs' is None)

+
+ +Expand source code + +
def is_z_reversed(crs: Optional[Any]) -> bool:
+    """
+    Returns True if the Z axe is reverse (ZIncreasingDownward=='True' or VerticalAxis.Direction=='down')
+    :param crs:
+    :return: By default, False is returned (if 'crs' is None)
+    """
+    reverse_z_values = False
+    if crs is not None:
+        # resqml 201
+        zincreasing_downward = search_attribute_matching_name(crs, "ZIncreasingDownward")
+        if len(zincreasing_downward) > 0:
+            reverse_z_values = zincreasing_downward[0]
+
+        # resqml >= 22
+        vert_axis = search_attribute_matching_name(crs, "VerticalAxis.Direction")
+        if len(vert_axis) > 0:
+            reverse_z_values = vert_axis[0].lower() == "down"
+
+    return reverse_z_values
+
+
+
+def prod_n_tab(val: Union[float, int, str], tab: List[Union[float, int, str]]) +
+
+

Multiply every value of the list 'tab' by the constant 'val' +:param val: +:param tab: +:return:

+
+ +Expand source code + +
def prod_n_tab(val: Union[float, int, str], tab: List[Union[float, int, str]]):
+    """
+    Multiply every value of the list 'tab' by the constant 'val'
+    :param val:
+    :param tab:
+    :return:
+    """
+    return list(map(lambda x: x*val, tab))
+
+
+
+def read_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List[Any] +
+
+

Read an array and return a list. The array is read depending on its type. see. :py:func:energyml.utils.data.helper.get_supported_array +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read an array and return a list. The array is read depending on its type. see. :py:func:`energyml.utils.data.helper.get_supported_array`
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    if isinstance(energyml_array, list):
+        return energyml_array
+    array_type_name = _array_name_mapping(type(energyml_array).__name__)
+
+    reader_func = get_array_reader_function(array_type_name)
+    if reader_func is not None:
+        return reader_func(
+            energyml_array=energyml_array,
+            root_obj=root_obj,
+            path_in_root=path_in_root,
+            epc=epc,
+        )
+    else:
+        print(f"Type {array_type_name} is not supported: function read_{snake_case(array_type_name)} not found")
+        raise Exception(f"Type {array_type_name} is not supported\n\t{energyml_array}: \n\tfunction read_{snake_case(array_type_name)} not found")
+
+
+
+def read_constant_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List[Any] +
+
+

Read a constant array ( BooleanConstantArray, DoubleConstantArray, FloatingPointConstantArray, IntegerConstantArray …) +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_constant_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a constant array ( BooleanConstantArray, DoubleConstantArray, FloatingPointConstantArray, IntegerConstantArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    # print(f"Reading constant array\n\t{energyml_array}")
+
+    value = get_object_attribute_no_verif(energyml_array, "value")
+    count = get_object_attribute_no_verif(energyml_array, "count")
+
+    # print(f"\tValue : {[value for i in range(0, count)]}")
+
+    return [value for i in range(0, count)]
+
+
+
+def read_external_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List[Any] +
+
+

Read an external array (BooleanExternalArray, BooleanHdf5Array, DoubleHdf5Array, IntegerHdf5Array, StringExternalArray …) +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_external_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read an external array (BooleanExternalArray, BooleanHdf5Array, DoubleHdf5Array, IntegerHdf5Array, StringExternalArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    hdf5_path = get_hdf5_path_from_external_path(
+                external_path_obj=energyml_array,
+                path_in_root=path_in_root,
+                root_obj=root_obj,
+                epc=epc,
+    )
+    h5_reader = HDF5FileReader()
+    path_in_external = get_hdf_reference(energyml_array)[0]
+
+    result_array = h5_reader.read_array(hdf5_path, path_in_external)
+
+    if path_in_root.lower().endswith("points") and len(result_array) > 0 and len(result_array[0]) == 3:
+        crs = get_crs_obj(
+            context_obj=energyml_array,
+            path_in_root=path_in_root,
+            root_obj=root_obj,
+            epc=epc,
+        )
+        zincreasing_downward = is_z_reversed(crs)
+
+        if zincreasing_downward:
+            result_array = list(map(lambda p: [p[0], p[1], -p[2]], result_array))
+
+    return result_array
+
+
+
+def read_grid2d_patch(patch: Any, grid2d: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List +
+
+
+
+ +Expand source code + +
def read_grid2d_patch(
+        patch: Any,
+        grid2d: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List:
+    points_path, points_obj = search_attribute_matching_name_with_path(patch, "Geometry.Points")[0]
+
+    return read_array(
+        energyml_array=points_obj,
+        root_obj=grid2d,
+        path_in_root=path_in_root + points_path,
+        epc=epc,
+    )
+
+
+
+def read_int_double_lattice_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) +
+
+

Read DoubleLatticeArray or IntegerLatticeArray. +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_int_double_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read DoubleLatticeArray or IntegerLatticeArray.
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    start_value = get_object_attribute_no_verif(energyml_array, "start_value")
+    offset = get_object_attribute_no_verif(energyml_array, "offset")
+
+    result = []
+
+    # if len(offset) == 1:
+    #     pass
+    # elif len(offset) == 2:
+    #     pass
+    # else:
+    raise Exception(f"{type(energyml_array)} read with an offset of length {len(offset)} is not supported")
+
+    # return result
+
+
+
+def read_jagged_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List[Any] +
+
+

Read a jagged array +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_jagged_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a jagged array
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    elements = read_array(
+        energyml_array=get_object_attribute_no_verif(energyml_array, "elements"),
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".elements",
+        epc=epc,
+    )
+    cumulative_length = read_array(
+        energyml_array=read_array(get_object_attribute_no_verif(energyml_array, "cumulative_length")),
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".cumulative_length",
+        epc=epc,
+    )
+
+    res = []
+    previous = 0
+    for cl in cumulative_length:
+        res.append(elements[previous: cl])
+        previous = cl
+    return res
+
+
+
+def read_point3d_from_representation_lattice_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) +
+
+

Read a Point3DFromRepresentationLatticeArray.

+

Note: Only works for Grid2DRepresentation.

+

:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_point3d_from_representation_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read a Point3DFromRepresentationLatticeArray.
+
+    Note: Only works for Grid2DRepresentation.
+
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    supporting_rep_identifier = get_obj_identifier(get_object_attribute_no_verif(energyml_array, "supporting_representation"))
+    print(f"energyml_array : {energyml_array}\n\t{supporting_rep_identifier}")
+    supporting_rep = epc.get_object_by_identifier(supporting_rep_identifier)
+
+    # TODO chercher un pattern \.*patch\.*.[d]+ pour trouver le numero du patch dans le path_in_root puis lire le patch
+    # print(f"path_in_root {path_in_root}")
+
+    result = []
+    if "grid2d" in str(type(supporting_rep)).lower():
+        patch_path, patch = search_attribute_matching_name_with_path(supporting_rep, "Grid2dPatch")[0]
+        points = read_grid2d_patch(
+            patch=patch,
+            grid2d=supporting_rep,
+            path_in_root=patch_path,
+            epc=epc,
+        )
+        # TODO: take the points by there indices from the NodeIndicesOnSupportingRepresentation
+        result = points
+
+    else:
+        raise Exception(f"Not supported type {type(energyml_array)} for object {type(root_obj)}")
+    # pour trouver les infos qu'il faut
+    return result
+
+
+
+def read_point3d_lattice_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List +
+
+

Read a Point3DLatticeArray.

+

Note: If a CRS is found and its 'ZIncreasingDownward' is set to true or its

+

:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_point3d_lattice_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List:
+    """
+    Read a Point3DLatticeArray.
+
+    Note: If a CRS is found and its 'ZIncreasingDownward' is set to true or its
+
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    result = []
+    origin = _point_as_array(get_object_attribute_no_verif(energyml_array, "origin"))
+    offset = get_object_attribute_no_verif(energyml_array, "offset")
+
+    if len(offset) == 2:
+        slowest = offset[0]
+        fastest = offset[1]
+
+        crs_sa_count = search_attribute_in_upper_matching_name(
+            obj=energyml_array,
+            name_rgx="SlowestAxisCount",
+            root_obj=root_obj,
+            current_path=path_in_root,
+        )
+
+        crs_fa_count = search_attribute_in_upper_matching_name(
+            obj=energyml_array,
+            name_rgx="FastestAxisCount",
+            root_obj=root_obj,
+            current_path=path_in_root,
+        )
+
+        crs = get_crs_obj(
+            context_obj=energyml_array,
+            path_in_root=path_in_root,
+            root_obj=root_obj,
+            epc=epc,
+        )
+        zincreasing_downward = is_z_reversed(crs)
+
+        slowest_vec = _point_as_array(get_object_attribute_no_verif(slowest, "offset"))
+        slowest_spacing = read_array(get_object_attribute_no_verif(slowest, "spacing"))
+        slowest_table = list(map(lambda x: prod_n_tab(x, slowest_vec), slowest_spacing))
+
+        fastest_vec = _point_as_array(get_object_attribute_no_verif(fastest, "offset"))
+        fastest_spacing = read_array(get_object_attribute_no_verif(fastest, "spacing"))
+        fastest_table = list(map(lambda x: prod_n_tab(x, fastest_vec), fastest_spacing))
+
+        slowest_size = len(slowest_table)
+        fastest_size = len(fastest_table)
+
+        if len(crs_sa_count) > 0 and len(crs_fa_count) and crs_sa_count[0] == fastest_size:
+            print("reversing order")
+            # if offset were given in the wrong order
+            tmp_table = slowest_table
+            slowest_table = fastest_table
+            fastest_table = tmp_table
+
+            tmp_size = slowest_size
+            slowest_size = fastest_size
+            fastest_size = tmp_size
+
+        for i in range(slowest_size):
+            for j in range(fastest_size):
+                previous_value = origin
+                # to avoid a sum of the parts of the array at each iteration, I take the previous value in the same line
+                # number i and add the fastest_table[j] value
+
+                if j > 0:
+                    if i > 0:
+                        line_idx = i * fastest_size  # numero de ligne
+                        previous_value = result[line_idx + j - 1]
+                    else:
+                        previous_value = result[j - 1]
+                    if zincreasing_downward:
+                        result.append(sum_lists(previous_value, slowest_table[i - 1]))
+                    else:
+                        result.append(sum_lists(previous_value, fastest_table[j - 1]))
+                else:
+                    if i > 0:
+                        prev_line_idx = (i - 1) * fastest_size  # numero de ligne precedent
+                        previous_value = result[prev_line_idx]
+                        if zincreasing_downward:
+                            result.append(sum_lists(previous_value, fastest_table[j - 1]))
+                        else:
+                            result.append(sum_lists(previous_value, slowest_table[i - 1]))
+                    else:
+                        result.append(previous_value)
+    else:
+        raise Exception(f"{type(energyml_array)} read with an offset of length {len(offset)} is not supported")
+
+    return result
+
+
+
+def read_point3d_zvalue_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) +
+
+

Read a Point3D2ValueArray +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_point3d_zvalue_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+):
+    """
+    Read a Point3D2ValueArray
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    supporting_geometry = get_object_attribute_no_verif(energyml_array, "supporting_geometry")
+    sup_geom_array = read_array(
+        energyml_array=supporting_geometry,
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".SupportingGeometry",
+        epc=epc,
+    )
+
+    zvalues = get_object_attribute_no_verif(energyml_array, "zvalues")
+    zvalues_array = flatten_concatenation(read_array(
+        energyml_array=zvalues,
+        root_obj=root_obj,
+        path_in_root=path_in_root + ".ZValues",
+        epc=epc,
+    ))
+
+    count = 0
+
+    for i in range(len(sup_geom_array)):
+        try:
+            sup_geom_array[i][2] = zvalues_array[i]
+        except Exception as e:
+            if count == 0:
+                print(e, f": {i} is out of bound of {len(zvalues_array)}")
+                count = count + 1
+
+    return sup_geom_array
+
+
+
+def read_xml_array(energyml_array: Any, root_obj: Optional[Any] = None, path_in_root: Optional[str] = None, epc: Optional[Epc] = None) ‑> List[Any] +
+
+

Read a xml array ( BooleanXmlArray, FloatingPointXmlArray, IntegerXmlArray, StringXmlArray …) +:param energyml_array: +:param root_obj: +:param path_in_root: +:param epc: +:return:

+
+ +Expand source code + +
def read_xml_array(
+        energyml_array: Any,
+        root_obj: Optional[Any] = None,
+        path_in_root: Optional[str] = None,
+        epc: Optional[Epc] = None
+) -> List[Any]:
+    """
+    Read a xml array ( BooleanXmlArray, FloatingPointXmlArray, IntegerXmlArray, StringXmlArray ...)
+    :param energyml_array:
+    :param root_obj:
+    :param path_in_root:
+    :param epc:
+    :return:
+    """
+    values = get_object_attribute_no_verif(energyml_array, "values")
+    # count = get_object_attribute_no_verif(energyml_array, "count_per_value")
+    return values
+
+
+
+def sum_lists(l1: List, l2: List) +
+
+

Sums 2 lists values.

+

Example

+

[1,1,1] and [2,2,3,6] gives : [3,3,4,6]

+

:param l1: +:param l2: +:return:

+
+ +Expand source code + +
def sum_lists(l1: List, l2: List):
+    """
+    Sums 2 lists values.
+
+    Example:
+        [1,1,1] and [2,2,3,6] gives : [3,3,4,6]
+
+    :param l1:
+    :param l2:
+    :return:
+    """
+    return [l1[i] + l2[i] for i in range(min(len(l1), len(l2)))]+max(l1, l2, key=len)[min(len(l1), len(l2)):]
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/data/index.html b/energyml-utils/docs/src/energyml/utils/data/index.html new file mode 100644 index 0000000..1fb08b3 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/data/index.html @@ -0,0 +1,91 @@ + + + + + + +src.energyml.utils.data API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.data

+
+
+

The data module.

+

Contains functions to help the read of specific entities like Grid2DRepresentation, TriangulatedSetRepresentation etc. +It also contains functions to export data into OFF/OBJ format.

+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+"""
+The data module.
+
+Contains functions to help the read of specific entities like Grid2DRepresentation, TriangulatedSetRepresentation etc.
+It also contains functions to export data into OFF/OBJ format.
+"""
+
+
+
+

Sub-modules

+
+
src.energyml.utils.data.hdf
+
+
+
+
src.energyml.utils.data.helper
+
+
+
+
src.energyml.utils.data.mesh
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/data/mesh.html b/energyml-utils/docs/src/energyml/utils/data/mesh.html new file mode 100644 index 0000000..6ae6e4b --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/data/mesh.html @@ -0,0 +1,1463 @@ + + + + + + +src.energyml.utils.data.mesh API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.data.mesh

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import inspect
+import re
+import sys
+from dataclasses import dataclass, field
+from io import BytesIO
+from typing import List, Optional, Any, Callable
+
+from .hdf import get_hdf_reference_with_path, \
+    get_hdf5_path_from_external_path, HDF5FileReader, get_crs_obj
+from .helper import read_array, read_grid2d_patch, is_z_reversed
+from ..epc import Epc, get_obj_identifier
+from ..introspection import search_attribute_matching_name, \
+    search_attribute_matching_type_with_path, \
+    search_attribute_matching_name_with_path, snake_case
+
+_FILE_HEADER: bytes = b"# file exported by energyml-utils python module (Geosiris)\n"
+
+Point = list[float]
+
+
+@dataclass
+class AbstractMesh:
+    energyml_object: Any = field(
+        default=None
+    )
+
+    crs_object: Any = field(
+        default=None
+    )
+
+    point_list: List[Point] = field(
+        default_factory=list,
+    )
+
+    identifier: str = field(
+        default=None,
+    )
+
+    def export_off(self, out: BytesIO) -> None:
+        pass
+
+    def get_nb_edges(self) -> int:
+        return 0
+
+    def get_nb_faces(self) -> int:
+        return 0
+
+    def get_indices(self) -> List[List[int]]:
+        return []
+
+
+@dataclass
+class PointSetMesh(AbstractMesh):
+    pass
+
+
+@dataclass
+class PolylineSetMesh(AbstractMesh):
+    line_indices: List[List[int]] = field(
+        default_factory=list,
+    )
+
+    def get_nb_edges(self) -> int:
+        return sum(list(map(lambda li: len(li) - 1, self.line_indices)))
+
+    def get_nb_faces(self) -> int:
+        return 0
+
+    def get_indices(self) -> List[List[int]]:
+        return self.line_indices
+
+
+@dataclass
+class SurfaceMesh(AbstractMesh):
+    faces_indices: List[List[int]] = field(
+        default_factory=list,
+    )
+
+    def get_nb_edges(self) -> int:
+        return sum(list(map(lambda li: len(li) - 1, self.faces_indices)))
+
+    def get_nb_faces(self) -> int:
+        return len(self.faces_indices)
+
+    def get_indices(self) -> List[List[int]]:
+        return self.faces_indices
+
+
+def get_mesh_reader_function(mesh_type_name: str) -> Optional[Callable]:
+    """
+    Returns the name of the potential appropriate function to read an object with type is named mesh_type_name
+    :param mesh_type_name: the initial type name
+    :return:
+    """
+    for name, obj in inspect.getmembers(sys.modules[__name__]):
+        if name == f"read_{snake_case(mesh_type_name)}":
+            return obj
+    return None
+
+
+def _mesh_name_mapping(array_type_name: str) -> str:
+    """
+    Transform the type name to match existing reader function
+    :param array_type_name:
+    :return:
+    """
+    array_type_name = array_type_name.replace("3D", "3d").replace("2D", "2d")
+    array_type_name = re.sub("^[Oo]bj([A-Z])", r"\1", array_type_name)
+    return array_type_name
+
+
+def read_mesh_object(
+        energyml_object: Any,
+        epc: Optional[Epc] = None
+) -> List[AbstractMesh]:
+    """
+    Read and "meshable" object. If :param:`energyml_object` is not supported, an exception will be raised.
+    :param energyml_object:
+    :param epc:
+    :return:
+    """
+    if isinstance(energyml_object, list):
+        return energyml_object
+    array_type_name = _mesh_name_mapping(type(energyml_object).__name__)
+
+    reader_func = get_mesh_reader_function(array_type_name)
+    if reader_func is not None:
+        return reader_func(
+            energyml_object=energyml_object,
+            epc=epc,
+        )
+    else:
+        print(f"Type {array_type_name} is not supported: function read_{snake_case(array_type_name)} not found")
+        raise Exception(f"Type {array_type_name} is not supported\n\t{energyml_object}: \n\tfunction read_{snake_case(array_type_name)} not found")
+
+
+def read_point_set_representation(energyml_object: Any, epc: Epc) -> List[PointSetMesh]:
+    # pt_geoms = search_attribute_matching_type(point_set, "AbstractGeometry")
+    h5_reader = HDF5FileReader()
+
+    meshes = []
+    for refer_path, refer_value in get_hdf_reference_with_path(energyml_object):
+        try:
+            hdf5_path = get_hdf5_path_from_external_path(
+                external_path_obj=refer_value,
+                path_in_root=refer_path,
+                root_obj=energyml_object,
+                epc=epc,
+            )
+            crs = get_crs_obj(
+                context_obj=refer_value,
+                path_in_root=refer_path,
+                root_obj=energyml_object,
+                epc=epc,
+            )
+            if hdf5_path is not None:
+                print(f"Reading h5 file : {hdf5_path}")
+                meshes.append(PointSetMesh(
+                    identifier=refer_value,
+                    energyml_object=energyml_object,
+                    crs_object=crs,
+                    point_list=h5_reader.read_array(hdf5_path, refer_value)
+                ))
+        except Exception as e:
+            print(f"Error with path {refer_path} -- {energyml_object}")
+            raise e
+    return meshes
+
+
+def read_polyline_set_representation(energyml_object: Any, epc: Epc) -> List[PointSetMesh]:
+    # pt_geoms = search_attribute_matching_type(point_set, "AbstractGeometry")
+    h5_reader = HDF5FileReader()
+
+    meshes = []
+
+    patch_idx = 0
+    for path_path_in_obj, patch in search_attribute_matching_name_with_path(energyml_object, "LinePatch"):
+        print(f"patch {patch}")
+        geometry_path_in_obj, geometry = search_attribute_matching_name_with_path(patch, "geometry")[0]
+        node_count_per_poly_path_in_obj, node_count_per_poly = \
+        search_attribute_matching_name_with_path(patch, "NodeCountPerPolyline")[0]
+        points_ext_array = search_attribute_matching_type_with_path(geometry, "ExternalDataArrayPart|Hdf5Dataset")
+        node_count_ext_array = search_attribute_matching_type_with_path(node_count_per_poly,
+                                                                        "ExternalDataArrayPart|Hdf5Dataset")
+
+        if len(points_ext_array) > 0:
+            point_per_elt = []
+            point_indices = []
+            crs = None
+
+            # Reading points
+            for patch_part_path, patchPart_value in points_ext_array:
+                patch_part_full_path_in_obj = path_path_in_obj + geometry_path_in_obj + patch_part_path
+                for refer_path, refer_value in get_hdf_reference_with_path(patchPart_value):
+                    print(f"refer_path {patch_part_full_path_in_obj}{refer_path} refer_value{refer_value} ")
+                    hdf5_path = get_hdf5_path_from_external_path(
+                        external_path_obj=refer_value,
+                        path_in_root=patch_part_full_path_in_obj + refer_path,
+                        root_obj=energyml_object,
+                        epc=epc,
+                    )
+                    crs = get_crs_obj(
+                        context_obj=refer_value,
+                        path_in_root=patch_part_full_path_in_obj + refer_path,
+                        root_obj=energyml_object,
+                        epc=epc,
+                    )
+                    if hdf5_path is not None:
+                        print(f"Reading h5 file : {hdf5_path}")
+                        point_per_elt = point_per_elt + h5_reader.read_array(hdf5_path, refer_value)
+
+            # Reading polyline indices
+            # for patch_part_path, patchPart_value in node_count_ext_array:
+            #     patch_part_full_path_in_obj = path_path_in_obj + node_count_per_poly_path_in_obj + patch_part_path
+            #     for refer_path, refer_value in get_hdf_reference_with_path(patchPart_value):
+            #         print(f"refer_path: {patch_part_full_path_in_obj}{refer_path} refer_value: {refer_value} ")
+            #         hdf5_path = get_hdf5_path_from_external_path(
+            #                     external_path_obj=refer_value,
+            #                     path_in_root=patch_part_full_path_in_obj + refer_path,
+            #                     root_obj=energyml_object,
+            #                     epc=epc,
+            #         )
+            #         if hdf5_path is not None:
+            #             node_counts_list = h5_reader.read_array(hdf5_path, refer_value)
+            #             idx = 0
+            #             for nb_node in node_counts_list:
+            #                 point_indices.append([x for x in range(idx, idx + nb_node)])
+            #                 idx = idx + nb_node
+
+            node_counts_list = read_array(
+                energyml_array=node_count_per_poly,
+                root_obj=energyml_object,
+                path_in_root=path_path_in_obj + node_count_per_poly_path_in_obj,
+                epc=epc,
+            )
+            idx = 0
+            for nb_node in node_counts_list:
+                point_indices.append([x for x in range(idx, idx + nb_node)])
+                idx = idx + nb_node
+
+            if len(point_per_elt) > 0:
+                # poly_idx = 0
+                # for single_poly_indices in point_indices:
+                meshes.append(PolylineSetMesh(
+                    # identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}_poly{poly_idx}",
+                    identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+                    energyml_object=energyml_object,
+                    crs_object=crs,
+                    point_list=point_per_elt,
+                    line_indices=point_indices
+                ))
+                # poly_idx = poly_idx + 1
+        patch_idx = patch_idx + 1
+
+    return meshes
+
+
+def read_grid2d_representation(energyml_object: Any, epc: Epc, keep_holes=False) -> List[SurfaceMesh]:
+    # h5_reader = HDF5FileReader()
+    meshes = []
+
+    patch_idx = 0
+    for patch_path, patch in search_attribute_matching_name_with_path(energyml_object, "Grid2dPatch"):
+        crs = get_crs_obj(
+            context_obj=patch,
+            path_in_root=patch_path,
+            root_obj=energyml_object,
+            epc=epc,
+        )
+
+        reverse_z_values = is_z_reversed(crs)
+
+        points = read_grid2d_patch(
+            patch=patch,
+            grid2d=energyml_object,
+            path_in_root=patch_path,
+            epc=epc,
+        )
+
+        fa_count = search_attribute_matching_name(patch, "FastestAxisCount")
+        if fa_count is None:
+            fa_count = search_attribute_matching_name(energyml_object, "FastestAxisCount")
+
+        sa_count = search_attribute_matching_name(patch, "SlowestAxisCount")
+        if sa_count is None:
+            sa_count = search_attribute_matching_name(energyml_object, "SlowestAxisCount")
+
+        fa_count = fa_count[0]
+        sa_count = sa_count[0]
+
+        print(f"sa_count {sa_count} fa_count {fa_count}")
+
+        points_no_nan = []
+
+        indice_to_final_indice = {}
+        if keep_holes:
+            for i in range(len(points)):
+                p = points[i]
+                if p[2] != p[2]:  # a NaN
+                    points[i][2] = 0
+                elif reverse_z_values:
+                    points[i][2] = - points[i][2]
+        else:
+            for i in range(len(points)):
+                p = points[i]
+                if p[2] == p[2]:  # not a NaN
+                    if reverse_z_values:
+                        points[i][2] = - points[i][2]
+                    indice_to_final_indice[i] = len(points_no_nan)
+                    points_no_nan.append(p)
+
+        indices = []
+
+        while sa_count*fa_count > len(points):
+            sa_count = sa_count - 1
+            fa_count = fa_count - 1
+
+        while sa_count*fa_count < len(points):
+            sa_count = sa_count + 1
+            fa_count = fa_count + 1
+
+        print(f"sa_count {sa_count} fa_count {fa_count} : {sa_count*fa_count} - {len(points)} ")
+
+        for sa in range(sa_count-1):
+            for fa in range(fa_count-1):
+                line = sa * fa_count
+                if sa+1 == int(sa_count / 2) and fa == int(fa_count / 2):
+                    print(
+                        "\n\t", (line + fa), " : ", (line + fa) in indice_to_final_indice,
+                        "\n\t", (line + fa + 1), " : ", (line + fa + 1) in indice_to_final_indice,
+                        "\n\t", (line + fa_count + fa + 1), " : ", (line + fa_count + fa + 1) in indice_to_final_indice,
+                        "\n\t", (line + fa_count + fa), " : ", (line + fa_count + fa) in indice_to_final_indice,
+                    )
+                if keep_holes:
+                    indices.append(
+                        [
+                            line + fa,
+                            line + fa + 1,
+                            line + fa_count + fa + 1,
+                            line + fa_count + fa,
+                        ]
+                    )
+                elif (
+                    (line + fa) in indice_to_final_indice
+                    and (line + fa + 1) in indice_to_final_indice
+                    and (line + fa_count + fa + 1) in indice_to_final_indice
+                    and (line + fa_count + fa) in indice_to_final_indice
+                ):
+                    indices.append(
+                        [
+                            indice_to_final_indice[line + fa],
+                            indice_to_final_indice[line + fa + 1],
+                            indice_to_final_indice[line + fa_count + fa + 1],
+                            indice_to_final_indice[line + fa_count + fa],
+                        ]
+                    )
+        # print(indices)
+        meshes.append(SurfaceMesh(
+            identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+            energyml_object=energyml_object,
+            crs_object=None,
+            point_list=points if keep_holes else points_no_nan,
+            faces_indices=indices
+        ))
+        patch_idx = patch_idx + 1
+
+    return meshes
+
+
+def read_triangulated_set_representation(energyml_object: Any, epc: Epc) -> List[SurfaceMesh]:
+    meshes = []
+
+    point_offset = 0
+    patch_idx = 0
+    for patch_path, patch in search_attribute_matching_name_with_path(energyml_object, "\\.*Patch"):
+        crs = get_crs_obj(
+            context_obj=patch,
+            path_in_root=patch_path,
+            root_obj=energyml_object,
+            epc=epc,
+        )
+
+        point_list: List[Point] = []
+        for point_path, point_obj in search_attribute_matching_name_with_path(patch, "Geometry.Points"):
+            point_list = point_list + read_array(
+                energyml_array=point_obj,
+                root_obj=energyml_object,
+                path_in_root=patch_path + point_path,
+                epc=epc,
+            )
+
+        triangles_list: List[List[int]] = []
+        for triangles_path, triangles_obj in search_attribute_matching_name_with_path(patch, "Triangles"):
+            triangles_list = triangles_list + read_array(
+                energyml_array=triangles_obj,
+                root_obj=energyml_object,
+                path_in_root=patch_path + triangles_path,
+                epc=epc,
+            )
+        triangles_list = list(map(lambda tr: [ti - point_offset for ti in tr], triangles_list))
+        meshes.append(SurfaceMesh(
+            identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+            energyml_object=energyml_object,
+            crs_object=crs,
+            point_list=point_list,
+            faces_indices=triangles_list
+        ))
+
+        point_offset = point_offset + len(point_list)
+
+    return meshes
+
+
+# MESH FILES
+
+
+def export_off(mesh_list: List[AbstractMesh], out: BytesIO):
+    """
+    Export an :class:`AbstractMesh` into off format.
+    :param mesh_list:
+    :param out:
+    :return:
+    """
+    nb_points = sum(list(map(lambda m: len(m.point_list), mesh_list)))
+    nb_edges = sum(list(map(lambda m: m.get_nb_edges(), mesh_list)))
+    nb_faces = sum(list(map(lambda m: m.get_nb_faces(), mesh_list)))
+
+    out.write(b"OFF\n")
+    out.write(_FILE_HEADER)
+    out.write(f"{nb_points} {nb_faces} {nb_edges}\n".encode('utf-8'))
+
+    points_io = BytesIO()
+    faces_io = BytesIO()
+
+    point_offset = 0
+    for m in mesh_list:
+        export_off_part(
+            off_point_part=points_io,
+            off_face_part=faces_io,
+            points=m.point_list,
+            indices=m.get_indices(),
+            point_offset=point_offset,
+            colors=[],
+        )
+        point_offset = point_offset + len(m.point_list)
+
+    out.write(points_io.getbuffer())
+    out.write(faces_io.getbuffer())
+
+
+def export_off_part(
+        off_point_part: BytesIO,
+        off_face_part: BytesIO,
+        points: List[List[float]],
+        indices: List[List[int]],
+        point_offset: Optional[int] = 0,
+        colors: Optional[List[List[int]]] = None
+) -> None:
+    for p in points:
+        for pi in p:
+            off_point_part.write(f"{pi} ".encode('utf-8'))
+        off_point_part.write(b"\n")
+
+    cpt = 0
+    for face in indices:
+        if len(face) > 1:
+            off_face_part.write(f"{len(face)} ".encode('utf-8'))
+            for pi in face:
+                off_face_part.write(f"{pi + point_offset} ".encode('utf-8'))
+
+            if colors is not None and len(colors) > cpt and colors[cpt] is not None and len(colors[cpt]) > 0:
+                for col in colors[cpt]:
+                    off_face_part.write(f"{col} ".encode('utf-8'))
+
+            off_face_part.write(b"\n")
+
+
+def export_obj(mesh_list: List[AbstractMesh], out: BytesIO, obj_name: Optional[str] = None):
+    """
+    Export an :class:`AbstractMesh` into obj format.
+
+    Each AbstractMesh from the list :param:`mesh_list` will be placed into its own group.
+    :param mesh_list:
+    :param out:
+    :param obj_name:
+    :return:
+    """
+    out.write(f"# Generated by energyml-utils a Geosiris python module\n\n".encode('utf-8'))
+
+    if obj_name is not None:
+        out.write(f"o {obj_name}\n\n".encode('utf-8'))
+
+    point_offset = 0
+    for m in mesh_list:
+        out.write(f"g {m.identifier}\n\n".encode('utf-8'))
+        _export_obj_elt(
+            off_point_part=out,
+            off_face_part=out,
+            points=m.point_list,
+            indices=m.get_indices(),
+            point_offset=point_offset,
+            colors=[],
+            elt_letter="l" if isinstance(m, PolylineSetMesh) else "f"
+        )
+        point_offset = point_offset + len(m.point_list)
+        out.write("\n".encode('utf-8'))
+
+
+def _export_obj_elt(
+        off_point_part: BytesIO,
+        off_face_part: BytesIO,
+        points: List[List[float]],
+        indices: List[List[int]],
+        point_offset: Optional[int] = 0,
+        colors: Optional[List[List[int]]] = None,
+        elt_letter: str = "f",
+) -> None:
+    """
+
+    :param off_point_part:
+    :param off_face_part:
+    :param points:
+    :param indices:
+    :param point_offset:
+    :param colors: currently not supported
+    :param elt_letter: "l" for line and "f" for faces
+    :return:
+    """
+    offset_obj = 1  # OBJ point indices starts at 1 not 0
+    for p in points:
+        if len(p) > 0:
+            off_point_part.write(f"v {' '.join(list(map(lambda xyz: str(xyz), p)))}\n".encode('utf-8'))
+
+    # cpt = 0
+    for face in indices:
+        if len(face) > 1:
+            # off_face_part.write(f"{elt_letter} ".encode('utf-8'))
+            # for pi in face:
+            #     off_face_part.write(f"{pi + point_offset} ".encode('utf-8'))
+            off_point_part.write(
+                f"{elt_letter} {' '.join(list(map(lambda x: str(x + point_offset + offset_obj), face)))}\n".encode(
+                    'utf-8'))
+
+            # if colors is not None and len(colors) > cpt and colors[cpt] is not None and len(colors[cpt]) > 0:
+            #     for col in colors[cpt]:
+            #         off_face_part.write(f"{col} ".encode('utf-8'))
+
+            # off_face_part.write(b"\n")
+
+
+
+
+
+
+
+

Functions

+
+
+def export_obj(mesh_list: List[AbstractMesh], out: _io.BytesIO, obj_name: Optional[str] = None) +
+
+

Export an :class:AbstractMesh into obj format.

+

Each AbstractMesh from the list :param:mesh_list will be placed into its own group. +:param mesh_list: +:param out: +:param obj_name: +:return:

+
+ +Expand source code + +
def export_obj(mesh_list: List[AbstractMesh], out: BytesIO, obj_name: Optional[str] = None):
+    """
+    Export an :class:`AbstractMesh` into obj format.
+
+    Each AbstractMesh from the list :param:`mesh_list` will be placed into its own group.
+    :param mesh_list:
+    :param out:
+    :param obj_name:
+    :return:
+    """
+    out.write(f"# Generated by energyml-utils a Geosiris python module\n\n".encode('utf-8'))
+
+    if obj_name is not None:
+        out.write(f"o {obj_name}\n\n".encode('utf-8'))
+
+    point_offset = 0
+    for m in mesh_list:
+        out.write(f"g {m.identifier}\n\n".encode('utf-8'))
+        _export_obj_elt(
+            off_point_part=out,
+            off_face_part=out,
+            points=m.point_list,
+            indices=m.get_indices(),
+            point_offset=point_offset,
+            colors=[],
+            elt_letter="l" if isinstance(m, PolylineSetMesh) else "f"
+        )
+        point_offset = point_offset + len(m.point_list)
+        out.write("\n".encode('utf-8'))
+
+
+
+def export_off(mesh_list: List[AbstractMesh], out: _io.BytesIO) +
+
+

Export an :class:AbstractMesh into off format. +:param mesh_list: +:param out: +:return:

+
+ +Expand source code + +
def export_off(mesh_list: List[AbstractMesh], out: BytesIO):
+    """
+    Export an :class:`AbstractMesh` into off format.
+    :param mesh_list:
+    :param out:
+    :return:
+    """
+    nb_points = sum(list(map(lambda m: len(m.point_list), mesh_list)))
+    nb_edges = sum(list(map(lambda m: m.get_nb_edges(), mesh_list)))
+    nb_faces = sum(list(map(lambda m: m.get_nb_faces(), mesh_list)))
+
+    out.write(b"OFF\n")
+    out.write(_FILE_HEADER)
+    out.write(f"{nb_points} {nb_faces} {nb_edges}\n".encode('utf-8'))
+
+    points_io = BytesIO()
+    faces_io = BytesIO()
+
+    point_offset = 0
+    for m in mesh_list:
+        export_off_part(
+            off_point_part=points_io,
+            off_face_part=faces_io,
+            points=m.point_list,
+            indices=m.get_indices(),
+            point_offset=point_offset,
+            colors=[],
+        )
+        point_offset = point_offset + len(m.point_list)
+
+    out.write(points_io.getbuffer())
+    out.write(faces_io.getbuffer())
+
+
+
+def export_off_part(off_point_part: _io.BytesIO, off_face_part: _io.BytesIO, points: List[List[float]], indices: List[List[int]], point_offset: Optional[int] = 0, colors: Optional[List[List[int]]] = None) ‑> None +
+
+
+
+ +Expand source code + +
def export_off_part(
+        off_point_part: BytesIO,
+        off_face_part: BytesIO,
+        points: List[List[float]],
+        indices: List[List[int]],
+        point_offset: Optional[int] = 0,
+        colors: Optional[List[List[int]]] = None
+) -> None:
+    for p in points:
+        for pi in p:
+            off_point_part.write(f"{pi} ".encode('utf-8'))
+        off_point_part.write(b"\n")
+
+    cpt = 0
+    for face in indices:
+        if len(face) > 1:
+            off_face_part.write(f"{len(face)} ".encode('utf-8'))
+            for pi in face:
+                off_face_part.write(f"{pi + point_offset} ".encode('utf-8'))
+
+            if colors is not None and len(colors) > cpt and colors[cpt] is not None and len(colors[cpt]) > 0:
+                for col in colors[cpt]:
+                    off_face_part.write(f"{col} ".encode('utf-8'))
+
+            off_face_part.write(b"\n")
+
+
+
+def get_mesh_reader_function(mesh_type_name: str) ‑> Optional[Callable] +
+
+

Returns the name of the potential appropriate function to read an object with type is named mesh_type_name +:param mesh_type_name: the initial type name +:return:

+
+ +Expand source code + +
def get_mesh_reader_function(mesh_type_name: str) -> Optional[Callable]:
+    """
+    Returns the name of the potential appropriate function to read an object with type is named mesh_type_name
+    :param mesh_type_name: the initial type name
+    :return:
+    """
+    for name, obj in inspect.getmembers(sys.modules[__name__]):
+        if name == f"read_{snake_case(mesh_type_name)}":
+            return obj
+    return None
+
+
+
+def read_grid2d_representation(energyml_object: Any, epc: Epc, keep_holes=False) ‑> List[SurfaceMesh] +
+
+
+
+ +Expand source code + +
def read_grid2d_representation(energyml_object: Any, epc: Epc, keep_holes=False) -> List[SurfaceMesh]:
+    # h5_reader = HDF5FileReader()
+    meshes = []
+
+    patch_idx = 0
+    for patch_path, patch in search_attribute_matching_name_with_path(energyml_object, "Grid2dPatch"):
+        crs = get_crs_obj(
+            context_obj=patch,
+            path_in_root=patch_path,
+            root_obj=energyml_object,
+            epc=epc,
+        )
+
+        reverse_z_values = is_z_reversed(crs)
+
+        points = read_grid2d_patch(
+            patch=patch,
+            grid2d=energyml_object,
+            path_in_root=patch_path,
+            epc=epc,
+        )
+
+        fa_count = search_attribute_matching_name(patch, "FastestAxisCount")
+        if fa_count is None:
+            fa_count = search_attribute_matching_name(energyml_object, "FastestAxisCount")
+
+        sa_count = search_attribute_matching_name(patch, "SlowestAxisCount")
+        if sa_count is None:
+            sa_count = search_attribute_matching_name(energyml_object, "SlowestAxisCount")
+
+        fa_count = fa_count[0]
+        sa_count = sa_count[0]
+
+        print(f"sa_count {sa_count} fa_count {fa_count}")
+
+        points_no_nan = []
+
+        indice_to_final_indice = {}
+        if keep_holes:
+            for i in range(len(points)):
+                p = points[i]
+                if p[2] != p[2]:  # a NaN
+                    points[i][2] = 0
+                elif reverse_z_values:
+                    points[i][2] = - points[i][2]
+        else:
+            for i in range(len(points)):
+                p = points[i]
+                if p[2] == p[2]:  # not a NaN
+                    if reverse_z_values:
+                        points[i][2] = - points[i][2]
+                    indice_to_final_indice[i] = len(points_no_nan)
+                    points_no_nan.append(p)
+
+        indices = []
+
+        while sa_count*fa_count > len(points):
+            sa_count = sa_count - 1
+            fa_count = fa_count - 1
+
+        while sa_count*fa_count < len(points):
+            sa_count = sa_count + 1
+            fa_count = fa_count + 1
+
+        print(f"sa_count {sa_count} fa_count {fa_count} : {sa_count*fa_count} - {len(points)} ")
+
+        for sa in range(sa_count-1):
+            for fa in range(fa_count-1):
+                line = sa * fa_count
+                if sa+1 == int(sa_count / 2) and fa == int(fa_count / 2):
+                    print(
+                        "\n\t", (line + fa), " : ", (line + fa) in indice_to_final_indice,
+                        "\n\t", (line + fa + 1), " : ", (line + fa + 1) in indice_to_final_indice,
+                        "\n\t", (line + fa_count + fa + 1), " : ", (line + fa_count + fa + 1) in indice_to_final_indice,
+                        "\n\t", (line + fa_count + fa), " : ", (line + fa_count + fa) in indice_to_final_indice,
+                    )
+                if keep_holes:
+                    indices.append(
+                        [
+                            line + fa,
+                            line + fa + 1,
+                            line + fa_count + fa + 1,
+                            line + fa_count + fa,
+                        ]
+                    )
+                elif (
+                    (line + fa) in indice_to_final_indice
+                    and (line + fa + 1) in indice_to_final_indice
+                    and (line + fa_count + fa + 1) in indice_to_final_indice
+                    and (line + fa_count + fa) in indice_to_final_indice
+                ):
+                    indices.append(
+                        [
+                            indice_to_final_indice[line + fa],
+                            indice_to_final_indice[line + fa + 1],
+                            indice_to_final_indice[line + fa_count + fa + 1],
+                            indice_to_final_indice[line + fa_count + fa],
+                        ]
+                    )
+        # print(indices)
+        meshes.append(SurfaceMesh(
+            identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+            energyml_object=energyml_object,
+            crs_object=None,
+            point_list=points if keep_holes else points_no_nan,
+            faces_indices=indices
+        ))
+        patch_idx = patch_idx + 1
+
+    return meshes
+
+
+
+def read_mesh_object(energyml_object: Any, epc: Optional[Epc] = None) ‑> List[AbstractMesh] +
+
+

Read and "meshable" object. If :param:energyml_object is not supported, an exception will be raised. +:param energyml_object: +:param epc: +:return:

+
+ +Expand source code + +
def read_mesh_object(
+        energyml_object: Any,
+        epc: Optional[Epc] = None
+) -> List[AbstractMesh]:
+    """
+    Read and "meshable" object. If :param:`energyml_object` is not supported, an exception will be raised.
+    :param energyml_object:
+    :param epc:
+    :return:
+    """
+    if isinstance(energyml_object, list):
+        return energyml_object
+    array_type_name = _mesh_name_mapping(type(energyml_object).__name__)
+
+    reader_func = get_mesh_reader_function(array_type_name)
+    if reader_func is not None:
+        return reader_func(
+            energyml_object=energyml_object,
+            epc=epc,
+        )
+    else:
+        print(f"Type {array_type_name} is not supported: function read_{snake_case(array_type_name)} not found")
+        raise Exception(f"Type {array_type_name} is not supported\n\t{energyml_object}: \n\tfunction read_{snake_case(array_type_name)} not found")
+
+
+
+def read_point_set_representation(energyml_object: Any, epc: Epc) ‑> List[PointSetMesh] +
+
+
+
+ +Expand source code + +
def read_point_set_representation(energyml_object: Any, epc: Epc) -> List[PointSetMesh]:
+    # pt_geoms = search_attribute_matching_type(point_set, "AbstractGeometry")
+    h5_reader = HDF5FileReader()
+
+    meshes = []
+    for refer_path, refer_value in get_hdf_reference_with_path(energyml_object):
+        try:
+            hdf5_path = get_hdf5_path_from_external_path(
+                external_path_obj=refer_value,
+                path_in_root=refer_path,
+                root_obj=energyml_object,
+                epc=epc,
+            )
+            crs = get_crs_obj(
+                context_obj=refer_value,
+                path_in_root=refer_path,
+                root_obj=energyml_object,
+                epc=epc,
+            )
+            if hdf5_path is not None:
+                print(f"Reading h5 file : {hdf5_path}")
+                meshes.append(PointSetMesh(
+                    identifier=refer_value,
+                    energyml_object=energyml_object,
+                    crs_object=crs,
+                    point_list=h5_reader.read_array(hdf5_path, refer_value)
+                ))
+        except Exception as e:
+            print(f"Error with path {refer_path} -- {energyml_object}")
+            raise e
+    return meshes
+
+
+
+def read_polyline_set_representation(energyml_object: Any, epc: Epc) ‑> List[PointSetMesh] +
+
+
+
+ +Expand source code + +
def read_polyline_set_representation(energyml_object: Any, epc: Epc) -> List[PointSetMesh]:
+    # pt_geoms = search_attribute_matching_type(point_set, "AbstractGeometry")
+    h5_reader = HDF5FileReader()
+
+    meshes = []
+
+    patch_idx = 0
+    for path_path_in_obj, patch in search_attribute_matching_name_with_path(energyml_object, "LinePatch"):
+        print(f"patch {patch}")
+        geometry_path_in_obj, geometry = search_attribute_matching_name_with_path(patch, "geometry")[0]
+        node_count_per_poly_path_in_obj, node_count_per_poly = \
+        search_attribute_matching_name_with_path(patch, "NodeCountPerPolyline")[0]
+        points_ext_array = search_attribute_matching_type_with_path(geometry, "ExternalDataArrayPart|Hdf5Dataset")
+        node_count_ext_array = search_attribute_matching_type_with_path(node_count_per_poly,
+                                                                        "ExternalDataArrayPart|Hdf5Dataset")
+
+        if len(points_ext_array) > 0:
+            point_per_elt = []
+            point_indices = []
+            crs = None
+
+            # Reading points
+            for patch_part_path, patchPart_value in points_ext_array:
+                patch_part_full_path_in_obj = path_path_in_obj + geometry_path_in_obj + patch_part_path
+                for refer_path, refer_value in get_hdf_reference_with_path(patchPart_value):
+                    print(f"refer_path {patch_part_full_path_in_obj}{refer_path} refer_value{refer_value} ")
+                    hdf5_path = get_hdf5_path_from_external_path(
+                        external_path_obj=refer_value,
+                        path_in_root=patch_part_full_path_in_obj + refer_path,
+                        root_obj=energyml_object,
+                        epc=epc,
+                    )
+                    crs = get_crs_obj(
+                        context_obj=refer_value,
+                        path_in_root=patch_part_full_path_in_obj + refer_path,
+                        root_obj=energyml_object,
+                        epc=epc,
+                    )
+                    if hdf5_path is not None:
+                        print(f"Reading h5 file : {hdf5_path}")
+                        point_per_elt = point_per_elt + h5_reader.read_array(hdf5_path, refer_value)
+
+            # Reading polyline indices
+            # for patch_part_path, patchPart_value in node_count_ext_array:
+            #     patch_part_full_path_in_obj = path_path_in_obj + node_count_per_poly_path_in_obj + patch_part_path
+            #     for refer_path, refer_value in get_hdf_reference_with_path(patchPart_value):
+            #         print(f"refer_path: {patch_part_full_path_in_obj}{refer_path} refer_value: {refer_value} ")
+            #         hdf5_path = get_hdf5_path_from_external_path(
+            #                     external_path_obj=refer_value,
+            #                     path_in_root=patch_part_full_path_in_obj + refer_path,
+            #                     root_obj=energyml_object,
+            #                     epc=epc,
+            #         )
+            #         if hdf5_path is not None:
+            #             node_counts_list = h5_reader.read_array(hdf5_path, refer_value)
+            #             idx = 0
+            #             for nb_node in node_counts_list:
+            #                 point_indices.append([x for x in range(idx, idx + nb_node)])
+            #                 idx = idx + nb_node
+
+            node_counts_list = read_array(
+                energyml_array=node_count_per_poly,
+                root_obj=energyml_object,
+                path_in_root=path_path_in_obj + node_count_per_poly_path_in_obj,
+                epc=epc,
+            )
+            idx = 0
+            for nb_node in node_counts_list:
+                point_indices.append([x for x in range(idx, idx + nb_node)])
+                idx = idx + nb_node
+
+            if len(point_per_elt) > 0:
+                # poly_idx = 0
+                # for single_poly_indices in point_indices:
+                meshes.append(PolylineSetMesh(
+                    # identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}_poly{poly_idx}",
+                    identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+                    energyml_object=energyml_object,
+                    crs_object=crs,
+                    point_list=point_per_elt,
+                    line_indices=point_indices
+                ))
+                # poly_idx = poly_idx + 1
+        patch_idx = patch_idx + 1
+
+    return meshes
+
+
+
+def read_triangulated_set_representation(energyml_object: Any, epc: Epc) ‑> List[SurfaceMesh] +
+
+
+
+ +Expand source code + +
def read_triangulated_set_representation(energyml_object: Any, epc: Epc) -> List[SurfaceMesh]:
+    meshes = []
+
+    point_offset = 0
+    patch_idx = 0
+    for patch_path, patch in search_attribute_matching_name_with_path(energyml_object, "\\.*Patch"):
+        crs = get_crs_obj(
+            context_obj=patch,
+            path_in_root=patch_path,
+            root_obj=energyml_object,
+            epc=epc,
+        )
+
+        point_list: List[Point] = []
+        for point_path, point_obj in search_attribute_matching_name_with_path(patch, "Geometry.Points"):
+            point_list = point_list + read_array(
+                energyml_array=point_obj,
+                root_obj=energyml_object,
+                path_in_root=patch_path + point_path,
+                epc=epc,
+            )
+
+        triangles_list: List[List[int]] = []
+        for triangles_path, triangles_obj in search_attribute_matching_name_with_path(patch, "Triangles"):
+            triangles_list = triangles_list + read_array(
+                energyml_array=triangles_obj,
+                root_obj=energyml_object,
+                path_in_root=patch_path + triangles_path,
+                epc=epc,
+            )
+        triangles_list = list(map(lambda tr: [ti - point_offset for ti in tr], triangles_list))
+        meshes.append(SurfaceMesh(
+            identifier=f"{get_obj_identifier(energyml_object)}_patch{patch_idx}",
+            energyml_object=energyml_object,
+            crs_object=crs,
+            point_list=point_list,
+            faces_indices=triangles_list
+        ))
+
+        point_offset = point_offset + len(point_list)
+
+    return meshes
+
+
+
+
+
+

Classes

+
+
+class AbstractMesh +(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = <factory>, identifier: str = None) +
+
+

AbstractMesh(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = , identifier: str = None)

+
+ +Expand source code + +
@dataclass
+class AbstractMesh:
+    energyml_object: Any = field(
+        default=None
+    )
+
+    crs_object: Any = field(
+        default=None
+    )
+
+    point_list: List[Point] = field(
+        default_factory=list,
+    )
+
+    identifier: str = field(
+        default=None,
+    )
+
+    def export_off(self, out: BytesIO) -> None:
+        pass
+
+    def get_nb_edges(self) -> int:
+        return 0
+
+    def get_nb_faces(self) -> int:
+        return 0
+
+    def get_indices(self) -> List[List[int]]:
+        return []
+
+

Subclasses

+ +

Class variables

+
+
var crs_object : Any
+
+
+
+
var energyml_object : Any
+
+
+
+
var identifier : str
+
+
+
+
var point_list : List[list[float]]
+
+
+
+
+

Methods

+
+
+def export_off(self, out: _io.BytesIO) ‑> None +
+
+
+
+ +Expand source code + +
def export_off(self, out: BytesIO) -> None:
+    pass
+
+
+
+def get_indices(self) ‑> List[List[int]] +
+
+
+
+ +Expand source code + +
def get_indices(self) -> List[List[int]]:
+    return []
+
+
+
+def get_nb_edges(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_edges(self) -> int:
+    return 0
+
+
+
+def get_nb_faces(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_faces(self) -> int:
+    return 0
+
+
+
+
+
+class PointSetMesh +(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = <factory>, identifier: str = None) +
+
+

PointSetMesh(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = , identifier: str = None)

+
+ +Expand source code + +
@dataclass
+class PointSetMesh(AbstractMesh):
+    pass
+
+

Ancestors

+ +
+
+class PolylineSetMesh +(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = <factory>, identifier: str = None, line_indices: List[List[int]] = <factory>) +
+
+

PolylineSetMesh(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = , identifier: str = None, line_indices: List[List[int]] = )

+
+ +Expand source code + +
@dataclass
+class PolylineSetMesh(AbstractMesh):
+    line_indices: List[List[int]] = field(
+        default_factory=list,
+    )
+
+    def get_nb_edges(self) -> int:
+        return sum(list(map(lambda li: len(li) - 1, self.line_indices)))
+
+    def get_nb_faces(self) -> int:
+        return 0
+
+    def get_indices(self) -> List[List[int]]:
+        return self.line_indices
+
+

Ancestors

+ +

Class variables

+
+
var line_indices : List[List[int]]
+
+
+
+
+

Methods

+
+
+def get_indices(self) ‑> List[List[int]] +
+
+
+
+ +Expand source code + +
def get_indices(self) -> List[List[int]]:
+    return self.line_indices
+
+
+
+def get_nb_edges(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_edges(self) -> int:
+    return sum(list(map(lambda li: len(li) - 1, self.line_indices)))
+
+
+
+def get_nb_faces(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_faces(self) -> int:
+    return 0
+
+
+
+
+
+class SurfaceMesh +(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = <factory>, identifier: str = None, faces_indices: List[List[int]] = <factory>) +
+
+

SurfaceMesh(energyml_object: Any = None, crs_object: Any = None, point_list: List[list[float]] = , identifier: str = None, faces_indices: List[List[int]] = )

+
+ +Expand source code + +
@dataclass
+class SurfaceMesh(AbstractMesh):
+    faces_indices: List[List[int]] = field(
+        default_factory=list,
+    )
+
+    def get_nb_edges(self) -> int:
+        return sum(list(map(lambda li: len(li) - 1, self.faces_indices)))
+
+    def get_nb_faces(self) -> int:
+        return len(self.faces_indices)
+
+    def get_indices(self) -> List[List[int]]:
+        return self.faces_indices
+
+

Ancestors

+ +

Class variables

+
+
var faces_indices : List[List[int]]
+
+
+
+
+

Methods

+
+
+def get_indices(self) ‑> List[List[int]] +
+
+
+
+ +Expand source code + +
def get_indices(self) -> List[List[int]]:
+    return self.faces_indices
+
+
+
+def get_nb_edges(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_edges(self) -> int:
+    return sum(list(map(lambda li: len(li) - 1, self.faces_indices)))
+
+
+
+def get_nb_faces(self) ‑> int +
+
+
+
+ +Expand source code + +
def get_nb_faces(self) -> int:
+    return len(self.faces_indices)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/epc.html b/energyml-utils/docs/src/energyml/utils/epc.html new file mode 100644 index 0000000..b0204a4 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/epc.html @@ -0,0 +1,1900 @@ + + + + + + +src.energyml.utils.epc API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.epc

+
+
+

This example module shows various types of documentation available for use +with pydoc. +To generate HTML documentation for this module issue the +command:

+
pydoc -w foo
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+"""
+This example module shows various types of documentation available for use
+with pydoc.  To generate HTML documentation for this module issue the
+command:
+
+    pydoc -w foo
+
+"""
+
+import datetime
+import re
+import zipfile
+from dataclasses import dataclass, field
+from enum import Enum
+from io import BytesIO
+from typing import List, Any, Union, Dict, Callable, Optional, Tuple
+
+from energyml.opc.opc import CoreProperties, Relationships, Types, Default, Relationship, Override
+from xsdata.exceptions import ParserError
+from xsdata.formats.dataclass.models.generics import DerivedElement
+
+from .introspection import (
+    get_class_from_content_type,
+    get_obj_type, search_attribute_matching_type, get_obj_version, get_obj_uuid,
+    get_object_type_for_file_path_from_class, get_content_type_from_class, get_direct_dor_list
+)
+from .manager import get_class_pkg, get_class_pkg_version
+from .serialization import (
+    serialize_xml, read_energyml_xml_str, read_energyml_xml_bytes, read_energyml_xml_bytes_as_class
+)
+from .xml import is_energyml_content_type
+
+RELS_CONTENT_TYPE = "application/vnd.openxmlformats-package.core-properties+xml"
+RELS_FOLDER_NAME = "_rels"
+
+
+class NoCrsException(Exception):
+    pass
+
+
+@dataclass
+class ObjectNotFoundNotException(Exception):
+    obj_id: str = field(
+        default=None
+    )
+
+
+class EpcExportVersion(Enum):
+    """EPC export version."""
+    #: Classical export
+    CLASSIC = 1
+    #: Export with objet path sorted by package (eml/resqml/witsml/prodml)
+    EXPANDED = 2
+
+
+class EPCRelsRelationshipType(Enum):
+    #: The object in Target is the destination of the relationship.
+    DESTINATION_OBJECT = "destinationObject"
+    #: The current object is the source in the relationship with the target object.
+    SOURCE_OBJECT = "sourceObject"
+    #: The target object is a proxy object for an external data object (HDF5 file).
+    ML_TO_EXTERNAL_PART_PROXY = "mlToExternalPartProxy"
+    #: The current object is used as a proxy object by the target object.
+    EXTERNAL_PART_PROXY_TO_ML = "externalPartProxyToMl"
+    #: The target is a resource outside of the EPC package. Note that TargetMode should be "External"
+    #: for this relationship.
+    EXTERNAL_RESOURCE = "externalResource"
+    #: The object in Target is a media representation for the current object. As a guideline, media files
+    #: should be stored in a "media" folder in the ROOT of the package.
+    DestinationMedia = "destinationMedia"
+    #: The current object is a media representation for the object in Target.
+    SOURCE_MEDIA = "sourceMedia"
+    #: The target is part of a larger data object that has been chunked into several smaller files
+    CHUNKED_PART = "chunkedPart"
+    #: /!\ not in the norm
+    EXTENDED_CORE_PROPERTIES = "extended-core-properties"
+
+    def get_type(self) -> str:
+        match self:
+            case EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES:
+                return "http://schemas.f2i-consulting.com/package/2014/relationships/" + str(self.value)
+            case (
+            EPCRelsRelationshipType.CHUNKED_PART
+            | EPCRelsRelationshipType.DESTINATION_OBJECT
+            | EPCRelsRelationshipType.SOURCE_OBJECT
+            | EPCRelsRelationshipType.ML_TO_EXTERNAL_PART_PROXY
+            | EPCRelsRelationshipType.EXTERNAL_PART_PROXY_TO_ML
+            | EPCRelsRelationshipType.EXTERNAL_RESOURCE
+            | EPCRelsRelationshipType.DestinationMedia
+            | EPCRelsRelationshipType.SOURCE_MEDIA
+            | _
+            ):
+                return "http://schemas.energistics.org/package/2012/relationships/" + str(self.value)
+
+
+@dataclass
+class RawFile:
+    path: str = field(default="_")
+    content: BytesIO = field(default=None)
+
+
+@dataclass
+class Epc:
+    """
+    A class that represent an EPC file content
+    """
+    # content_type: List[str] = field(
+    #     default_factory=list,
+    # )
+
+    export_version: EpcExportVersion = field(
+        default=EpcExportVersion.CLASSIC
+    )
+
+    core_props: CoreProperties = field(default=None)
+
+    """ xml files refered in the [Content_Types].xml  """
+    energyml_objects: List = field(
+        default_factory=list,
+    )
+
+    """ Other files content like pdf etc """
+    raw_files: List[RawFile] = field(
+        default_factory=list,
+    )
+
+    """ A list of external files. It ca be used to link hdf5 files """
+    external_files_path: List[str] = field(
+        default_factory=list,
+    )
+
+    """ 
+    Additional rels for objects. Key is the object (same than in @energyml_objects) and value is a list of
+    RelationShip. This can be used to link an HDF5 to an ExternalPartReference in resqml 2.0.1
+    Key is a value returned by @get_obj_identifier
+    """
+    additional_rels: Dict[str, List[Relationship]] = field(
+        default_factory=lambda: {}
+    )
+
+    """
+    Epc file path. Used when loaded from a local file or for export
+    """
+    epc_file_path: Optional[str] = field(
+        default=None
+    )
+
+    def __str__(self):
+        return (
+                "EPC file (" + str(self.export_version) + ") "
+                + f"{len(self.energyml_objects)} energyml objects and {len(self.raw_files)} other files {[f.path for f in self.raw_files]}"
+                # + f"\n{[serialize_json(ar) for ar in self.additional_rels]}"
+        )
+
+    # EXPORT functions
+
+    def gen_opc_content_type(self) -> Types:
+        """
+        Generates a :class:`Types` instance and fill it with energyml objects :class:`Override` values
+        :return:
+        """
+        ct = Types()
+        rels_default = Default()
+        rels_default.content_type = RELS_CONTENT_TYPE
+        rels_default.extension = "rels"
+
+        ct.default = [rels_default]
+
+        ct.override = []
+        for e_obj in self.energyml_objects:
+            ct.override.append(Override(
+                content_type=get_content_type_from_class(type(e_obj)),
+                part_name=gen_energyml_object_path(e_obj, self.export_version),
+            ))
+
+        if self.core_props is not None:
+            ct.override.append(Override(
+                content_type=get_content_type_from_class(self.core_props),
+                part_name=gen_core_props_path(self.export_version),
+            ))
+
+        return ct
+
+    def export_file(self, path: Optional[str] = None) -> None:
+        """
+        Export the epc file. If :param:`path` is None, the epc 'self.epc_file_path' is used
+        :param path:
+        :return:
+        """
+        if path is None:
+            path = self.epc_file_path
+        epc_io = self.export_io()
+        with open(path, "wb") as f:
+            f.write(epc_io.getbuffer())
+
+    def export_io(self) -> BytesIO:
+        """
+        Export the epc file into a :class:`BytesIO` instance. The result is an 'in-memory' zip file.
+        :return:
+        """
+        zip_buffer = BytesIO()
+
+        with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
+            #  Energyml objects
+            for e_obj in self.energyml_objects:
+                e_path = gen_energyml_object_path(e_obj, self.export_version)
+                zip_info = zipfile.ZipInfo(filename=e_path, date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(e_obj)
+                zip_file.writestr(zip_info, data)
+
+            # Rels
+            for rels_path, rels in self.compute_rels().items():
+                zip_info = zipfile.ZipInfo(filename=rels_path, date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(rels)
+                zip_file.writestr(zip_info, data)
+
+            # CoreProps
+            if self.core_props is not None:
+                zip_info = zipfile.ZipInfo(filename=gen_core_props_path(self.export_version),
+                                           date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(self.core_props)
+                zip_file.writestr(zip_info, data)
+
+            # ContentType
+            zip_info = zipfile.ZipInfo(filename=get_epc_content_type_path(),
+                                       date_time=datetime.datetime.now().timetuple()[:6])
+            data = serialize_xml(self.gen_opc_content_type())
+            zip_file.writestr(zip_info, data)
+
+        return zip_buffer
+
+    def compute_rels(self) -> Dict[str, Relationships]:
+        """
+        Returns a dict containing for each objet, the rels xml file path as key and the RelationShips object as value
+        :return:
+        """
+        dor_relation = get_reverse_dor_list(self.energyml_objects)
+
+        # destObject
+        rels = {
+            obj_id: [
+                Relationship(
+                    target=gen_energyml_object_path(target_obj, self.export_version),
+                    type_value=EPCRelsRelationshipType.DESTINATION_OBJECT.get_type(),
+                    id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+                ) for target_obj in target_obj_list
+            ]
+            for obj_id, target_obj_list in dor_relation.items()
+        }
+        # sourceObject
+        for obj in self.energyml_objects:
+            obj_id = get_obj_identifier(obj)
+            if obj_id not in rels:
+                rels[obj_id] = []
+            for target_obj in get_direct_dor_list(obj):
+                rels[obj_id].append(Relationship(
+                    target=gen_energyml_object_path(target_obj, self.export_version),
+                    type_value=EPCRelsRelationshipType.SOURCE_OBJECT.get_type(),
+                    id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+                ))
+
+        map_obj_id_to_obj = {
+            get_obj_identifier(obj): obj
+            for obj in self.energyml_objects
+        }
+
+        obj_rels = {
+            gen_rels_path(energyml_object=map_obj_id_to_obj.get(obj_id), export_version=self.export_version): Relationships(
+                relationship=obj_rels + (self.additional_rels[obj_id] if obj_id in self.additional_rels else []),
+
+            )
+            for obj_id, obj_rels in rels.items()
+        }
+
+        # CoreProps
+        if self.core_props is not None:
+            obj_rels[gen_rels_path(self.core_props)] = Relationships(
+                relationship=[
+                    Relationship(
+                        target=gen_core_props_path(),
+                        type_value=EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type(),
+                        id="CoreProperties"
+                    )
+                ]
+            )
+
+        return obj_rels
+
+    # -----------
+
+    def get_object_by_uuid(self, uuid: str) -> List[Any]:
+        """
+        Search all objects with the uuid :param:`uuid`.
+        :param uuid:
+        :return:
+        """
+        return list(filter(lambda o: get_obj_uuid(o) == uuid, self.energyml_objects))
+
+    def get_object_by_identifier(self, identifier: str) -> Optional[Any]:
+        """
+        Search an object by its identifier.
+        :param identifier: given by the function :func:`get_obj_identifier`
+        :return:
+        """
+        for o in self.energyml_objects:
+            if get_obj_identifier(o) == identifier:
+                return o
+        return None
+
+    def get_epc_file_folder(self) -> Optional[str]:
+        if self.epc_file_path is not None and len(self.epc_file_path) > 0:
+            folders_and_name = re.split(r"[\\/]", self.epc_file_path)
+            if len(folders_and_name) > 1:
+                return "/".join(folders_and_name[:-1])
+            else:
+                return ""
+        return None
+
+    # Class methods
+
+    @classmethod
+    def read_file(cls, epc_file_path: str):
+        with open(epc_file_path, "rb") as f:
+            epc = cls.read_stream(BytesIO(f.read()))
+            epc.epc_file_path = epc_file_path
+            return epc
+
+    @classmethod
+    def read_stream(cls, epc_file_io: BytesIO):  # returns an Epc instance
+        """
+        :param epc_file_io:
+        :return: an :class:`EPC` instance
+        """
+        try:
+            _read_files = []
+            obj_list = []
+            raw_file_list = []
+            additional_rels = {}
+            core_props = None
+            with zipfile.ZipFile(epc_file_io, "r", zipfile.ZIP_DEFLATED) as epc_file:
+                content_type_file_name = get_epc_content_type_path()
+                content_type_info = None
+                try:
+                    content_type_info = epc_file.getinfo(content_type_file_name)
+                except KeyError:
+                    for info in epc_file.infolist():
+                        if info.filename.lower() == content_type_file_name.lower():
+                            content_type_info = info
+                            break
+
+                _read_files.append(content_type_file_name)
+
+                if content_type_info is None:
+                    print(f"No {content_type_file_name} file found")
+                else:
+                    content_type_obj: Types = read_energyml_xml_bytes(epc_file.read(content_type_file_name))
+                    path_to_obj = {}
+                    for ov in content_type_obj.override:
+                        ov_ct = ov.content_type
+                        ov_path = ov.part_name
+                        # print(ov_ct)
+                        while ov_path.startswith("/") or ov_path.startswith("\\"):
+                            ov_path = ov_path[1:]
+                        if is_energyml_content_type(ov_ct):
+                            _read_files.append(ov_path)
+                            try:
+                                ov_obj = read_energyml_xml_bytes_as_class(
+                                    epc_file.read(ov_path),
+                                    get_class_from_content_type(ov_ct)
+                                )
+                                if isinstance(ov_obj, DerivedElement):
+                                    ov_obj = ov_obj.value
+                                path_to_obj[ov_path] = ov_obj
+                                obj_list.append(ov_obj)
+                            except ParserError as e:
+                                print(f"Epc.@read_stream failed to parse file {ov_path} for content-type: {ov_ct} => {get_class_from_content_type(ov_ct)}")
+                                raise e
+                        elif get_class_from_content_type(ov_ct) == CoreProperties:
+                            _read_files.append(ov_path)
+                            core_props = read_energyml_xml_bytes_as_class(epc_file.read(ov_path), CoreProperties)
+
+                    for f_info in epc_file.infolist():
+                        if f_info.filename not in _read_files:
+                            _read_files.append(f_info.filename)
+                            if not f_info.filename.lower().endswith(".rels"):
+                                try:
+                                    raw_file_list.append(
+                                        RawFile(
+                                            path=f_info.filename,
+                                            content=BytesIO(epc_file.read(f_info.filename)),
+                                        )
+                                    )
+                                except IOError as e:
+                                    print(e)
+                            else:  # rels
+                                # print(f"reading rels {f_info.filename}")
+                                rels_folder, rels_file_name = get_file_folder_and_name_from_path(f_info.filename)
+                                while rels_folder.endswith("/"):
+                                    rels_folder = rels_folder[:-1]
+                                obj_folder = rels_folder[:rels_folder.rindex("/") + 1] if "/" in rels_folder else ""
+                                obj_file_name = rels_file_name[:-5]  # removing the ".rels"
+                                rels_file: Relationships = read_energyml_xml_bytes_as_class(
+                                    epc_file.read(f_info.filename),
+                                    Relationships
+                                )
+                                obj_path = obj_folder + obj_file_name
+                                if obj_path in path_to_obj:
+                                    try:
+                                        additional_rels_key = get_obj_identifier(path_to_obj[obj_path])
+                                        for rel in rels_file.relationship:
+                                            # print(f"\t\t{rel.type_value}")
+                                            if (rel.type_value != EPCRelsRelationshipType.DESTINATION_OBJECT.get_type()
+                                                    and rel.type_value != EPCRelsRelationshipType.SOURCE_OBJECT.get_type()
+                                                    and rel.type_value != EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type()
+                                            ):  # not a computable relation
+                                                if additional_rels_key not in additional_rels:
+                                                    additional_rels[additional_rels_key] = []
+                                                additional_rels[additional_rels_key].append(rel)
+                                    except Exception as e:
+                                        print(f"Error with obj path {obj_path} {path_to_obj[obj_path]}")
+                                        raise e
+                                else:
+                                    print(f"xml file {obj_path} not found in EPC (rels is not associate to any object)")
+
+            return Epc(energyml_objects=obj_list,
+                       raw_files=raw_file_list,
+                       core_props=core_props,
+                       additional_rels=additional_rels
+                       )
+        except zipfile.BadZipFile as error:
+            print(error)
+
+        return None
+
+
+#     ______                                      __   ____                 __  _
+#    / ____/___  ___  _________ ___  ______ ___  / /  / __/_  ______  _____/ /_(_)___  ____  _____
+#   / __/ / __ \/ _ \/ ___/ __ `/ / / / __ `__ \/ /  / /_/ / / / __ \/ ___/ __/ / __ \/ __ \/ ___/
+#  / /___/ / / /  __/ /  / /_/ / /_/ / / / / / / /  / __/ /_/ / / / / /__/ /_/ / /_/ / / / (__  )
+# /_____/_/ /_/\___/_/   \__, /\__, /_/ /_/ /_/_/  /_/  \__,_/_/ /_/\___/\__/_/\____/_/ /_/____/
+#                       /____//____/
+
+
+def get_obj_identifier(obj: Any) -> str:
+    """
+    Generates an objet identifier as : 'OBJ_UUID.OBJ_VERSION'
+    If the object version is None, the result is 'OBJ_UUID.'
+    :param obj:
+    :return: str
+    """
+    obj_obj_version = get_obj_version(obj)
+    if obj_obj_version is None:
+        obj_obj_version = ""
+    obj_uuid = get_obj_uuid(obj)
+    return f"{obj_uuid}.{obj_obj_version}"
+
+
+def get_reverse_dor_list(obj_list: List[Any], key_func: Callable = get_obj_identifier) -> Dict[str, List[Any]]:
+    """
+    Compute a dict with 'OBJ_UUID.OBJ_VERSION' as Key, and list of DOR that reference it.
+    If the object version is None, key is 'OBJ_UUID.'
+    :param obj_list:
+    :param key_func: a callable to create the key of the dict from the object instance
+    :return: str
+    """
+    rels = {}
+    for obj in obj_list:
+        for dor in search_attribute_matching_type(obj, "DataObjectReference", return_self=False):
+            key = key_func(dor)
+            if key not in rels:
+                rels[key] = []
+            rels[key] = rels.get(key, []) + [obj]
+    return rels
+
+
+# PATHS
+
+
+def gen_core_props_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC):
+    return "docProps/core.xml"
+
+
+def gen_energyml_object_path(energyml_object: Union[str, Any],
+                             export_version: EpcExportVersion = EpcExportVersion.CLASSIC):
+    """
+    Generate a path to store the :param:`energyml_object` into an epc file (depending on the :param:`export_version`)
+    :param energyml_object:
+    :param export_version:
+    :return:
+    """
+    if isinstance(energyml_object, str):
+        energyml_object = read_energyml_xml_str(energyml_object)
+
+    obj_type = get_object_type_for_file_path_from_class(energyml_object.__class__)
+
+    pkg = get_class_pkg(energyml_object)
+    pkg_version = get_class_pkg_version(energyml_object)
+    object_version = get_obj_version(energyml_object)
+    uuid = get_obj_uuid(energyml_object)
+
+    # if object_version is None:
+    #     object_version = "0"
+
+    if export_version == EpcExportVersion.EXPANDED:
+        return f"namespace_{pkg}{pkg_version.replace('.', '')}/{uuid}{('/version_' + object_version) if object_version is not None else ''}/{obj_type}_{uuid}.xml"
+    else:
+        return obj_type + "_" + uuid + ".xml"
+
+
+def get_file_folder_and_name_from_path(path: str) -> Tuple[str, str]:
+    """
+    Returns a tuple (FOLDER_PATH, FILE_NAME)
+    :param path:
+    :return:
+    """
+    obj_folder = path[:path.rindex("/") + 1] if "/" in path else ""
+    obj_file_name = path[path.rindex("/") + 1:] if "/" in path else path
+    return obj_folder, obj_file_name
+
+
+def gen_rels_path(energyml_object: Any,
+                  export_version: EpcExportVersion = EpcExportVersion.CLASSIC
+                  ) -> str:
+    """
+    Generate a path to store the :param:`energyml_object` rels file into an epc file
+    (depending on the :param:`export_version`)
+    :param energyml_object:
+    :param export_version:
+    :return:
+    """
+    if isinstance(obj, CoreProperties):
+        return f"{RELS_FOLDER_NAME}/.rels"
+    else:
+        obj_path = gen_energyml_object_path(obj, export_version)
+        obj_folder, obj_file_name = get_file_folder_and_name_from_path(obj_path, )
+        return f"{obj_folder}{RELS_FOLDER_NAME}/{obj_file_name}.rels"
+
+
+def get_epc_content_type_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC) -> str:
+    """
+    Generate a path to store the "[Content_Types].xml" file into an epc file
+    (depending on the :param:`export_version`)
+    :return:
+    """
+    return "[Content_Types].xml"
+
+
+
+
+
+
+
+

Functions

+
+
+def gen_core_props_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC) +
+
+
+
+ +Expand source code + +
def gen_core_props_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC):
+    return "docProps/core.xml"
+
+
+
+def gen_energyml_object_path(energyml_object: Union[str, Any], export_version: EpcExportVersion = EpcExportVersion.CLASSIC) +
+
+

Generate a path to store the :param:energyml_object into an epc file (depending on the :param:export_version) +:param energyml_object: +:param export_version: +:return:

+
+ +Expand source code + +
def gen_energyml_object_path(energyml_object: Union[str, Any],
+                             export_version: EpcExportVersion = EpcExportVersion.CLASSIC):
+    """
+    Generate a path to store the :param:`energyml_object` into an epc file (depending on the :param:`export_version`)
+    :param energyml_object:
+    :param export_version:
+    :return:
+    """
+    if isinstance(energyml_object, str):
+        energyml_object = read_energyml_xml_str(energyml_object)
+
+    obj_type = get_object_type_for_file_path_from_class(energyml_object.__class__)
+
+    pkg = get_class_pkg(energyml_object)
+    pkg_version = get_class_pkg_version(energyml_object)
+    object_version = get_obj_version(energyml_object)
+    uuid = get_obj_uuid(energyml_object)
+
+    # if object_version is None:
+    #     object_version = "0"
+
+    if export_version == EpcExportVersion.EXPANDED:
+        return f"namespace_{pkg}{pkg_version.replace('.', '')}/{uuid}{('/version_' + object_version) if object_version is not None else ''}/{obj_type}_{uuid}.xml"
+    else:
+        return obj_type + "_" + uuid + ".xml"
+
+
+
+def gen_rels_path(energyml_object: Any, export_version: EpcExportVersion = EpcExportVersion.CLASSIC) ‑> str +
+
+

Generate a path to store the :param:energyml_object rels file into an epc file +(depending on the :param:export_version) +:param energyml_object: +:param export_version: +:return:

+
+ +Expand source code + +
def gen_rels_path(energyml_object: Any,
+                  export_version: EpcExportVersion = EpcExportVersion.CLASSIC
+                  ) -> str:
+    """
+    Generate a path to store the :param:`energyml_object` rels file into an epc file
+    (depending on the :param:`export_version`)
+    :param energyml_object:
+    :param export_version:
+    :return:
+    """
+    if isinstance(obj, CoreProperties):
+        return f"{RELS_FOLDER_NAME}/.rels"
+    else:
+        obj_path = gen_energyml_object_path(obj, export_version)
+        obj_folder, obj_file_name = get_file_folder_and_name_from_path(obj_path, )
+        return f"{obj_folder}{RELS_FOLDER_NAME}/{obj_file_name}.rels"
+
+
+
+def get_epc_content_type_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC) ‑> str +
+
+

Generate a path to store the "[Content_Types].xml" file into an epc file +(depending on the :param:export_version) +:return:

+
+ +Expand source code + +
def get_epc_content_type_path(export_version: EpcExportVersion = EpcExportVersion.CLASSIC) -> str:
+    """
+    Generate a path to store the "[Content_Types].xml" file into an epc file
+    (depending on the :param:`export_version`)
+    :return:
+    """
+    return "[Content_Types].xml"
+
+
+
+def get_file_folder_and_name_from_path(path: str) ‑> Tuple[str, str] +
+
+

Returns a tuple (FOLDER_PATH, FILE_NAME) +:param path: +:return:

+
+ +Expand source code + +
def get_file_folder_and_name_from_path(path: str) -> Tuple[str, str]:
+    """
+    Returns a tuple (FOLDER_PATH, FILE_NAME)
+    :param path:
+    :return:
+    """
+    obj_folder = path[:path.rindex("/") + 1] if "/" in path else ""
+    obj_file_name = path[path.rindex("/") + 1:] if "/" in path else path
+    return obj_folder, obj_file_name
+
+
+
+def get_obj_identifier(obj: Any) ‑> str +
+
+

Generates an objet identifier as : 'OBJ_UUID.OBJ_VERSION' +If the object version is None, the result is 'OBJ_UUID.' +:param obj: +:return: str

+
+ +Expand source code + +
def get_obj_identifier(obj: Any) -> str:
+    """
+    Generates an objet identifier as : 'OBJ_UUID.OBJ_VERSION'
+    If the object version is None, the result is 'OBJ_UUID.'
+    :param obj:
+    :return: str
+    """
+    obj_obj_version = get_obj_version(obj)
+    if obj_obj_version is None:
+        obj_obj_version = ""
+    obj_uuid = get_obj_uuid(obj)
+    return f"{obj_uuid}.{obj_obj_version}"
+
+
+
+def get_reverse_dor_list(obj_list: List[Any], key_func: Callable = <function get_obj_identifier>) ‑> Dict[str, List[Any]] +
+
+

Compute a dict with 'OBJ_UUID.OBJ_VERSION' as Key, and list of DOR that reference it. +If the object version is None, key is 'OBJ_UUID.' +:param obj_list: +:param key_func: a callable to create the key of the dict from the object instance +:return: str

+
+ +Expand source code + +
def get_reverse_dor_list(obj_list: List[Any], key_func: Callable = get_obj_identifier) -> Dict[str, List[Any]]:
+    """
+    Compute a dict with 'OBJ_UUID.OBJ_VERSION' as Key, and list of DOR that reference it.
+    If the object version is None, key is 'OBJ_UUID.'
+    :param obj_list:
+    :param key_func: a callable to create the key of the dict from the object instance
+    :return: str
+    """
+    rels = {}
+    for obj in obj_list:
+        for dor in search_attribute_matching_type(obj, "DataObjectReference", return_self=False):
+            key = key_func(dor)
+            if key not in rels:
+                rels[key] = []
+            rels[key] = rels.get(key, []) + [obj]
+    return rels
+
+
+
+
+
+

Classes

+
+
+class EPCRelsRelationshipType +(*args, **kwds) +
+
+

Create a collection of name/value pairs.

+

Example enumeration:

+
>>> class Color(Enum):
+...     RED = 1
+...     BLUE = 2
+...     GREEN = 3
+
+

Access them by:

+
    +
  • attribute access::
  • +
+
>>> Color.RED
+<Color.RED: 1>
+
+
    +
  • value lookup:
  • +
+
>>> Color(1)
+<Color.RED: 1>
+
+
    +
  • name lookup:
  • +
+
>>> Color['RED']
+<Color.RED: 1>
+
+

Enumerations can be iterated over, and know how many members they have:

+
>>> len(Color)
+3
+
+
>>> list(Color)
+[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
+
+

Methods can be added to enumerations, and members can have their own +attributes – see the documentation for details.

+
+ +Expand source code + +
class EPCRelsRelationshipType(Enum):
+    #: The object in Target is the destination of the relationship.
+    DESTINATION_OBJECT = "destinationObject"
+    #: The current object is the source in the relationship with the target object.
+    SOURCE_OBJECT = "sourceObject"
+    #: The target object is a proxy object for an external data object (HDF5 file).
+    ML_TO_EXTERNAL_PART_PROXY = "mlToExternalPartProxy"
+    #: The current object is used as a proxy object by the target object.
+    EXTERNAL_PART_PROXY_TO_ML = "externalPartProxyToMl"
+    #: The target is a resource outside of the EPC package. Note that TargetMode should be "External"
+    #: for this relationship.
+    EXTERNAL_RESOURCE = "externalResource"
+    #: The object in Target is a media representation for the current object. As a guideline, media files
+    #: should be stored in a "media" folder in the ROOT of the package.
+    DestinationMedia = "destinationMedia"
+    #: The current object is a media representation for the object in Target.
+    SOURCE_MEDIA = "sourceMedia"
+    #: The target is part of a larger data object that has been chunked into several smaller files
+    CHUNKED_PART = "chunkedPart"
+    #: /!\ not in the norm
+    EXTENDED_CORE_PROPERTIES = "extended-core-properties"
+
+    def get_type(self) -> str:
+        match self:
+            case EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES:
+                return "http://schemas.f2i-consulting.com/package/2014/relationships/" + str(self.value)
+            case (
+            EPCRelsRelationshipType.CHUNKED_PART
+            | EPCRelsRelationshipType.DESTINATION_OBJECT
+            | EPCRelsRelationshipType.SOURCE_OBJECT
+            | EPCRelsRelationshipType.ML_TO_EXTERNAL_PART_PROXY
+            | EPCRelsRelationshipType.EXTERNAL_PART_PROXY_TO_ML
+            | EPCRelsRelationshipType.EXTERNAL_RESOURCE
+            | EPCRelsRelationshipType.DestinationMedia
+            | EPCRelsRelationshipType.SOURCE_MEDIA
+            | _
+            ):
+                return "http://schemas.energistics.org/package/2012/relationships/" + str(self.value)
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var CHUNKED_PART
+
+

The target is part of a larger data object that has been chunked into several smaller files

+
+
var DESTINATION_OBJECT
+
+

The object in Target is the destination of the relationship.

+
+
var DestinationMedia
+
+

The object in Target is a media representation for the current object. As a guideline, media files +should be stored in a "media" folder in the ROOT of the package.

+
+
var EXTENDED_CORE_PROPERTIES
+
+

/!\ not in the norm

+
+
var EXTERNAL_PART_PROXY_TO_ML
+
+

The current object is used as a proxy object by the target object.

+
+
var EXTERNAL_RESOURCE
+
+

The target is a resource outside of the EPC package. Note that TargetMode should be "External" +for this relationship.

+
+
var ML_TO_EXTERNAL_PART_PROXY
+
+

The target object is a proxy object for an external data object (HDF5 file).

+
+
var SOURCE_MEDIA
+
+

The current object is a media representation for the object in Target.

+
+
var SOURCE_OBJECT
+
+

The current object is the source in the relationship with the target object.

+
+
+

Methods

+
+
+def get_type(self) ‑> str +
+
+
+
+ +Expand source code + +
def get_type(self) -> str:
+    match self:
+        case EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES:
+            return "http://schemas.f2i-consulting.com/package/2014/relationships/" + str(self.value)
+        case (
+        EPCRelsRelationshipType.CHUNKED_PART
+        | EPCRelsRelationshipType.DESTINATION_OBJECT
+        | EPCRelsRelationshipType.SOURCE_OBJECT
+        | EPCRelsRelationshipType.ML_TO_EXTERNAL_PART_PROXY
+        | EPCRelsRelationshipType.EXTERNAL_PART_PROXY_TO_ML
+        | EPCRelsRelationshipType.EXTERNAL_RESOURCE
+        | EPCRelsRelationshipType.DestinationMedia
+        | EPCRelsRelationshipType.SOURCE_MEDIA
+        | _
+        ):
+            return "http://schemas.energistics.org/package/2012/relationships/" + str(self.value)
+
+
+
+
+
+class Epc +(export_version: EpcExportVersion = EpcExportVersion.CLASSIC, core_props: energyml.opc.opc.CoreProperties = None, energyml_objects: List = <factory>, raw_files: List[RawFile] = <factory>, external_files_path: List[str] = <factory>, additional_rels: Dict[str, List[energyml.opc.opc.Relationship]] = <factory>, epc_file_path: Optional[str] = None) +
+
+

A class that represent an EPC file content

+
+ +Expand source code + +
@dataclass
+class Epc:
+    """
+    A class that represent an EPC file content
+    """
+    # content_type: List[str] = field(
+    #     default_factory=list,
+    # )
+
+    export_version: EpcExportVersion = field(
+        default=EpcExportVersion.CLASSIC
+    )
+
+    core_props: CoreProperties = field(default=None)
+
+    """ xml files refered in the [Content_Types].xml  """
+    energyml_objects: List = field(
+        default_factory=list,
+    )
+
+    """ Other files content like pdf etc """
+    raw_files: List[RawFile] = field(
+        default_factory=list,
+    )
+
+    """ A list of external files. It ca be used to link hdf5 files """
+    external_files_path: List[str] = field(
+        default_factory=list,
+    )
+
+    """ 
+    Additional rels for objects. Key is the object (same than in @energyml_objects) and value is a list of
+    RelationShip. This can be used to link an HDF5 to an ExternalPartReference in resqml 2.0.1
+    Key is a value returned by @get_obj_identifier
+    """
+    additional_rels: Dict[str, List[Relationship]] = field(
+        default_factory=lambda: {}
+    )
+
+    """
+    Epc file path. Used when loaded from a local file or for export
+    """
+    epc_file_path: Optional[str] = field(
+        default=None
+    )
+
+    def __str__(self):
+        return (
+                "EPC file (" + str(self.export_version) + ") "
+                + f"{len(self.energyml_objects)} energyml objects and {len(self.raw_files)} other files {[f.path for f in self.raw_files]}"
+                # + f"\n{[serialize_json(ar) for ar in self.additional_rels]}"
+        )
+
+    # EXPORT functions
+
+    def gen_opc_content_type(self) -> Types:
+        """
+        Generates a :class:`Types` instance and fill it with energyml objects :class:`Override` values
+        :return:
+        """
+        ct = Types()
+        rels_default = Default()
+        rels_default.content_type = RELS_CONTENT_TYPE
+        rels_default.extension = "rels"
+
+        ct.default = [rels_default]
+
+        ct.override = []
+        for e_obj in self.energyml_objects:
+            ct.override.append(Override(
+                content_type=get_content_type_from_class(type(e_obj)),
+                part_name=gen_energyml_object_path(e_obj, self.export_version),
+            ))
+
+        if self.core_props is not None:
+            ct.override.append(Override(
+                content_type=get_content_type_from_class(self.core_props),
+                part_name=gen_core_props_path(self.export_version),
+            ))
+
+        return ct
+
+    def export_file(self, path: Optional[str] = None) -> None:
+        """
+        Export the epc file. If :param:`path` is None, the epc 'self.epc_file_path' is used
+        :param path:
+        :return:
+        """
+        if path is None:
+            path = self.epc_file_path
+        epc_io = self.export_io()
+        with open(path, "wb") as f:
+            f.write(epc_io.getbuffer())
+
+    def export_io(self) -> BytesIO:
+        """
+        Export the epc file into a :class:`BytesIO` instance. The result is an 'in-memory' zip file.
+        :return:
+        """
+        zip_buffer = BytesIO()
+
+        with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
+            #  Energyml objects
+            for e_obj in self.energyml_objects:
+                e_path = gen_energyml_object_path(e_obj, self.export_version)
+                zip_info = zipfile.ZipInfo(filename=e_path, date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(e_obj)
+                zip_file.writestr(zip_info, data)
+
+            # Rels
+            for rels_path, rels in self.compute_rels().items():
+                zip_info = zipfile.ZipInfo(filename=rels_path, date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(rels)
+                zip_file.writestr(zip_info, data)
+
+            # CoreProps
+            if self.core_props is not None:
+                zip_info = zipfile.ZipInfo(filename=gen_core_props_path(self.export_version),
+                                           date_time=datetime.datetime.now().timetuple()[:6])
+                data = serialize_xml(self.core_props)
+                zip_file.writestr(zip_info, data)
+
+            # ContentType
+            zip_info = zipfile.ZipInfo(filename=get_epc_content_type_path(),
+                                       date_time=datetime.datetime.now().timetuple()[:6])
+            data = serialize_xml(self.gen_opc_content_type())
+            zip_file.writestr(zip_info, data)
+
+        return zip_buffer
+
+    def compute_rels(self) -> Dict[str, Relationships]:
+        """
+        Returns a dict containing for each objet, the rels xml file path as key and the RelationShips object as value
+        :return:
+        """
+        dor_relation = get_reverse_dor_list(self.energyml_objects)
+
+        # destObject
+        rels = {
+            obj_id: [
+                Relationship(
+                    target=gen_energyml_object_path(target_obj, self.export_version),
+                    type_value=EPCRelsRelationshipType.DESTINATION_OBJECT.get_type(),
+                    id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+                ) for target_obj in target_obj_list
+            ]
+            for obj_id, target_obj_list in dor_relation.items()
+        }
+        # sourceObject
+        for obj in self.energyml_objects:
+            obj_id = get_obj_identifier(obj)
+            if obj_id not in rels:
+                rels[obj_id] = []
+            for target_obj in get_direct_dor_list(obj):
+                rels[obj_id].append(Relationship(
+                    target=gen_energyml_object_path(target_obj, self.export_version),
+                    type_value=EPCRelsRelationshipType.SOURCE_OBJECT.get_type(),
+                    id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+                ))
+
+        map_obj_id_to_obj = {
+            get_obj_identifier(obj): obj
+            for obj in self.energyml_objects
+        }
+
+        obj_rels = {
+            gen_rels_path(energyml_object=map_obj_id_to_obj.get(obj_id), export_version=self.export_version): Relationships(
+                relationship=obj_rels + (self.additional_rels[obj_id] if obj_id in self.additional_rels else []),
+
+            )
+            for obj_id, obj_rels in rels.items()
+        }
+
+        # CoreProps
+        if self.core_props is not None:
+            obj_rels[gen_rels_path(self.core_props)] = Relationships(
+                relationship=[
+                    Relationship(
+                        target=gen_core_props_path(),
+                        type_value=EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type(),
+                        id="CoreProperties"
+                    )
+                ]
+            )
+
+        return obj_rels
+
+    # -----------
+
+    def get_object_by_uuid(self, uuid: str) -> List[Any]:
+        """
+        Search all objects with the uuid :param:`uuid`.
+        :param uuid:
+        :return:
+        """
+        return list(filter(lambda o: get_obj_uuid(o) == uuid, self.energyml_objects))
+
+    def get_object_by_identifier(self, identifier: str) -> Optional[Any]:
+        """
+        Search an object by its identifier.
+        :param identifier: given by the function :func:`get_obj_identifier`
+        :return:
+        """
+        for o in self.energyml_objects:
+            if get_obj_identifier(o) == identifier:
+                return o
+        return None
+
+    def get_epc_file_folder(self) -> Optional[str]:
+        if self.epc_file_path is not None and len(self.epc_file_path) > 0:
+            folders_and_name = re.split(r"[\\/]", self.epc_file_path)
+            if len(folders_and_name) > 1:
+                return "/".join(folders_and_name[:-1])
+            else:
+                return ""
+        return None
+
+    # Class methods
+
+    @classmethod
+    def read_file(cls, epc_file_path: str):
+        with open(epc_file_path, "rb") as f:
+            epc = cls.read_stream(BytesIO(f.read()))
+            epc.epc_file_path = epc_file_path
+            return epc
+
+    @classmethod
+    def read_stream(cls, epc_file_io: BytesIO):  # returns an Epc instance
+        """
+        :param epc_file_io:
+        :return: an :class:`EPC` instance
+        """
+        try:
+            _read_files = []
+            obj_list = []
+            raw_file_list = []
+            additional_rels = {}
+            core_props = None
+            with zipfile.ZipFile(epc_file_io, "r", zipfile.ZIP_DEFLATED) as epc_file:
+                content_type_file_name = get_epc_content_type_path()
+                content_type_info = None
+                try:
+                    content_type_info = epc_file.getinfo(content_type_file_name)
+                except KeyError:
+                    for info in epc_file.infolist():
+                        if info.filename.lower() == content_type_file_name.lower():
+                            content_type_info = info
+                            break
+
+                _read_files.append(content_type_file_name)
+
+                if content_type_info is None:
+                    print(f"No {content_type_file_name} file found")
+                else:
+                    content_type_obj: Types = read_energyml_xml_bytes(epc_file.read(content_type_file_name))
+                    path_to_obj = {}
+                    for ov in content_type_obj.override:
+                        ov_ct = ov.content_type
+                        ov_path = ov.part_name
+                        # print(ov_ct)
+                        while ov_path.startswith("/") or ov_path.startswith("\\"):
+                            ov_path = ov_path[1:]
+                        if is_energyml_content_type(ov_ct):
+                            _read_files.append(ov_path)
+                            try:
+                                ov_obj = read_energyml_xml_bytes_as_class(
+                                    epc_file.read(ov_path),
+                                    get_class_from_content_type(ov_ct)
+                                )
+                                if isinstance(ov_obj, DerivedElement):
+                                    ov_obj = ov_obj.value
+                                path_to_obj[ov_path] = ov_obj
+                                obj_list.append(ov_obj)
+                            except ParserError as e:
+                                print(f"Epc.@read_stream failed to parse file {ov_path} for content-type: {ov_ct} => {get_class_from_content_type(ov_ct)}")
+                                raise e
+                        elif get_class_from_content_type(ov_ct) == CoreProperties:
+                            _read_files.append(ov_path)
+                            core_props = read_energyml_xml_bytes_as_class(epc_file.read(ov_path), CoreProperties)
+
+                    for f_info in epc_file.infolist():
+                        if f_info.filename not in _read_files:
+                            _read_files.append(f_info.filename)
+                            if not f_info.filename.lower().endswith(".rels"):
+                                try:
+                                    raw_file_list.append(
+                                        RawFile(
+                                            path=f_info.filename,
+                                            content=BytesIO(epc_file.read(f_info.filename)),
+                                        )
+                                    )
+                                except IOError as e:
+                                    print(e)
+                            else:  # rels
+                                # print(f"reading rels {f_info.filename}")
+                                rels_folder, rels_file_name = get_file_folder_and_name_from_path(f_info.filename)
+                                while rels_folder.endswith("/"):
+                                    rels_folder = rels_folder[:-1]
+                                obj_folder = rels_folder[:rels_folder.rindex("/") + 1] if "/" in rels_folder else ""
+                                obj_file_name = rels_file_name[:-5]  # removing the ".rels"
+                                rels_file: Relationships = read_energyml_xml_bytes_as_class(
+                                    epc_file.read(f_info.filename),
+                                    Relationships
+                                )
+                                obj_path = obj_folder + obj_file_name
+                                if obj_path in path_to_obj:
+                                    try:
+                                        additional_rels_key = get_obj_identifier(path_to_obj[obj_path])
+                                        for rel in rels_file.relationship:
+                                            # print(f"\t\t{rel.type_value}")
+                                            if (rel.type_value != EPCRelsRelationshipType.DESTINATION_OBJECT.get_type()
+                                                    and rel.type_value != EPCRelsRelationshipType.SOURCE_OBJECT.get_type()
+                                                    and rel.type_value != EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type()
+                                            ):  # not a computable relation
+                                                if additional_rels_key not in additional_rels:
+                                                    additional_rels[additional_rels_key] = []
+                                                additional_rels[additional_rels_key].append(rel)
+                                    except Exception as e:
+                                        print(f"Error with obj path {obj_path} {path_to_obj[obj_path]}")
+                                        raise e
+                                else:
+                                    print(f"xml file {obj_path} not found in EPC (rels is not associate to any object)")
+
+            return Epc(energyml_objects=obj_list,
+                       raw_files=raw_file_list,
+                       core_props=core_props,
+                       additional_rels=additional_rels
+                       )
+        except zipfile.BadZipFile as error:
+            print(error)
+
+        return None
+
+

Class variables

+
+
var additional_rels : Dict[str, List[energyml.opc.opc.Relationship]]
+
+

Epc file path. Used when loaded from a local file or for export

+
+
var core_props : energyml.opc.opc.CoreProperties
+
+

xml files refered in the [Content_Types].xml

+
+
var energyml_objects : List
+
+

Other files content like pdf etc

+
+
var epc_file_path : Optional[str]
+
+
+
+
var export_versionEpcExportVersion
+
+
+
+
var external_files_path : List[str]
+
+

Additional rels for objects. Key is the object (same than in @energyml_objects) and value is a list of +RelationShip. This can be used to link an HDF5 to an ExternalPartReference in resqml 2.0.1 +Key is a value returned by @get_obj_identifier

+
+
var raw_files : List[RawFile]
+
+

A list of external files. It ca be used to link hdf5 files

+
+
+

Static methods

+
+
+def read_file(epc_file_path: str) +
+
+
+
+ +Expand source code + +
@classmethod
+def read_file(cls, epc_file_path: str):
+    with open(epc_file_path, "rb") as f:
+        epc = cls.read_stream(BytesIO(f.read()))
+        epc.epc_file_path = epc_file_path
+        return epc
+
+
+
+def read_stream(epc_file_io: _io.BytesIO) +
+
+

:param epc_file_io: +:return: an :class:EPC instance

+
+ +Expand source code + +
@classmethod
+def read_stream(cls, epc_file_io: BytesIO):  # returns an Epc instance
+    """
+    :param epc_file_io:
+    :return: an :class:`EPC` instance
+    """
+    try:
+        _read_files = []
+        obj_list = []
+        raw_file_list = []
+        additional_rels = {}
+        core_props = None
+        with zipfile.ZipFile(epc_file_io, "r", zipfile.ZIP_DEFLATED) as epc_file:
+            content_type_file_name = get_epc_content_type_path()
+            content_type_info = None
+            try:
+                content_type_info = epc_file.getinfo(content_type_file_name)
+            except KeyError:
+                for info in epc_file.infolist():
+                    if info.filename.lower() == content_type_file_name.lower():
+                        content_type_info = info
+                        break
+
+            _read_files.append(content_type_file_name)
+
+            if content_type_info is None:
+                print(f"No {content_type_file_name} file found")
+            else:
+                content_type_obj: Types = read_energyml_xml_bytes(epc_file.read(content_type_file_name))
+                path_to_obj = {}
+                for ov in content_type_obj.override:
+                    ov_ct = ov.content_type
+                    ov_path = ov.part_name
+                    # print(ov_ct)
+                    while ov_path.startswith("/") or ov_path.startswith("\\"):
+                        ov_path = ov_path[1:]
+                    if is_energyml_content_type(ov_ct):
+                        _read_files.append(ov_path)
+                        try:
+                            ov_obj = read_energyml_xml_bytes_as_class(
+                                epc_file.read(ov_path),
+                                get_class_from_content_type(ov_ct)
+                            )
+                            if isinstance(ov_obj, DerivedElement):
+                                ov_obj = ov_obj.value
+                            path_to_obj[ov_path] = ov_obj
+                            obj_list.append(ov_obj)
+                        except ParserError as e:
+                            print(f"Epc.@read_stream failed to parse file {ov_path} for content-type: {ov_ct} => {get_class_from_content_type(ov_ct)}")
+                            raise e
+                    elif get_class_from_content_type(ov_ct) == CoreProperties:
+                        _read_files.append(ov_path)
+                        core_props = read_energyml_xml_bytes_as_class(epc_file.read(ov_path), CoreProperties)
+
+                for f_info in epc_file.infolist():
+                    if f_info.filename not in _read_files:
+                        _read_files.append(f_info.filename)
+                        if not f_info.filename.lower().endswith(".rels"):
+                            try:
+                                raw_file_list.append(
+                                    RawFile(
+                                        path=f_info.filename,
+                                        content=BytesIO(epc_file.read(f_info.filename)),
+                                    )
+                                )
+                            except IOError as e:
+                                print(e)
+                        else:  # rels
+                            # print(f"reading rels {f_info.filename}")
+                            rels_folder, rels_file_name = get_file_folder_and_name_from_path(f_info.filename)
+                            while rels_folder.endswith("/"):
+                                rels_folder = rels_folder[:-1]
+                            obj_folder = rels_folder[:rels_folder.rindex("/") + 1] if "/" in rels_folder else ""
+                            obj_file_name = rels_file_name[:-5]  # removing the ".rels"
+                            rels_file: Relationships = read_energyml_xml_bytes_as_class(
+                                epc_file.read(f_info.filename),
+                                Relationships
+                            )
+                            obj_path = obj_folder + obj_file_name
+                            if obj_path in path_to_obj:
+                                try:
+                                    additional_rels_key = get_obj_identifier(path_to_obj[obj_path])
+                                    for rel in rels_file.relationship:
+                                        # print(f"\t\t{rel.type_value}")
+                                        if (rel.type_value != EPCRelsRelationshipType.DESTINATION_OBJECT.get_type()
+                                                and rel.type_value != EPCRelsRelationshipType.SOURCE_OBJECT.get_type()
+                                                and rel.type_value != EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type()
+                                        ):  # not a computable relation
+                                            if additional_rels_key not in additional_rels:
+                                                additional_rels[additional_rels_key] = []
+                                            additional_rels[additional_rels_key].append(rel)
+                                except Exception as e:
+                                    print(f"Error with obj path {obj_path} {path_to_obj[obj_path]}")
+                                    raise e
+                            else:
+                                print(f"xml file {obj_path} not found in EPC (rels is not associate to any object)")
+
+        return Epc(energyml_objects=obj_list,
+                   raw_files=raw_file_list,
+                   core_props=core_props,
+                   additional_rels=additional_rels
+                   )
+    except zipfile.BadZipFile as error:
+        print(error)
+
+    return None
+
+
+
+

Methods

+
+
+def compute_rels(self) ‑> Dict[str, energyml.opc.opc.Relationships] +
+
+

Returns a dict containing for each objet, the rels xml file path as key and the RelationShips object as value +:return:

+
+ +Expand source code + +
def compute_rels(self) -> Dict[str, Relationships]:
+    """
+    Returns a dict containing for each objet, the rels xml file path as key and the RelationShips object as value
+    :return:
+    """
+    dor_relation = get_reverse_dor_list(self.energyml_objects)
+
+    # destObject
+    rels = {
+        obj_id: [
+            Relationship(
+                target=gen_energyml_object_path(target_obj, self.export_version),
+                type_value=EPCRelsRelationshipType.DESTINATION_OBJECT.get_type(),
+                id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+            ) for target_obj in target_obj_list
+        ]
+        for obj_id, target_obj_list in dor_relation.items()
+    }
+    # sourceObject
+    for obj in self.energyml_objects:
+        obj_id = get_obj_identifier(obj)
+        if obj_id not in rels:
+            rels[obj_id] = []
+        for target_obj in get_direct_dor_list(obj):
+            rels[obj_id].append(Relationship(
+                target=gen_energyml_object_path(target_obj, self.export_version),
+                type_value=EPCRelsRelationshipType.SOURCE_OBJECT.get_type(),
+                id=f"_{obj_id}_{get_obj_type(target_obj)}_{get_obj_identifier(target_obj)}",
+            ))
+
+    map_obj_id_to_obj = {
+        get_obj_identifier(obj): obj
+        for obj in self.energyml_objects
+    }
+
+    obj_rels = {
+        gen_rels_path(energyml_object=map_obj_id_to_obj.get(obj_id), export_version=self.export_version): Relationships(
+            relationship=obj_rels + (self.additional_rels[obj_id] if obj_id in self.additional_rels else []),
+
+        )
+        for obj_id, obj_rels in rels.items()
+    }
+
+    # CoreProps
+    if self.core_props is not None:
+        obj_rels[gen_rels_path(self.core_props)] = Relationships(
+            relationship=[
+                Relationship(
+                    target=gen_core_props_path(),
+                    type_value=EPCRelsRelationshipType.EXTENDED_CORE_PROPERTIES.get_type(),
+                    id="CoreProperties"
+                )
+            ]
+        )
+
+    return obj_rels
+
+
+
+def export_file(self, path: Optional[str] = None) ‑> None +
+
+

Export the epc file. If :param:path is None, the epc 'self.epc_file_path' is used +:param path: +:return:

+
+ +Expand source code + +
def export_file(self, path: Optional[str] = None) -> None:
+    """
+    Export the epc file. If :param:`path` is None, the epc 'self.epc_file_path' is used
+    :param path:
+    :return:
+    """
+    if path is None:
+        path = self.epc_file_path
+    epc_io = self.export_io()
+    with open(path, "wb") as f:
+        f.write(epc_io.getbuffer())
+
+
+
+def export_io(self) ‑> _io.BytesIO +
+
+

Export the epc file into a :class:BytesIO instance. The result is an 'in-memory' zip file. +:return:

+
+ +Expand source code + +
def export_io(self) -> BytesIO:
+    """
+    Export the epc file into a :class:`BytesIO` instance. The result is an 'in-memory' zip file.
+    :return:
+    """
+    zip_buffer = BytesIO()
+
+    with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
+        #  Energyml objects
+        for e_obj in self.energyml_objects:
+            e_path = gen_energyml_object_path(e_obj, self.export_version)
+            zip_info = zipfile.ZipInfo(filename=e_path, date_time=datetime.datetime.now().timetuple()[:6])
+            data = serialize_xml(e_obj)
+            zip_file.writestr(zip_info, data)
+
+        # Rels
+        for rels_path, rels in self.compute_rels().items():
+            zip_info = zipfile.ZipInfo(filename=rels_path, date_time=datetime.datetime.now().timetuple()[:6])
+            data = serialize_xml(rels)
+            zip_file.writestr(zip_info, data)
+
+        # CoreProps
+        if self.core_props is not None:
+            zip_info = zipfile.ZipInfo(filename=gen_core_props_path(self.export_version),
+                                       date_time=datetime.datetime.now().timetuple()[:6])
+            data = serialize_xml(self.core_props)
+            zip_file.writestr(zip_info, data)
+
+        # ContentType
+        zip_info = zipfile.ZipInfo(filename=get_epc_content_type_path(),
+                                   date_time=datetime.datetime.now().timetuple()[:6])
+        data = serialize_xml(self.gen_opc_content_type())
+        zip_file.writestr(zip_info, data)
+
+    return zip_buffer
+
+
+
+def gen_opc_content_type(self) ‑> energyml.opc.opc.Types +
+
+

Generates a :class:Types instance and fill it with energyml objects :class:Override values +:return:

+
+ +Expand source code + +
def gen_opc_content_type(self) -> Types:
+    """
+    Generates a :class:`Types` instance and fill it with energyml objects :class:`Override` values
+    :return:
+    """
+    ct = Types()
+    rels_default = Default()
+    rels_default.content_type = RELS_CONTENT_TYPE
+    rels_default.extension = "rels"
+
+    ct.default = [rels_default]
+
+    ct.override = []
+    for e_obj in self.energyml_objects:
+        ct.override.append(Override(
+            content_type=get_content_type_from_class(type(e_obj)),
+            part_name=gen_energyml_object_path(e_obj, self.export_version),
+        ))
+
+    if self.core_props is not None:
+        ct.override.append(Override(
+            content_type=get_content_type_from_class(self.core_props),
+            part_name=gen_core_props_path(self.export_version),
+        ))
+
+    return ct
+
+
+
+def get_epc_file_folder(self) ‑> Optional[str] +
+
+
+
+ +Expand source code + +
def get_epc_file_folder(self) -> Optional[str]:
+    if self.epc_file_path is not None and len(self.epc_file_path) > 0:
+        folders_and_name = re.split(r"[\\/]", self.epc_file_path)
+        if len(folders_and_name) > 1:
+            return "/".join(folders_and_name[:-1])
+        else:
+            return ""
+    return None
+
+
+
+def get_object_by_identifier(self, identifier: str) ‑> Optional[Any] +
+
+

Search an object by its identifier. +:param identifier: given by the function :func:get_obj_identifier() +:return:

+
+ +Expand source code + +
def get_object_by_identifier(self, identifier: str) -> Optional[Any]:
+    """
+    Search an object by its identifier.
+    :param identifier: given by the function :func:`get_obj_identifier`
+    :return:
+    """
+    for o in self.energyml_objects:
+        if get_obj_identifier(o) == identifier:
+            return o
+    return None
+
+
+
+def get_object_by_uuid(self, uuid: str) ‑> List[Any] +
+
+

Search all objects with the uuid :param:uuid. +:param uuid: +:return:

+
+ +Expand source code + +
def get_object_by_uuid(self, uuid: str) -> List[Any]:
+    """
+    Search all objects with the uuid :param:`uuid`.
+    :param uuid:
+    :return:
+    """
+    return list(filter(lambda o: get_obj_uuid(o) == uuid, self.energyml_objects))
+
+
+
+
+
+class EpcExportVersion +(*args, **kwds) +
+
+

EPC export version.

+
+ +Expand source code + +
class EpcExportVersion(Enum):
+    """EPC export version."""
+    #: Classical export
+    CLASSIC = 1
+    #: Export with objet path sorted by package (eml/resqml/witsml/prodml)
+    EXPANDED = 2
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var CLASSIC
+
+

Classical export

+
+
var EXPANDED
+
+

Export with objet path sorted by package (eml/resqml/witsml/prodml)

+
+
+
+
+class NoCrsException +(*args, **kwargs) +
+
+

Common base class for all non-exit exceptions.

+
+ +Expand source code + +
class NoCrsException(Exception):
+    pass
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+
+
+class ObjectNotFoundNotException +(obj_id: str = None) +
+
+

ObjectNotFoundNotException(obj_id: str = None)

+
+ +Expand source code + +
@dataclass
+class ObjectNotFoundNotException(Exception):
+    obj_id: str = field(
+        default=None
+    )
+
+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Class variables

+
+
var obj_id : str
+
+
+
+
+
+
+class RawFile +(path: str = '_', content: _io.BytesIO = None) +
+
+

RawFile(path: str = '_', content: _io.BytesIO = None)

+
+ +Expand source code + +
@dataclass
+class RawFile:
+    path: str = field(default="_")
+    content: BytesIO = field(default=None)
+
+

Class variables

+
+
var content : _io.BytesIO
+
+
+
+
var path : str
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/index.html b/energyml-utils/docs/src/energyml/utils/index.html new file mode 100644 index 0000000..b18f86d --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/index.html @@ -0,0 +1,140 @@ + + + + + + +src.energyml.utils API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils

+
+
+

The energyml.utils module. +It contains tools for energyml management.

+

Please check the following module (depending on your need): +- energyml-opc +- energyml-common2-0 +- energyml-common2-1 +- energyml-common2-2 +- energyml-common2-3 +- energyml-resqml2-0-1 +- energyml-resqml2-2-dev3 +- energyml-resqml2-2 +- energyml-witsml2-0 +- energyml-witsml2-1 +- energyml-prodml2-0 +- energyml-prodml2-2

+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+The energyml.utils module.
+It contains tools for energyml management.
+
+Please check the following module (depending on your need):
+    - energyml-opc
+    - energyml-common2-0
+    - energyml-common2-1
+    - energyml-common2-2
+    - energyml-common2-3
+    - energyml-resqml2-0-1
+    - energyml-resqml2-2-dev3
+    - energyml-resqml2-2
+    - energyml-witsml2-0
+    - energyml-witsml2-1
+    - energyml-prodml2-0
+    - energyml-prodml2-2
+"""
+
+
+
+

Sub-modules

+
+
src.energyml.utils.data
+
+

The data module …

+
+
src.energyml.utils.epc
+
+

This example module shows various types of documentation available for use +with pydoc. +To generate HTML documentation for this module issue the +…

+
+
src.energyml.utils.introspection
+
+
+
+
src.energyml.utils.manager
+
+
+
+
src.energyml.utils.serialization
+
+
+
+
src.energyml.utils.validation
+
+
+
+
src.energyml.utils.xml
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/introspection.html b/energyml-utils/docs/src/energyml/utils/introspection.html new file mode 100644 index 0000000..23716e2 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/introspection.html @@ -0,0 +1,2141 @@ + + + + + + +src.energyml.utils.introspection API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.introspection

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import datetime
+import random
+import re
+import sys
+import typing
+import uuid as uuid_mod
+from dataclasses import Field
+from enum import Enum
+from importlib import import_module
+from typing import Any, List, Optional, Union, Dict, Tuple
+
+from .manager import get_class_pkg, get_class_pkg_version, RELATED_MODULES, \
+    get_related_energyml_modules_name, get_sub_classes, get_classes_matching_name
+from .xml import parse_content_type, ENERGYML_NAMESPACES
+
+
+primitives = (bool, str, int, float, type(None))
+
+
+def is_enum(cls: Union[type, Any]):
+    """
+    Returns True if :param:`cls` is an Enum
+    :param cls:
+    :return:
+    """
+    if isinstance(cls, type):
+        return Enum in cls.__bases__
+    return is_enum(type(cls))
+
+
+def is_primitive(cls: Union[type, Any]) -> bool:
+    """
+    Returns True if :param:`cls` is a primitiv type or extends Enum
+    :param cls:
+    :return: bool
+    """
+    if isinstance(cls, type):
+        return cls in primitives or Enum in cls.__bases__
+    return is_primitive(type(cls))
+
+
+def is_abstract(cls: Union[type, Any]) -> bool:
+    """
+    Returns True if :param:`cls` is an abstract class
+    :param cls:
+    :return: bool
+    """
+    if isinstance(cls, type):
+        return not is_primitive(cls) and (cls.__name__.startswith("Abstract") or (hasattr(cls, "__dataclass_fields__") and len(cls.__dataclass_fields__)) == 0) and len(get_class_methods(cls)) == 0
+    return is_abstract(type(cls))
+
+
+def get_class_methods(cls: Union[type, Any]) -> List[str]:
+    """
+    Returns the list of the methods names for a specific class.
+    :param cls:
+    :return:
+    """
+    return [func for func in dir(cls) if callable(getattr(cls, func)) and not func.startswith("__") and not isinstance(getattr(cls, func), type)]
+
+
+def get_class_from_name(class_name_and_module: str) -> Optional[type]:
+    """
+    Return a :class:`type` object matching with the name :param:`class_name_and_module`.
+    :param class_name_and_module:
+    :return:
+    """
+    module_name = class_name_and_module[: class_name_and_module.rindex(".")]
+    last_ns_part = class_name_and_module[
+                   class_name_and_module.rindex(".") + 1:
+                   ]
+    try:
+        # Required to read "CustomData" on eml objects that may contain resqml values
+        # ==> we need to import all modules related to the same version of the common
+        import_related_module(module_name)
+        return getattr(sys.modules[module_name], last_ns_part)
+    except AttributeError as e:
+        if "2d" in last_ns_part:
+            return get_class_from_name(
+                class_name_and_module.replace("2d", "2D")
+            )
+        elif "3d" in last_ns_part:
+            return get_class_from_name(
+                class_name_and_module.replace("3d", "3D")
+            )
+        elif last_ns_part[0].islower():
+            return get_class_from_name(
+                module_name + "." + last_ns_part[0].upper() + last_ns_part[1:]
+            )
+        else:
+            print(e)
+    return None
+
+
+def get_class_from_content_type(content_type: str) -> Optional[type]:
+    """
+    Return a :class:`type` object matching with the content-type :param:`content_type`.
+    :param content_type:
+    :return:
+    """
+    ct = parse_content_type(content_type)
+    domain = ct.group("domain")
+    if domain is None:
+        domain = "opc"
+    if domain == "opc":
+        xml_domain = ct.group("xmlDomain")
+        if "." in xml_domain:
+            xml_domain = xml_domain[xml_domain.rindex(".") + 1:]
+        if "extended" in xml_domain:
+            xml_domain = xml_domain.replace("extended", "")
+        opc_type = pascal_case(xml_domain)
+        # print("energyml.opc.opc." + opc_type)
+        return get_class_from_name("energyml.opc.opc." + opc_type)
+    else:
+        ns = ENERGYML_NAMESPACES[domain]
+        domain = ct.group("domain")
+        obj_type = ct.group("type")
+        if obj_type.lower().startswith("obj_"):  # for resqml201
+            obj_type = "Obj" + obj_type[4:]
+        version_num = str(ct.group("domainVersion")).replace(".", "_")
+        if domain.lower() == "resqml" and version_num.startswith("2_0"):
+            version_num = "2_0_1"
+        return get_class_from_name(
+            "energyml."
+            + domain
+            + ".v"
+            + version_num
+            + "."
+            + ns[ns.rindex("/") + 1:]
+            + "."
+            + obj_type
+        )
+
+
+def snake_case(s: str) -> str:
+    """ Transform a str into snake case. """
+    s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', s)
+    s = re.sub('__([A-Z])', r'_\1', s)
+    s = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s)
+    return s.lower()
+
+
+def pascal_case(s: str) -> str:
+    """ Transform a str into pascal case. """
+    return snake_case(s).replace("_", " ").title().replace(" ", "")
+
+
+def flatten_concatenation(matrix) -> List:
+    """
+    Flatten a matrix.
+
+    Example :
+        [ [a,b,c], [d,e,f], [ [x,y,z], [0] ] ]
+        will be translated in: [a, b, c, d, e, f, [x,y,z], [0]]
+    :param matrix:
+    :return:
+    """
+    flat_list = []
+    for row in matrix:
+        flat_list += row
+    return flat_list
+
+
+def import_related_module(energyml_module_name: str) -> None:
+    """
+    Import related modules for a specific energyml module. (See. :const:`RELATED_MODULES`)
+    :param energyml_module_name:
+    :return:
+    """
+    for related in RELATED_MODULES:
+        if energyml_module_name in related:
+            for m in related:
+                try:
+                    import_module(m)
+                except Exception as e:
+                    pass
+                    # print(e)
+
+
+def get_class_fields(cls: Union[type, Any]) -> Dict[str, Field]:
+    """
+    Return all class fields names, mapped to their :class:`Field` value.
+    :param cls:
+    :return:
+    """
+    if not isinstance(cls, type):  # if cls is an instance
+        cls = type(cls)
+    try:
+        return cls.__dataclass_fields__
+    except AttributeError:
+        return {}
+
+
+def get_class_attributes(cls: Union[type, Any]) -> List[str]:
+    """
+    returns a list of attributes (not private ones)
+    """
+    # if not isinstance(cls, type):  # if cls is an instance
+    #     cls = type(cls)
+    # return list(filter(lambda a: not a.startswith("__"), dir(cls)))
+    return list(get_class_fields(cls).keys())
+
+
+def get_matching_class_attribute_name(
+        cls: Union[type, Any], attribute_name: str, re_flags=re.IGNORECASE,
+) -> Optional[str]:
+    """
+    From an object and an attribute name, returns the correct attribute name of the class.
+    Example : "ObjectVersion" --> object_version.
+    This method doesn't only transform to snake case but search into the obj class attributes
+    """
+    class_fields = get_class_fields(cls)
+
+    # a search with the exact value
+    for name, cf in class_fields.items():
+        if (
+                snake_case(name) == snake_case(attribute_name)
+                or ('name' in cf.metadata and cf.metadata['name'] == attribute_name)
+        ):
+            return name
+
+    # search regex after to avoid shadowing perfect match
+    pattern = re.compile(attribute_name, flags=re_flags)
+    for name, cf in class_fields.items():
+        # print(f"\t->{name} : {attribute_name} {pattern.match(name)} {('name' in cf.metadata and pattern.match(cf.metadata['name']))}")
+        if pattern.match(name) or ('name' in cf.metadata and pattern.match(cf.metadata['name'])):
+            return name
+
+    return None
+
+
+def get_object_attribute(
+        obj: Any, attr_dot_path: str, force_snake_case=True
+) -> Any:
+    """
+    returns the value of an attribute given by a dot representation of its path in the object
+    example "Citation.Title"
+    """
+    while attr_dot_path.startswith("."):  # avoid '.Citation.Title' to take an empty attribute name before the first '.'
+        attr_dot_path = attr_dot_path[1:]
+
+    current_attrib_name = attr_dot_path
+
+    if "." in attr_dot_path:
+        current_attrib_name = attr_dot_path.split(".")[0]
+
+    if force_snake_case:
+        current_attrib_name = snake_case(current_attrib_name)
+
+    value = None
+    if isinstance(obj, list):
+        value = obj[int(current_attrib_name)]
+    elif isinstance(obj, dict):
+        value = obj[current_attrib_name]
+    else:
+        value = getattr(obj, current_attrib_name)
+
+    if "." in attr_dot_path:
+        return get_object_attribute(
+            value, attr_dot_path[len(current_attrib_name) + 1:]
+        )
+    else:
+        return value
+
+
+def get_object_attribute_advanced(obj: Any, attr_dot_path: str) -> Any:
+    """
+    see @get_matching_class_attribute_name and @get_object_attribute
+    """
+    current_attrib_name = attr_dot_path
+
+    if "." in attr_dot_path:
+        current_attrib_name = attr_dot_path.split(".")[0]
+
+    current_attrib_name = get_matching_class_attribute_name(
+        obj, current_attrib_name
+    )
+
+    value = None
+    if isinstance(obj, list):
+        value = obj[int(current_attrib_name)]
+    elif isinstance(obj, dict):
+        value = obj[current_attrib_name]
+    else:
+        value = getattr(obj, current_attrib_name)
+
+    if "." in attr_dot_path:
+        return get_object_attribute_advanced(
+            value, attr_dot_path[len(current_attrib_name) + 1:]
+        )
+    else:
+        return value
+
+
+def get_object_attribute_no_verif(obj: Any, attr_name: str) -> Any:
+    """
+    Return the value of the attribute named after param :param:`attr_name` without verification (may raise an exception
+    if it doesn't exists).
+
+    Note: attr_name="0" will work if :param:`obj` is of type :class:`List`
+    :param obj:
+    :param attr_name:
+    :return:
+    """
+    if isinstance(obj, list):
+        return obj[int(attr_name)]
+    elif isinstance(obj, dict):
+        return obj[attr_name]
+    else:
+        return getattr(obj, attr_name)
+
+
+def get_object_attribute_rgx(obj: Any, attr_dot_path_rgx: str) -> Any:
+    """
+    see @get_object_attribute. Search the attribute name using regex for values between dots.
+    Example : [Cc]itation.[Tt]it\\.*
+    """
+    current_attrib_name = attr_dot_path_rgx
+
+    attrib_list = re.split(r"(?<!\\)\.+", attr_dot_path_rgx)
+
+    if len(attrib_list) > 0:
+        current_attrib_name = attrib_list[0]
+
+    # unescape Dot
+    current_attrib_name = current_attrib_name.replace("\\.", ".")
+
+    real_attrib_name = get_matching_class_attribute_name(
+        obj, current_attrib_name
+    )
+    if real_attrib_name is not None:
+        value = get_object_attribute_no_verif(obj, real_attrib_name)
+
+        if len(attrib_list) > 1:
+            return get_object_attribute_rgx(
+                value, attr_dot_path_rgx[len(current_attrib_name) + 1:]
+            )
+        else:
+            return value
+    return None
+
+
+def get_obj_type(obj: Any) -> str:
+    """ Return the type name of an object. If obj is already a :class:`type`, return its __name__"""
+    if isinstance(obj, type):
+        return str(obj.__name__)
+    return get_obj_type(type(obj))
+
+
+def class_match_rgx(
+        cls: Union[type, Any],
+        rgx: str,
+        super_class_search: bool = True,
+        re_flags=re.IGNORECASE,
+):
+    if not isinstance(cls, type):
+        cls = type(cls)
+
+    if re.match(rgx, cls.__name__, re_flags):
+        return True
+
+    if not is_primitive(cls) and super_class_search:
+        for base in cls.__bases__:
+            if class_match_rgx(base, rgx, super_class_search, re_flags):
+                return True
+    return False
+
+
+def search_attribute_matching_type_with_path(
+        obj: Any,
+        type_rgx: str,
+        re_flags=re.IGNORECASE,
+        return_self: bool = True,  # test directly on input object and not only in its attributes
+        deep_search: bool = True,  # Search inside a matching object
+        super_class_search: bool = True,  # Search inside in super classes of the object
+        current_path: str = "",
+) -> List[Tuple[str, Any]]:
+    """
+    Returns a list of tuple (path, value) for each sub attribute with type matching param "type_rgx".
+    The path is a dot-version like ".Citation.Title"
+    :param obj:
+    :param type_rgx:
+    :param re_flags:
+    :param return_self:
+    :param deep_search:
+    :param super_class_search:
+    :param current_path:
+    :return:
+    """
+    res = []
+    if obj is not None:
+        if return_self and class_match_rgx(
+                obj, type_rgx, super_class_search, re_flags
+        ):
+            res.append((current_path, obj))
+            if not deep_search:
+                return res
+
+    if isinstance(obj, list):
+        cpt = 0
+        for s_o in obj:
+            res = res + search_attribute_matching_type_with_path(
+                obj=s_o,
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{cpt}",
+                super_class_search=super_class_search,
+            )
+            cpt = cpt + 1
+    elif isinstance(obj, dict):
+        for k, s_o in obj.items():
+            res = res + search_attribute_matching_type_with_path(
+                obj=s_o,
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{k}",
+                super_class_search=super_class_search,
+            )
+    elif not is_primitive(obj):
+        for att_name in get_class_attributes(obj):
+            res = res + search_attribute_matching_type_with_path(
+                obj=get_object_attribute_rgx(obj, att_name),
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{att_name}",
+                super_class_search=super_class_search,
+            )
+
+    return res
+
+
+def search_attribute_in_upper_matching_name(
+        obj: Any,
+        name_rgx: str,
+        root_obj: Optional[Any] = None,
+        re_flags=re.IGNORECASE,
+        current_path: str = "",
+) -> Optional[Any]:
+    """
+    See :func:`search_attribute_matching_type_with_path`. It only returns the value not the path
+    :param obj:
+    :param name_rgx:
+    :param root_obj:
+    :param re_flags:
+    :param current_path:
+    :return:
+    """
+    elt_list = search_attribute_matching_name(obj, name_rgx, search_in_sub_obj=False, deep_search=False)
+    if elt_list is not None and len(elt_list) > 0:
+        return elt_list
+
+    if obj != root_obj:
+        upper_path = current_path[:current_path.rindex(".")]
+        if len(upper_path) > 0:
+            return search_attribute_in_upper_matching_name(
+                obj=get_object_attribute(root_obj, upper_path),
+                name_rgx=name_rgx,
+                root_obj=root_obj,
+                re_flags=re_flags,
+                current_path=upper_path,
+            )
+
+    return None
+
+
+def search_attribute_matching_type(
+        obj: Any,
+        type_rgx: str,
+        re_flags=re.IGNORECASE,
+        return_self: bool = True,  # test directly on input object and not only in its attributes
+        deep_search: bool = True,  # Search inside a matching object
+        super_class_search: bool = True,  # Search inside in super classes of the object
+) -> List[Any]:
+    """
+    See :func:`search_attribute_matching_type_with_path`. It only returns the value not the path
+    :param obj:
+    :param type_rgx:
+    :param re_flags:
+    :param return_self:
+    :param deep_search:
+    :param super_class_search:
+    :return:
+    """
+    return [
+        val
+        for path, val in search_attribute_matching_type_with_path(
+            obj=obj,
+            type_rgx=type_rgx,
+            re_flags=re_flags,
+            return_self=return_self,
+            deep_search=deep_search,
+            super_class_search=super_class_search,
+        )
+    ]
+
+
+def search_attribute_matching_name_with_path(
+        obj: Any,
+        name_rgx: str,
+        re_flags=re.IGNORECASE,
+        current_path: str = "",
+        deep_search: bool = True,  # Search inside a matching object
+        search_in_sub_obj: bool = True,  # Search in obj attributes
+) -> List[Tuple[str, Any]]:
+    """
+    Returns a list of tuple (path, value) for each sub attribute with type matching param "name_rgx".
+    The path is a dot-version like ".Citation.Title"
+    :param obj:
+    :param name_rgx:
+    :param re_flags:
+    :param current_path:
+    :param deep_search:
+    :param search_in_sub_obj:
+    :return:
+    """
+    while name_rgx.startswith("."):
+        name_rgx = name_rgx[1:]
+    current_match = name_rgx
+    next_match = current_match
+    if '.' in current_match:
+        attrib_list = re.split(r"(?<!\\)\.+", name_rgx)
+        current_match = attrib_list[0]
+        next_match = '.'.join(attrib_list[1:])
+
+    res = []
+
+    match_value = None
+    match_path_and_obj = []
+    not_match_path_and_obj = []
+    if isinstance(obj, list):
+        cpt = 0
+        for s_o in obj:
+            match = re.match(current_match.replace("\\.", "."), str(cpt), flags=re_flags)
+            if match is not None:
+                match_value = match.group(0)
+                match_path_and_obj.append( (f"{current_path}.{cpt}", s_o) )
+            else:
+                not_match_path_and_obj.append( (f"{current_path}.{cpt}", s_o) )
+            cpt = cpt + 1
+    elif isinstance(obj, dict):
+        for k, s_o in obj.items():
+            match = re.match(current_match.replace("\\.", "."), k, flags=re_flags)
+            if match is not None:
+                match_value = match.group(0)
+                match_path_and_obj.append( (f"{current_path}.{k}", s_o) )
+            else:
+                not_match_path_and_obj.append( (f"{current_path}.{k}", s_o) )
+    elif not is_primitive(obj):
+        match_value = get_matching_class_attribute_name(obj, current_match.replace("\\.", "."))
+        if match_value is not None:
+            match_path_and_obj.append( (f"{current_path}.{match_value}", get_object_attribute_no_verif(obj, match_value)) )
+        for att_name in get_class_attributes(obj):
+            if att_name != match_value:
+                not_match_path_and_obj.append( (f"{current_path}.{att_name}", get_object_attribute_no_verif(obj, att_name)) )
+
+    for matched_path, matched in match_path_and_obj:
+        if next_match != current_match and len(next_match) > 0:  # next_match is different, match is not final
+            res = res + search_attribute_matching_name_with_path(
+                obj=matched,
+                name_rgx=next_match,
+                re_flags=re_flags,
+                current_path=matched_path,
+                deep_search=False,  # no deep with partial
+                search_in_sub_obj=False,  # no partial search in sub obj with no match
+            )
+        else:  # a complete match
+            res.append( (matched_path, matched) )
+            if deep_search:
+                res = res + search_attribute_matching_name_with_path(
+                    obj=matched,
+                    name_rgx=name_rgx,
+                    re_flags=re_flags,
+                    current_path=matched_path,
+                    deep_search=deep_search,  # no deep with partial
+                    search_in_sub_obj=True,
+                )
+    if search_in_sub_obj:
+        for not_matched_path, not_matched in not_match_path_and_obj:
+            res = res + search_attribute_matching_name_with_path(
+                obj=not_matched,
+                name_rgx=name_rgx,
+                re_flags=re_flags,
+                current_path=not_matched_path,
+                deep_search=deep_search,
+                search_in_sub_obj=True,
+            )
+
+    return res
+
+
+def search_attribute_matching_name(
+        obj: Any,
+        name_rgx: str,
+        re_flags=re.IGNORECASE,
+        deep_search: bool = True,  # Search inside a matching object
+        search_in_sub_obj: bool = True,  # Search in obj attributes
+) -> List[Any]:
+    """
+    See :func:`search_attribute_matching_name_with_path`. It only returns the value not the path
+
+    :param obj:
+    :param name_rgx:
+    :param re_flags:
+    :param deep_search:
+    :param search_in_sub_obj:
+    :return:
+    """
+    return [
+        val
+        for path, val in search_attribute_matching_name_with_path(
+            obj=obj,
+            name_rgx=name_rgx,
+            re_flags=re_flags,
+            deep_search=deep_search,
+            search_in_sub_obj=search_in_sub_obj
+        )
+    ]
+
+
+# Utility functions
+
+
+def gen_uuid() -> str:
+    """
+    Generate a new uuid.
+    :return:
+    """
+    return str(uuid_mod.uuid4())
+
+
+def get_obj_uuid(obj: Any) -> str:
+    """
+    Return the object uuid (attribute must match the following regex : "[Uu]u?id|UUID").
+    :param obj:
+    :return:
+    """
+    return get_object_attribute_rgx(obj, "[Uu]u?id|UUID")
+
+
+def get_obj_version(obj: Any) -> str:
+    """
+    Return the object version (check for "object_version" or "version_string" attribute).
+    :param obj:
+    :return:
+    """
+    try:
+        return get_object_attribute_no_verif(obj, "object_version")
+    except AttributeError as e:
+        try:
+            return get_object_attribute_no_verif(obj, "version_string")
+        except Exception:
+            print(f"Error with {type(obj)}")
+            raise e
+
+
+def get_direct_dor_list(obj: Any) -> List[Any]:
+    """
+    Search all sub attribute of type "DataObjectreference".
+    :param obj:
+    :return:
+    """
+    return search_attribute_matching_type(obj, "DataObjectreference")
+
+
+def get_data_object_type(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2):
+    return get_class_pkg(cls) + "." + get_class_pkg_version(cls, print_dev_version, nb_max_version_digits)
+
+
+def get_qualified_type_from_class(cls: Union[type, Any], print_dev_version=True):
+    return (
+            get_data_object_type(cls, print_dev_version, 2)
+            .replace(".", "") + "." + get_object_type_for_file_path_from_class(cls)
+    )
+
+
+def get_content_type_from_class(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2):
+    if not isinstance(cls, type):
+        cls = type(cls)
+
+    if ".opc." in cls.__module__:
+        if cls.__name__.lower() == "coreproperties":
+            return "application/vnd.openxmlformats-package.core-properties+xml"
+    else:
+        return ("application/x-" + get_class_pkg(cls)
+                + "+xml;version=" + get_class_pkg_version(cls, print_dev_version, nb_max_version_digits) + ";type="
+                + get_object_type_for_file_path_from_class(cls))
+
+    print(f"@get_content_type_from_class not supported type : {cls}")
+    return None
+
+
+def get_object_type_for_file_path_from_class(cls) -> str:
+    # obj_type = get_obj_type(cls)
+    # pkg = get_class_pkg(cls)
+    # if re.match(r"Obj[A-Z].*", obj_type) is not None and pkg == "resqml":
+    #     return "obj_" + obj_type[3:]
+    # return obj_type
+
+    try:
+        return cls.Meta.name  # to work with 3d transformed in 3D and Obj[A-Z] in obj_[A-Z]
+    except AttributeError:
+        pkg = get_class_pkg(cls)
+        return get_obj_type(cls)
+
+
+def now(time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> int:
+    """ Return an epoch value """
+    return int(datetime.datetime.timestamp(datetime.datetime.now(time_zone)))
+
+
+def epoch(time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> int:
+    return int(now(time_zone))
+
+
+def date_to_epoch(date: str) -> int:
+    """
+    Transform a energyml date into an epoch datetime
+    :return: int
+    """
+    return int(datetime.datetime.fromisoformat(date).timestamp())
+
+
+def epoch_to_date(epoch_value: int, time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> str:
+    date = datetime.datetime.fromtimestamp(epoch_value / 1e3, time_zone)
+    return date.strftime("%Y-%m-%dT%H:%M:%S%z")
+
+
+#  RANDOM
+
+
+def get_class_from_simple_name(simple_name: str, energyml_module_context=None) -> type:
+    """
+    Search for a :class:`type` depending on the simple class name :param:`simple_name`.
+    :param simple_name:
+    :param energyml_module_context:
+    :return:
+    """
+    if energyml_module_context is None:
+        energyml_module_context = []
+    try:
+        return eval(simple_name)
+    except NameError as e:
+        for mod in energyml_module_context:
+            try:
+                exec(f"from {mod} import *")
+                # required to be able to access to type in
+                # typing values like "List[ObjectAlias]"
+            except ModuleNotFoundError:
+                pass
+        return eval(simple_name)
+
+
+def _gen_str_from_attribute_name(attribute_name: Optional[str], _parent_class: Optional[type]=None) -> str:
+    """
+    Generate a str from the attribute name. The result is not the same for an attribute named "Uuid" than for an
+    attribute named "mime_type" for example.
+    :param attribute_name:
+    :param _parent_class:
+    :return:
+    """
+    attribute_name_lw = attribute_name.lower()
+    if attribute_name is not None:
+        if attribute_name_lw == "uuid" or attribute_name_lw == "uid":
+            return gen_uuid()
+        elif attribute_name_lw == "title":
+            return f"{_parent_class.__name__} title (" + str(random_value_from_class(int)) + ")"
+        elif attribute_name_lw == "schema_version" and get_class_pkg_version(_parent_class) is not None:
+            return get_class_pkg_version(_parent_class)
+        elif re.match(r"\w*version$", attribute_name_lw):
+            return str(random_value_from_class(int))
+        elif re.match(r"\w*date_.*", attribute_name_lw):
+            return epoch_to_date(epoch())
+        elif re.match(r"path_in_.*", attribute_name_lw):
+            return f"/FOLDER/{gen_uuid()}/a_patch{random.randint(0, 30)}"
+        elif "mime_type" in attribute_name_lw and ("external" in _parent_class.__name__.lower() and "part" in _parent_class.__name__.lower()):
+            return f"application/x-hdf5"
+        elif "type" in attribute_name_lw:
+            if attribute_name_lw.startswith("qualified"):
+                return get_qualified_type_from_class(get_classes_matching_name(_parent_class, "Abstract")[0])
+            if attribute_name_lw.startswith("content"):
+                return get_content_type_from_class(get_classes_matching_name(_parent_class, "Abstract")[0])
+    return "A random str " + (f"[{attribute_name}] " if attribute_name is not None else "") + "(" + str(
+        random_value_from_class(int)) + ")"
+
+
+def random_value_from_class(cls: type):
+    """
+    Generate a random value for a :class:`type`. All attributes should be filled with random values.
+    :param cls:
+    :return:
+    """
+    energyml_module_context = []
+    if not is_primitive(cls):
+        # import_related_module(cls.__module__)
+        energyml_module_context = get_related_energyml_modules_name(cls)
+    return _random_value_from_class(cls=cls, energyml_module_context=energyml_module_context, attribute_name=None)
+
+
+def _random_value_from_class(cls: Any, energyml_module_context: List[str], attribute_name: Optional[str] = None, _parent_class: Optional[type]=None):
+    """
+    Generate a random value for a :class:`type`. All attributes should be filled with random values.
+    :param cls:
+    :param energyml_module_context:
+    :param attribute_name:
+    :param _parent_class: the :class:`type`of the parent object
+    :return:
+    """
+
+    try:
+        if isinstance(cls, str) or cls == str:
+            return _gen_str_from_attribute_name(attribute_name, _parent_class)
+        elif isinstance(cls, int) or cls == int:
+            return random.randint(0, 10000)
+        elif isinstance(cls, float) or cls == float:
+            return random.randint(0, 1000000) / 100.
+        elif isinstance(cls, bool) or cls == bool:
+            return random.randint(0, 1) == 1
+        elif is_enum(cls):
+            return cls[cls._member_names_[random.randint(0, len(cls._member_names_) - 1)]]
+        elif isinstance(cls, typing.Union.__class__):
+            type_list = list(cls.__args__)
+            if type(None) in type_list:
+                type_list.remove(type(None))  # we don't want to generate none value
+            chosen_type = type_list[random.randint(0, len(type_list))]
+            return _random_value_from_class(chosen_type, energyml_module_context, attribute_name, cls)
+        elif cls.__module__ == 'typing':
+            nb_value_for_list = random.randint(2, 3)
+            type_list = list(cls.__args__)
+            if type(None) in type_list:
+                type_list.remove(type(None))  # we don't want to generate none value
+
+            if cls._name == "List":
+                lst = []
+                for i in range(nb_value_for_list):
+                    chosen_type = type_list[random.randint(0, len(type_list) - 1)]
+                    lst.append(_random_value_from_class(chosen_type, energyml_module_context, attribute_name, list))
+                return lst
+            else:
+                chosen_type = type_list[random.randint(0, len(type_list) - 1)]
+                return _random_value_from_class(chosen_type, energyml_module_context, attribute_name, _parent_class)
+        else:
+            potential_classes = list(filter(lambda _c: not is_abstract(_c), [cls] + get_sub_classes(cls)))
+            if len(potential_classes) > 0:
+                chosen_type = potential_classes[random.randint(0, len(potential_classes) - 1)]
+                args = {}
+                for k, v in get_class_fields(chosen_type).items():
+                    # print(f"get_class_fields {k} : {v}")
+                    args[k] = _random_value_from_class(
+                        cls=get_class_from_simple_name(simple_name=v.type, energyml_module_context=energyml_module_context),
+                        energyml_module_context=energyml_module_context,
+                        attribute_name=k,
+                        _parent_class=chosen_type)
+
+                if not isinstance(chosen_type, type):
+                    chosen_type = type(chosen_type)
+                return chosen_type(**args)
+
+    except Exception as e:
+        print(f"exception on attribute '{attribute_name}' for class {cls} :")
+        raise e
+
+    print(f"@_random_value_from_class Not supported object type generation {cls}")
+    return None
+
+
+
+
+
+
+
+

Functions

+
+
+def class_match_rgx(cls: Union[type, Any], rgx: str, super_class_search: bool = True, re_flags=re.IGNORECASE) +
+
+
+
+ +Expand source code + +
def class_match_rgx(
+        cls: Union[type, Any],
+        rgx: str,
+        super_class_search: bool = True,
+        re_flags=re.IGNORECASE,
+):
+    if not isinstance(cls, type):
+        cls = type(cls)
+
+    if re.match(rgx, cls.__name__, re_flags):
+        return True
+
+    if not is_primitive(cls) and super_class_search:
+        for base in cls.__bases__:
+            if class_match_rgx(base, rgx, super_class_search, re_flags):
+                return True
+    return False
+
+
+
+def date_to_epoch(date: str) ‑> int +
+
+

Transform a energyml date into an epoch datetime +:return: int

+
+ +Expand source code + +
def date_to_epoch(date: str) -> int:
+    """
+    Transform a energyml date into an epoch datetime
+    :return: int
+    """
+    return int(datetime.datetime.fromisoformat(date).timestamp())
+
+
+
+def epoch(time_zone=datetime.timezone(datetime.timedelta(seconds=3600), 'UTC')) ‑> int +
+
+
+
+ +Expand source code + +
def epoch(time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> int:
+    return int(now(time_zone))
+
+
+
+def epoch_to_date(epoch_value: int, time_zone=datetime.timezone(datetime.timedelta(seconds=3600), 'UTC')) ‑> str +
+
+
+
+ +Expand source code + +
def epoch_to_date(epoch_value: int, time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> str:
+    date = datetime.datetime.fromtimestamp(epoch_value / 1e3, time_zone)
+    return date.strftime("%Y-%m-%dT%H:%M:%S%z")
+
+
+
+def flatten_concatenation(matrix) ‑> List +
+
+

Flatten a matrix.

+

Example : +[ [a,b,c], [d,e,f], [ [x,y,z], [0] ] ] +will be translated in: [a, b, c, d, e, f, [x,y,z], [0]] +:param matrix: +:return:

+
+ +Expand source code + +
def flatten_concatenation(matrix) -> List:
+    """
+    Flatten a matrix.
+
+    Example :
+        [ [a,b,c], [d,e,f], [ [x,y,z], [0] ] ]
+        will be translated in: [a, b, c, d, e, f, [x,y,z], [0]]
+    :param matrix:
+    :return:
+    """
+    flat_list = []
+    for row in matrix:
+        flat_list += row
+    return flat_list
+
+
+
+def gen_uuid() ‑> str +
+
+

Generate a new uuid. +:return:

+
+ +Expand source code + +
def gen_uuid() -> str:
+    """
+    Generate a new uuid.
+    :return:
+    """
+    return str(uuid_mod.uuid4())
+
+
+
+def get_class_attributes(cls: Union[type, Any]) ‑> List[str] +
+
+

returns a list of attributes (not private ones)

+
+ +Expand source code + +
def get_class_attributes(cls: Union[type, Any]) -> List[str]:
+    """
+    returns a list of attributes (not private ones)
+    """
+    # if not isinstance(cls, type):  # if cls is an instance
+    #     cls = type(cls)
+    # return list(filter(lambda a: not a.startswith("__"), dir(cls)))
+    return list(get_class_fields(cls).keys())
+
+
+
+def get_class_fields(cls: Union[type, Any]) ‑> Dict[str, dataclasses.Field] +
+
+

Return all class fields names, mapped to their :class:Field value. +:param cls: +:return:

+
+ +Expand source code + +
def get_class_fields(cls: Union[type, Any]) -> Dict[str, Field]:
+    """
+    Return all class fields names, mapped to their :class:`Field` value.
+    :param cls:
+    :return:
+    """
+    if not isinstance(cls, type):  # if cls is an instance
+        cls = type(cls)
+    try:
+        return cls.__dataclass_fields__
+    except AttributeError:
+        return {}
+
+
+
+def get_class_from_content_type(content_type: str) ‑> Optional[type] +
+
+

Return a :class:type object matching with the content-type :param:content_type. +:param content_type: +:return:

+
+ +Expand source code + +
def get_class_from_content_type(content_type: str) -> Optional[type]:
+    """
+    Return a :class:`type` object matching with the content-type :param:`content_type`.
+    :param content_type:
+    :return:
+    """
+    ct = parse_content_type(content_type)
+    domain = ct.group("domain")
+    if domain is None:
+        domain = "opc"
+    if domain == "opc":
+        xml_domain = ct.group("xmlDomain")
+        if "." in xml_domain:
+            xml_domain = xml_domain[xml_domain.rindex(".") + 1:]
+        if "extended" in xml_domain:
+            xml_domain = xml_domain.replace("extended", "")
+        opc_type = pascal_case(xml_domain)
+        # print("energyml.opc.opc." + opc_type)
+        return get_class_from_name("energyml.opc.opc." + opc_type)
+    else:
+        ns = ENERGYML_NAMESPACES[domain]
+        domain = ct.group("domain")
+        obj_type = ct.group("type")
+        if obj_type.lower().startswith("obj_"):  # for resqml201
+            obj_type = "Obj" + obj_type[4:]
+        version_num = str(ct.group("domainVersion")).replace(".", "_")
+        if domain.lower() == "resqml" and version_num.startswith("2_0"):
+            version_num = "2_0_1"
+        return get_class_from_name(
+            "energyml."
+            + domain
+            + ".v"
+            + version_num
+            + "."
+            + ns[ns.rindex("/") + 1:]
+            + "."
+            + obj_type
+        )
+
+
+
+def get_class_from_name(class_name_and_module: str) ‑> Optional[type] +
+
+

Return a :class:type object matching with the name :param:class_name_and_module. +:param class_name_and_module: +:return:

+
+ +Expand source code + +
def get_class_from_name(class_name_and_module: str) -> Optional[type]:
+    """
+    Return a :class:`type` object matching with the name :param:`class_name_and_module`.
+    :param class_name_and_module:
+    :return:
+    """
+    module_name = class_name_and_module[: class_name_and_module.rindex(".")]
+    last_ns_part = class_name_and_module[
+                   class_name_and_module.rindex(".") + 1:
+                   ]
+    try:
+        # Required to read "CustomData" on eml objects that may contain resqml values
+        # ==> we need to import all modules related to the same version of the common
+        import_related_module(module_name)
+        return getattr(sys.modules[module_name], last_ns_part)
+    except AttributeError as e:
+        if "2d" in last_ns_part:
+            return get_class_from_name(
+                class_name_and_module.replace("2d", "2D")
+            )
+        elif "3d" in last_ns_part:
+            return get_class_from_name(
+                class_name_and_module.replace("3d", "3D")
+            )
+        elif last_ns_part[0].islower():
+            return get_class_from_name(
+                module_name + "." + last_ns_part[0].upper() + last_ns_part[1:]
+            )
+        else:
+            print(e)
+    return None
+
+
+
+def get_class_from_simple_name(simple_name: str, energyml_module_context=None) ‑> type +
+
+

Search for a :class:type depending on the simple class name :param:simple_name. +:param simple_name: +:param energyml_module_context: +:return:

+
+ +Expand source code + +
def get_class_from_simple_name(simple_name: str, energyml_module_context=None) -> type:
+    """
+    Search for a :class:`type` depending on the simple class name :param:`simple_name`.
+    :param simple_name:
+    :param energyml_module_context:
+    :return:
+    """
+    if energyml_module_context is None:
+        energyml_module_context = []
+    try:
+        return eval(simple_name)
+    except NameError as e:
+        for mod in energyml_module_context:
+            try:
+                exec(f"from {mod} import *")
+                # required to be able to access to type in
+                # typing values like "List[ObjectAlias]"
+            except ModuleNotFoundError:
+                pass
+        return eval(simple_name)
+
+
+
+def get_class_methods(cls: Union[type, Any]) ‑> List[str] +
+
+

Returns the list of the methods names for a specific class. +:param cls: +:return:

+
+ +Expand source code + +
def get_class_methods(cls: Union[type, Any]) -> List[str]:
+    """
+    Returns the list of the methods names for a specific class.
+    :param cls:
+    :return:
+    """
+    return [func for func in dir(cls) if callable(getattr(cls, func)) and not func.startswith("__") and not isinstance(getattr(cls, func), type)]
+
+
+
+def get_content_type_from_class(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2) +
+
+
+
+ +Expand source code + +
def get_content_type_from_class(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2):
+    if not isinstance(cls, type):
+        cls = type(cls)
+
+    if ".opc." in cls.__module__:
+        if cls.__name__.lower() == "coreproperties":
+            return "application/vnd.openxmlformats-package.core-properties+xml"
+    else:
+        return ("application/x-" + get_class_pkg(cls)
+                + "+xml;version=" + get_class_pkg_version(cls, print_dev_version, nb_max_version_digits) + ";type="
+                + get_object_type_for_file_path_from_class(cls))
+
+    print(f"@get_content_type_from_class not supported type : {cls}")
+    return None
+
+
+
+def get_data_object_type(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2) +
+
+
+
+ +Expand source code + +
def get_data_object_type(cls: Union[type, Any], print_dev_version=True, nb_max_version_digits=2):
+    return get_class_pkg(cls) + "." + get_class_pkg_version(cls, print_dev_version, nb_max_version_digits)
+
+
+
+def get_direct_dor_list(obj: Any) ‑> List[Any] +
+
+

Search all sub attribute of type "DataObjectreference". +:param obj: +:return:

+
+ +Expand source code + +
def get_direct_dor_list(obj: Any) -> List[Any]:
+    """
+    Search all sub attribute of type "DataObjectreference".
+    :param obj:
+    :return:
+    """
+    return search_attribute_matching_type(obj, "DataObjectreference")
+
+
+
+def get_matching_class_attribute_name(cls: Union[type, Any], attribute_name: str, re_flags=re.IGNORECASE) ‑> Optional[str] +
+
+

From an object and an attribute name, returns the correct attribute name of the class. +Example : "ObjectVersion" –> object_version. +This method doesn't only transform to snake case but search into the obj class attributes

+
+ +Expand source code + +
def get_matching_class_attribute_name(
+        cls: Union[type, Any], attribute_name: str, re_flags=re.IGNORECASE,
+) -> Optional[str]:
+    """
+    From an object and an attribute name, returns the correct attribute name of the class.
+    Example : "ObjectVersion" --> object_version.
+    This method doesn't only transform to snake case but search into the obj class attributes
+    """
+    class_fields = get_class_fields(cls)
+
+    # a search with the exact value
+    for name, cf in class_fields.items():
+        if (
+                snake_case(name) == snake_case(attribute_name)
+                or ('name' in cf.metadata and cf.metadata['name'] == attribute_name)
+        ):
+            return name
+
+    # search regex after to avoid shadowing perfect match
+    pattern = re.compile(attribute_name, flags=re_flags)
+    for name, cf in class_fields.items():
+        # print(f"\t->{name} : {attribute_name} {pattern.match(name)} {('name' in cf.metadata and pattern.match(cf.metadata['name']))}")
+        if pattern.match(name) or ('name' in cf.metadata and pattern.match(cf.metadata['name'])):
+            return name
+
+    return None
+
+
+
+def get_obj_type(obj: Any) ‑> str +
+
+

Return the type name of an object. If obj is already a :class:type, return its name

+
+ +Expand source code + +
def get_obj_type(obj: Any) -> str:
+    """ Return the type name of an object. If obj is already a :class:`type`, return its __name__"""
+    if isinstance(obj, type):
+        return str(obj.__name__)
+    return get_obj_type(type(obj))
+
+
+
+def get_obj_uuid(obj: Any) ‑> str +
+
+

Return the object uuid (attribute must match the following regex : "[Uu]u?id|UUID"). +:param obj: +:return:

+
+ +Expand source code + +
def get_obj_uuid(obj: Any) -> str:
+    """
+    Return the object uuid (attribute must match the following regex : "[Uu]u?id|UUID").
+    :param obj:
+    :return:
+    """
+    return get_object_attribute_rgx(obj, "[Uu]u?id|UUID")
+
+
+
+def get_obj_version(obj: Any) ‑> str +
+
+

Return the object version (check for "object_version" or "version_string" attribute). +:param obj: +:return:

+
+ +Expand source code + +
def get_obj_version(obj: Any) -> str:
+    """
+    Return the object version (check for "object_version" or "version_string" attribute).
+    :param obj:
+    :return:
+    """
+    try:
+        return get_object_attribute_no_verif(obj, "object_version")
+    except AttributeError as e:
+        try:
+            return get_object_attribute_no_verif(obj, "version_string")
+        except Exception:
+            print(f"Error with {type(obj)}")
+            raise e
+
+
+
+def get_object_attribute(obj: Any, attr_dot_path: str, force_snake_case=True) ‑> Any +
+
+

returns the value of an attribute given by a dot representation of its path in the object +example "Citation.Title"

+
+ +Expand source code + +
def get_object_attribute(
+        obj: Any, attr_dot_path: str, force_snake_case=True
+) -> Any:
+    """
+    returns the value of an attribute given by a dot representation of its path in the object
+    example "Citation.Title"
+    """
+    while attr_dot_path.startswith("."):  # avoid '.Citation.Title' to take an empty attribute name before the first '.'
+        attr_dot_path = attr_dot_path[1:]
+
+    current_attrib_name = attr_dot_path
+
+    if "." in attr_dot_path:
+        current_attrib_name = attr_dot_path.split(".")[0]
+
+    if force_snake_case:
+        current_attrib_name = snake_case(current_attrib_name)
+
+    value = None
+    if isinstance(obj, list):
+        value = obj[int(current_attrib_name)]
+    elif isinstance(obj, dict):
+        value = obj[current_attrib_name]
+    else:
+        value = getattr(obj, current_attrib_name)
+
+    if "." in attr_dot_path:
+        return get_object_attribute(
+            value, attr_dot_path[len(current_attrib_name) + 1:]
+        )
+    else:
+        return value
+
+
+
+def get_object_attribute_advanced(obj: Any, attr_dot_path: str) ‑> Any +
+
+

see @get_matching_class_attribute_name and @get_object_attribute

+
+ +Expand source code + +
def get_object_attribute_advanced(obj: Any, attr_dot_path: str) -> Any:
+    """
+    see @get_matching_class_attribute_name and @get_object_attribute
+    """
+    current_attrib_name = attr_dot_path
+
+    if "." in attr_dot_path:
+        current_attrib_name = attr_dot_path.split(".")[0]
+
+    current_attrib_name = get_matching_class_attribute_name(
+        obj, current_attrib_name
+    )
+
+    value = None
+    if isinstance(obj, list):
+        value = obj[int(current_attrib_name)]
+    elif isinstance(obj, dict):
+        value = obj[current_attrib_name]
+    else:
+        value = getattr(obj, current_attrib_name)
+
+    if "." in attr_dot_path:
+        return get_object_attribute_advanced(
+            value, attr_dot_path[len(current_attrib_name) + 1:]
+        )
+    else:
+        return value
+
+
+
+def get_object_attribute_no_verif(obj: Any, attr_name: str) ‑> Any +
+
+

Return the value of the attribute named after param :param:attr_name without verification (may raise an exception +if it doesn't exists).

+

Note: attr_name="0" will work if :param:obj is of type :class:List +:param obj: +:param attr_name: +:return:

+
+ +Expand source code + +
def get_object_attribute_no_verif(obj: Any, attr_name: str) -> Any:
+    """
+    Return the value of the attribute named after param :param:`attr_name` without verification (may raise an exception
+    if it doesn't exists).
+
+    Note: attr_name="0" will work if :param:`obj` is of type :class:`List`
+    :param obj:
+    :param attr_name:
+    :return:
+    """
+    if isinstance(obj, list):
+        return obj[int(attr_name)]
+    elif isinstance(obj, dict):
+        return obj[attr_name]
+    else:
+        return getattr(obj, attr_name)
+
+
+
+def get_object_attribute_rgx(obj: Any, attr_dot_path_rgx: str) ‑> Any +
+
+

see @get_object_attribute. Search the attribute name using regex for values between dots. +Example : [Cc]itation.[Tt]it.*

+
+ +Expand source code + +
def get_object_attribute_rgx(obj: Any, attr_dot_path_rgx: str) -> Any:
+    """
+    see @get_object_attribute. Search the attribute name using regex for values between dots.
+    Example : [Cc]itation.[Tt]it\\.*
+    """
+    current_attrib_name = attr_dot_path_rgx
+
+    attrib_list = re.split(r"(?<!\\)\.+", attr_dot_path_rgx)
+
+    if len(attrib_list) > 0:
+        current_attrib_name = attrib_list[0]
+
+    # unescape Dot
+    current_attrib_name = current_attrib_name.replace("\\.", ".")
+
+    real_attrib_name = get_matching_class_attribute_name(
+        obj, current_attrib_name
+    )
+    if real_attrib_name is not None:
+        value = get_object_attribute_no_verif(obj, real_attrib_name)
+
+        if len(attrib_list) > 1:
+            return get_object_attribute_rgx(
+                value, attr_dot_path_rgx[len(current_attrib_name) + 1:]
+            )
+        else:
+            return value
+    return None
+
+
+
+def get_object_type_for_file_path_from_class(cls) ‑> str +
+
+
+
+ +Expand source code + +
def get_object_type_for_file_path_from_class(cls) -> str:
+    # obj_type = get_obj_type(cls)
+    # pkg = get_class_pkg(cls)
+    # if re.match(r"Obj[A-Z].*", obj_type) is not None and pkg == "resqml":
+    #     return "obj_" + obj_type[3:]
+    # return obj_type
+
+    try:
+        return cls.Meta.name  # to work with 3d transformed in 3D and Obj[A-Z] in obj_[A-Z]
+    except AttributeError:
+        pkg = get_class_pkg(cls)
+        return get_obj_type(cls)
+
+
+
+def get_qualified_type_from_class(cls: Union[type, Any], print_dev_version=True) +
+
+
+
+ +Expand source code + +
def get_qualified_type_from_class(cls: Union[type, Any], print_dev_version=True):
+    return (
+            get_data_object_type(cls, print_dev_version, 2)
+            .replace(".", "") + "." + get_object_type_for_file_path_from_class(cls)
+    )
+
+
+ +
+

Import related modules for a specific energyml module. (See. :const:RELATED_MODULES) +:param energyml_module_name: +:return:

+
+ +Expand source code + +
def import_related_module(energyml_module_name: str) -> None:
+    """
+    Import related modules for a specific energyml module. (See. :const:`RELATED_MODULES`)
+    :param energyml_module_name:
+    :return:
+    """
+    for related in RELATED_MODULES:
+        if energyml_module_name in related:
+            for m in related:
+                try:
+                    import_module(m)
+                except Exception as e:
+                    pass
+                    # print(e)
+
+
+
+def is_abstract(cls: Union[type, Any]) ‑> bool +
+
+

Returns True if :param:cls is an abstract class +:param cls: +:return: bool

+
+ +Expand source code + +
def is_abstract(cls: Union[type, Any]) -> bool:
+    """
+    Returns True if :param:`cls` is an abstract class
+    :param cls:
+    :return: bool
+    """
+    if isinstance(cls, type):
+        return not is_primitive(cls) and (cls.__name__.startswith("Abstract") or (hasattr(cls, "__dataclass_fields__") and len(cls.__dataclass_fields__)) == 0) and len(get_class_methods(cls)) == 0
+    return is_abstract(type(cls))
+
+
+
+def is_enum(cls: Union[type, Any]) +
+
+

Returns True if :param:cls is an Enum +:param cls: +:return:

+
+ +Expand source code + +
def is_enum(cls: Union[type, Any]):
+    """
+    Returns True if :param:`cls` is an Enum
+    :param cls:
+    :return:
+    """
+    if isinstance(cls, type):
+        return Enum in cls.__bases__
+    return is_enum(type(cls))
+
+
+
+def is_primitive(cls: Union[type, Any]) ‑> bool +
+
+

Returns True if :param:cls is a primitiv type or extends Enum +:param cls: +:return: bool

+
+ +Expand source code + +
def is_primitive(cls: Union[type, Any]) -> bool:
+    """
+    Returns True if :param:`cls` is a primitiv type or extends Enum
+    :param cls:
+    :return: bool
+    """
+    if isinstance(cls, type):
+        return cls in primitives or Enum in cls.__bases__
+    return is_primitive(type(cls))
+
+
+
+def now(time_zone=datetime.timezone(datetime.timedelta(seconds=3600), 'UTC')) ‑> int +
+
+

Return an epoch value

+
+ +Expand source code + +
def now(time_zone=datetime.timezone(datetime.timedelta(hours=1), "UTC")) -> int:
+    """ Return an epoch value """
+    return int(datetime.datetime.timestamp(datetime.datetime.now(time_zone)))
+
+
+
+def pascal_case(s: str) ‑> str +
+
+

Transform a str into pascal case.

+
+ +Expand source code + +
def pascal_case(s: str) -> str:
+    """ Transform a str into pascal case. """
+    return snake_case(s).replace("_", " ").title().replace(" ", "")
+
+
+
+def random_value_from_class(cls: type) +
+
+

Generate a random value for a :class:type. All attributes should be filled with random values. +:param cls: +:return:

+
+ +Expand source code + +
def random_value_from_class(cls: type):
+    """
+    Generate a random value for a :class:`type`. All attributes should be filled with random values.
+    :param cls:
+    :return:
+    """
+    energyml_module_context = []
+    if not is_primitive(cls):
+        # import_related_module(cls.__module__)
+        energyml_module_context = get_related_energyml_modules_name(cls)
+    return _random_value_from_class(cls=cls, energyml_module_context=energyml_module_context, attribute_name=None)
+
+
+
+def search_attribute_in_upper_matching_name(obj: Any, name_rgx: str, root_obj: Optional[Any] = None, re_flags=re.IGNORECASE, current_path: str = '') ‑> Optional[Any] +
+
+

See :func:search_attribute_matching_type_with_path(). It only returns the value not the path +:param obj: +:param name_rgx: +:param root_obj: +:param re_flags: +:param current_path: +:return:

+
+ +Expand source code + +
def search_attribute_in_upper_matching_name(
+        obj: Any,
+        name_rgx: str,
+        root_obj: Optional[Any] = None,
+        re_flags=re.IGNORECASE,
+        current_path: str = "",
+) -> Optional[Any]:
+    """
+    See :func:`search_attribute_matching_type_with_path`. It only returns the value not the path
+    :param obj:
+    :param name_rgx:
+    :param root_obj:
+    :param re_flags:
+    :param current_path:
+    :return:
+    """
+    elt_list = search_attribute_matching_name(obj, name_rgx, search_in_sub_obj=False, deep_search=False)
+    if elt_list is not None and len(elt_list) > 0:
+        return elt_list
+
+    if obj != root_obj:
+        upper_path = current_path[:current_path.rindex(".")]
+        if len(upper_path) > 0:
+            return search_attribute_in_upper_matching_name(
+                obj=get_object_attribute(root_obj, upper_path),
+                name_rgx=name_rgx,
+                root_obj=root_obj,
+                re_flags=re_flags,
+                current_path=upper_path,
+            )
+
+    return None
+
+
+
+def search_attribute_matching_name(obj: Any, name_rgx: str, re_flags=re.IGNORECASE, deep_search: bool = True, search_in_sub_obj: bool = True) ‑> List[Any] +
+
+

See :func:search_attribute_matching_name_with_path(). It only returns the value not the path

+

:param obj: +:param name_rgx: +:param re_flags: +:param deep_search: +:param search_in_sub_obj: +:return:

+
+ +Expand source code + +
def search_attribute_matching_name(
+        obj: Any,
+        name_rgx: str,
+        re_flags=re.IGNORECASE,
+        deep_search: bool = True,  # Search inside a matching object
+        search_in_sub_obj: bool = True,  # Search in obj attributes
+) -> List[Any]:
+    """
+    See :func:`search_attribute_matching_name_with_path`. It only returns the value not the path
+
+    :param obj:
+    :param name_rgx:
+    :param re_flags:
+    :param deep_search:
+    :param search_in_sub_obj:
+    :return:
+    """
+    return [
+        val
+        for path, val in search_attribute_matching_name_with_path(
+            obj=obj,
+            name_rgx=name_rgx,
+            re_flags=re_flags,
+            deep_search=deep_search,
+            search_in_sub_obj=search_in_sub_obj
+        )
+    ]
+
+
+
+def search_attribute_matching_name_with_path(obj: Any, name_rgx: str, re_flags=re.IGNORECASE, current_path: str = '', deep_search: bool = True, search_in_sub_obj: bool = True) ‑> List[Tuple[str, Any]] +
+
+

Returns a list of tuple (path, value) for each sub attribute with type matching param "name_rgx". +The path is a dot-version like ".Citation.Title" +:param obj: +:param name_rgx: +:param re_flags: +:param current_path: +:param deep_search: +:param search_in_sub_obj: +:return:

+
+ +Expand source code + +
def search_attribute_matching_name_with_path(
+        obj: Any,
+        name_rgx: str,
+        re_flags=re.IGNORECASE,
+        current_path: str = "",
+        deep_search: bool = True,  # Search inside a matching object
+        search_in_sub_obj: bool = True,  # Search in obj attributes
+) -> List[Tuple[str, Any]]:
+    """
+    Returns a list of tuple (path, value) for each sub attribute with type matching param "name_rgx".
+    The path is a dot-version like ".Citation.Title"
+    :param obj:
+    :param name_rgx:
+    :param re_flags:
+    :param current_path:
+    :param deep_search:
+    :param search_in_sub_obj:
+    :return:
+    """
+    while name_rgx.startswith("."):
+        name_rgx = name_rgx[1:]
+    current_match = name_rgx
+    next_match = current_match
+    if '.' in current_match:
+        attrib_list = re.split(r"(?<!\\)\.+", name_rgx)
+        current_match = attrib_list[0]
+        next_match = '.'.join(attrib_list[1:])
+
+    res = []
+
+    match_value = None
+    match_path_and_obj = []
+    not_match_path_and_obj = []
+    if isinstance(obj, list):
+        cpt = 0
+        for s_o in obj:
+            match = re.match(current_match.replace("\\.", "."), str(cpt), flags=re_flags)
+            if match is not None:
+                match_value = match.group(0)
+                match_path_and_obj.append( (f"{current_path}.{cpt}", s_o) )
+            else:
+                not_match_path_and_obj.append( (f"{current_path}.{cpt}", s_o) )
+            cpt = cpt + 1
+    elif isinstance(obj, dict):
+        for k, s_o in obj.items():
+            match = re.match(current_match.replace("\\.", "."), k, flags=re_flags)
+            if match is not None:
+                match_value = match.group(0)
+                match_path_and_obj.append( (f"{current_path}.{k}", s_o) )
+            else:
+                not_match_path_and_obj.append( (f"{current_path}.{k}", s_o) )
+    elif not is_primitive(obj):
+        match_value = get_matching_class_attribute_name(obj, current_match.replace("\\.", "."))
+        if match_value is not None:
+            match_path_and_obj.append( (f"{current_path}.{match_value}", get_object_attribute_no_verif(obj, match_value)) )
+        for att_name in get_class_attributes(obj):
+            if att_name != match_value:
+                not_match_path_and_obj.append( (f"{current_path}.{att_name}", get_object_attribute_no_verif(obj, att_name)) )
+
+    for matched_path, matched in match_path_and_obj:
+        if next_match != current_match and len(next_match) > 0:  # next_match is different, match is not final
+            res = res + search_attribute_matching_name_with_path(
+                obj=matched,
+                name_rgx=next_match,
+                re_flags=re_flags,
+                current_path=matched_path,
+                deep_search=False,  # no deep with partial
+                search_in_sub_obj=False,  # no partial search in sub obj with no match
+            )
+        else:  # a complete match
+            res.append( (matched_path, matched) )
+            if deep_search:
+                res = res + search_attribute_matching_name_with_path(
+                    obj=matched,
+                    name_rgx=name_rgx,
+                    re_flags=re_flags,
+                    current_path=matched_path,
+                    deep_search=deep_search,  # no deep with partial
+                    search_in_sub_obj=True,
+                )
+    if search_in_sub_obj:
+        for not_matched_path, not_matched in not_match_path_and_obj:
+            res = res + search_attribute_matching_name_with_path(
+                obj=not_matched,
+                name_rgx=name_rgx,
+                re_flags=re_flags,
+                current_path=not_matched_path,
+                deep_search=deep_search,
+                search_in_sub_obj=True,
+            )
+
+    return res
+
+
+
+def search_attribute_matching_type(obj: Any, type_rgx: str, re_flags=re.IGNORECASE, return_self: bool = True, deep_search: bool = True, super_class_search: bool = True) ‑> List[Any] +
+
+

See :func:search_attribute_matching_type_with_path(). It only returns the value not the path +:param obj: +:param type_rgx: +:param re_flags: +:param return_self: +:param deep_search: +:param super_class_search: +:return:

+
+ +Expand source code + +
def search_attribute_matching_type(
+        obj: Any,
+        type_rgx: str,
+        re_flags=re.IGNORECASE,
+        return_self: bool = True,  # test directly on input object and not only in its attributes
+        deep_search: bool = True,  # Search inside a matching object
+        super_class_search: bool = True,  # Search inside in super classes of the object
+) -> List[Any]:
+    """
+    See :func:`search_attribute_matching_type_with_path`. It only returns the value not the path
+    :param obj:
+    :param type_rgx:
+    :param re_flags:
+    :param return_self:
+    :param deep_search:
+    :param super_class_search:
+    :return:
+    """
+    return [
+        val
+        for path, val in search_attribute_matching_type_with_path(
+            obj=obj,
+            type_rgx=type_rgx,
+            re_flags=re_flags,
+            return_self=return_self,
+            deep_search=deep_search,
+            super_class_search=super_class_search,
+        )
+    ]
+
+
+
+def search_attribute_matching_type_with_path(obj: Any, type_rgx: str, re_flags=re.IGNORECASE, return_self: bool = True, deep_search: bool = True, super_class_search: bool = True, current_path: str = '') ‑> List[Tuple[str, Any]] +
+
+

Returns a list of tuple (path, value) for each sub attribute with type matching param "type_rgx". +The path is a dot-version like ".Citation.Title" +:param obj: +:param type_rgx: +:param re_flags: +:param return_self: +:param deep_search: +:param super_class_search: +:param current_path: +:return:

+
+ +Expand source code + +
def search_attribute_matching_type_with_path(
+        obj: Any,
+        type_rgx: str,
+        re_flags=re.IGNORECASE,
+        return_self: bool = True,  # test directly on input object and not only in its attributes
+        deep_search: bool = True,  # Search inside a matching object
+        super_class_search: bool = True,  # Search inside in super classes of the object
+        current_path: str = "",
+) -> List[Tuple[str, Any]]:
+    """
+    Returns a list of tuple (path, value) for each sub attribute with type matching param "type_rgx".
+    The path is a dot-version like ".Citation.Title"
+    :param obj:
+    :param type_rgx:
+    :param re_flags:
+    :param return_self:
+    :param deep_search:
+    :param super_class_search:
+    :param current_path:
+    :return:
+    """
+    res = []
+    if obj is not None:
+        if return_self and class_match_rgx(
+                obj, type_rgx, super_class_search, re_flags
+        ):
+            res.append((current_path, obj))
+            if not deep_search:
+                return res
+
+    if isinstance(obj, list):
+        cpt = 0
+        for s_o in obj:
+            res = res + search_attribute_matching_type_with_path(
+                obj=s_o,
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{cpt}",
+                super_class_search=super_class_search,
+            )
+            cpt = cpt + 1
+    elif isinstance(obj, dict):
+        for k, s_o in obj.items():
+            res = res + search_attribute_matching_type_with_path(
+                obj=s_o,
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{k}",
+                super_class_search=super_class_search,
+            )
+    elif not is_primitive(obj):
+        for att_name in get_class_attributes(obj):
+            res = res + search_attribute_matching_type_with_path(
+                obj=get_object_attribute_rgx(obj, att_name),
+                type_rgx=type_rgx,
+                re_flags=re_flags,
+                return_self=True,
+                deep_search=deep_search,
+                current_path=f"{current_path}.{att_name}",
+                super_class_search=super_class_search,
+            )
+
+    return res
+
+
+
+def snake_case(s: str) ‑> str +
+
+

Transform a str into snake case.

+
+ +Expand source code + +
def snake_case(s: str) -> str:
+    """ Transform a str into snake case. """
+    s = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', s)
+    s = re.sub('__([A-Z])', r'_\1', s)
+    s = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s)
+    return s.lower()
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/manager.html b/energyml-utils/docs/src/energyml/utils/manager.html new file mode 100644 index 0000000..016a54e --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/manager.html @@ -0,0 +1,615 @@ + + + + + + +src.energyml.utils.manager API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.manager

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import importlib
+import inspect
+import pkgutil
+import re
+from typing import List, Union, Any
+
+REGEX_ENERGYML_MODULE_NAME = r"energyml\.(?P<pkg>.*)\.v(?P<version>(?P<versionNumber>\d+(_\d+)*)(_dev(?P<versionDev>.*))?)\..*"
+REGEX_PROJECT_VERSION = r"(?P<n0>[\d]+)(.(?P<n1>[\d]+)(.(?P<n2>[\d]+))?)?"
+
+ENERGYML_MODULES_NAMES = ["eml", "prodml", "witsml", "resqml"]
+
+RELATED_MODULES = [
+    ["energyml.eml.v2_0.commonv2", "energyml.resqml.v2_0_1.resqmlv2"],
+    [
+        "energyml.eml.v2_1.commonv2",
+        "energyml.prodml.v2_0.prodmlv2",
+        "energyml.witsml.v2_0.witsmlv2",
+    ],
+    ["energyml.eml.v2_2.commonv2", "energyml.resqml.v2_2_dev3.resqmlv2"],
+    [
+        "energyml.eml.v2_3.commonv2",
+        "energyml.resqml.v2_2.resqmlv2",
+        "energyml.prodml.v2_2.prodmlv2",
+        "energyml.witsml.v2_1.witsmlv2",
+    ],
+]
+
+
+def get_related_energyml_modules_name(cls: Union[type, Any]) -> List[str]:
+    """
+    Return the list of all energyml modules related to another one.
+    For example resqml 2.0.1 is related to common 2.0
+    :param cls:
+    :return:
+    """
+    if isinstance(cls, type):
+        for related in RELATED_MODULES:
+            if cls.__module__ in related:
+                return related
+    else:
+        return get_related_energyml_modules_name(type(cls))
+    return []
+
+
+def dict_energyml_modules() -> List:
+    """
+    List all accessible energyml python modules
+    :return:
+    """
+    modules = {}
+
+    energyml_module = importlib.import_module("energyml")
+    # print("> energyml")
+
+    for mod in pkgutil.iter_modules(energyml_module.__path__):
+        # print(f"{mod.name}")
+        if mod.name in ENERGYML_MODULES_NAMES:
+            energyml_sub_module = importlib.import_module(
+                f"energyml.{mod.name}"
+            )
+            if mod.name not in modules:
+                modules[mod.name] = []
+            for sub_mod in pkgutil.iter_modules(energyml_sub_module.__path__):
+                modules[mod.name].append(sub_mod.name)
+                # modules[mod.name].append(re.sub(r"^\D*(?P<number>\d+(.\d+)*$)",
+                # r"\g<number>", sub_mod.name).replace("_", "."))
+    return modules
+
+
+def list_energyml_modules():
+    try:
+        energyml_module = importlib.import_module("energyml")
+        modules = []
+        for obj in pkgutil.iter_modules(energyml_module.__path__):
+            # print(f"{obj.name}")
+            if obj.name in ENERGYML_MODULES_NAMES:
+                modules.append(obj.name)
+        return modules
+    except ModuleNotFoundError:
+        return []
+
+
+def list_classes(module_path: str) -> List:
+    """
+    List all accessible classes from a specific module
+    :param module_path:
+    :return:
+    """
+    try:
+        module = importlib.import_module(module_path)
+        class_list = []
+        for _, obj in inspect.getmembers(module):
+            if inspect.isclass(obj):
+                class_list.append(obj)
+        return class_list
+    except ModuleNotFoundError:
+        print(f"Err : module {module_path} not found")
+        return []
+
+
+def get_sub_classes(cls: type) -> List[type]:
+    """
+    Return all classes that extends the class :param:`cls`.
+    :param cls:
+    :return:
+    """
+    sub_classes = []
+    for related in get_related_energyml_modules_name(cls):
+        try:
+            module = importlib.import_module(related)
+            for _, obj in inspect.getmembers(module):
+                if inspect.isclass(obj) and cls in obj.__bases__:
+                    sub_classes.append(obj)
+                    sub_classes = sub_classes + get_sub_classes(obj)
+        except ModuleNotFoundError:
+            pass
+    return list(dict.fromkeys(sub_classes))
+
+
+def get_classes_matching_name(cls: type, name_rgx: str, re_flags=re.IGNORECASE,) -> List[type]:
+    """
+    Search a class matching the regex @re_flags. The search is the energyml packages related to the objet type @cls.
+    :param cls:
+    :param name_rgx:
+    :param re_flags:
+    :return:
+    """
+    match_classes = []
+    for related in get_related_energyml_modules_name(cls):
+        try:
+            module = importlib.import_module(related)
+            for _, obj in inspect.getmembers(module):
+                if inspect.isclass(obj) and re.match(name_rgx, obj.__name__, re_flags):
+                    match_classes.append(obj)
+        except ModuleNotFoundError:
+            pass
+    return list(dict.fromkeys(match_classes))
+
+
+def get_all_energyml_classes() -> dict:
+    result = {}
+    for mod_name, versions in dict_energyml_modules().items():
+        for version in versions:
+            result = result | get_all_classes(mod_name, version)
+    return result
+
+
+def get_all_classes(module_name: str, version: str) -> dict:
+    result = {}
+    pkg_path = f"energyml.{module_name}.{version}"
+    package = importlib.import_module(pkg_path)
+    for _, modname, _ in pkgutil.walk_packages(
+        path=getattr(package, "__path__"),
+        prefix=package.__name__ + ".",
+        onerror=lambda x: None,
+    ):
+        result[pkg_path] = []
+        for classFound in list_classes(modname):
+            try:
+                result[pkg_path].append(classFound)
+            except Exception:
+                pass
+
+    return result
+
+
+def get_class_pkg(cls):
+    try:
+        p = re.compile(REGEX_ENERGYML_MODULE_NAME)
+        m = p.search(cls.__module__)
+        return m.group("pkg")
+    except AttributeError as e:
+        print(f"Exception to get class package for '{cls}'")
+        raise e
+
+
+def reshape_version(version: str, nb_digit: int) -> str:
+    """
+    Reshape a project version to have only specific number of digits. If 0 < nbDigit < 4 then the reshape is done,
+    else, the original version is returned.
+    Example : reshapeVersion("v2.0.1", 2) ==> "2.0" and reshapeVersion("version2.0.1.3.2.5", 4) ==> "version2.0.1.3.2.5"
+    """
+    p = re.compile(REGEX_PROJECT_VERSION)
+    m = p.search(version)
+    if m is not None:
+        n0 = m.group("n0")
+        n1 = m.group("n1")
+        n2 = m.group("n2")
+        if nb_digit == 1:
+            return n0
+        elif nb_digit == 2:
+            return n0 + ("." + n1 if n1 is not None else "")
+        elif nb_digit == 3:
+            return n0 + (
+                "." + n1 + ("." + n2 if n2 is not None else "")
+                if n1 is not None
+                else ""
+            )
+
+    return version
+
+
+def get_class_pkg_version(
+    cls, print_dev_version: bool = True, nb_max_version_digits: int = 2
+):
+    p = re.compile(REGEX_ENERGYML_MODULE_NAME)
+    m = p.search(
+        cls.__module__ if isinstance(cls, type) else type(cls).__module__
+    )
+    return reshape_version(m.group("versionNumber"), nb_max_version_digits) + (
+        m.group("versionDev")
+        if m.group("versionDev") is not None and print_dev_version
+        else ""
+    )
+
+
+# ProtocolDict = DefaultDict[str, MessageDict]
+# def get_all__classes() -> ProtocolDict:
+#     protocolDict: ProtocolDict = defaultdict(
+#         lambda: defaultdict(type(ETPModel))
+#     )
+#     package = energyml
+#     for _, modname, _ in pkgutil.walk_packages(
+#         path=getattr(package, "__path__"),
+#         prefix=package.__name__ + ".",
+#         onerror=lambda x: None,
+#     ):
+#         for classFound in list_classes(modname):
+#             try:
+#                 schem = json.loads(avro_schema(classFound))
+#                 protocolId = schem["protocol"]
+#                 messageType = schem["messageType"]
+#                 protocolDict[protocolId][messageType] = classFound
+#             except Exception:
+#                 pass
+#     return protocolDict
+
+
+
+
+
+
+
+

Functions

+
+
+def dict_energyml_modules() ‑> List +
+
+

List all accessible energyml python modules +:return:

+
+ +Expand source code + +
def dict_energyml_modules() -> List:
+    """
+    List all accessible energyml python modules
+    :return:
+    """
+    modules = {}
+
+    energyml_module = importlib.import_module("energyml")
+    # print("> energyml")
+
+    for mod in pkgutil.iter_modules(energyml_module.__path__):
+        # print(f"{mod.name}")
+        if mod.name in ENERGYML_MODULES_NAMES:
+            energyml_sub_module = importlib.import_module(
+                f"energyml.{mod.name}"
+            )
+            if mod.name not in modules:
+                modules[mod.name] = []
+            for sub_mod in pkgutil.iter_modules(energyml_sub_module.__path__):
+                modules[mod.name].append(sub_mod.name)
+                # modules[mod.name].append(re.sub(r"^\D*(?P<number>\d+(.\d+)*$)",
+                # r"\g<number>", sub_mod.name).replace("_", "."))
+    return modules
+
+
+
+def get_all_classes(module_name: str, version: str) ‑> dict +
+
+
+
+ +Expand source code + +
def get_all_classes(module_name: str, version: str) -> dict:
+    result = {}
+    pkg_path = f"energyml.{module_name}.{version}"
+    package = importlib.import_module(pkg_path)
+    for _, modname, _ in pkgutil.walk_packages(
+        path=getattr(package, "__path__"),
+        prefix=package.__name__ + ".",
+        onerror=lambda x: None,
+    ):
+        result[pkg_path] = []
+        for classFound in list_classes(modname):
+            try:
+                result[pkg_path].append(classFound)
+            except Exception:
+                pass
+
+    return result
+
+
+
+def get_all_energyml_classes() ‑> dict +
+
+
+
+ +Expand source code + +
def get_all_energyml_classes() -> dict:
+    result = {}
+    for mod_name, versions in dict_energyml_modules().items():
+        for version in versions:
+            result = result | get_all_classes(mod_name, version)
+    return result
+
+
+
+def get_class_pkg(cls) +
+
+
+
+ +Expand source code + +
def get_class_pkg(cls):
+    try:
+        p = re.compile(REGEX_ENERGYML_MODULE_NAME)
+        m = p.search(cls.__module__)
+        return m.group("pkg")
+    except AttributeError as e:
+        print(f"Exception to get class package for '{cls}'")
+        raise e
+
+
+
+def get_class_pkg_version(cls, print_dev_version: bool = True, nb_max_version_digits: int = 2) +
+
+
+
+ +Expand source code + +
def get_class_pkg_version(
+    cls, print_dev_version: bool = True, nb_max_version_digits: int = 2
+):
+    p = re.compile(REGEX_ENERGYML_MODULE_NAME)
+    m = p.search(
+        cls.__module__ if isinstance(cls, type) else type(cls).__module__
+    )
+    return reshape_version(m.group("versionNumber"), nb_max_version_digits) + (
+        m.group("versionDev")
+        if m.group("versionDev") is not None and print_dev_version
+        else ""
+    )
+
+
+
+def get_classes_matching_name(cls: type, name_rgx: str, re_flags=re.IGNORECASE) ‑> List[type] +
+
+

Search a class matching the regex @re_flags. The search is the energyml packages related to the objet type @cls. +:param cls: +:param name_rgx: +:param re_flags: +:return:

+
+ +Expand source code + +
def get_classes_matching_name(cls: type, name_rgx: str, re_flags=re.IGNORECASE,) -> List[type]:
+    """
+    Search a class matching the regex @re_flags. The search is the energyml packages related to the objet type @cls.
+    :param cls:
+    :param name_rgx:
+    :param re_flags:
+    :return:
+    """
+    match_classes = []
+    for related in get_related_energyml_modules_name(cls):
+        try:
+            module = importlib.import_module(related)
+            for _, obj in inspect.getmembers(module):
+                if inspect.isclass(obj) and re.match(name_rgx, obj.__name__, re_flags):
+                    match_classes.append(obj)
+        except ModuleNotFoundError:
+            pass
+    return list(dict.fromkeys(match_classes))
+
+
+ +
+

Return the list of all energyml modules related to another one. +For example resqml 2.0.1 is related to common 2.0 +:param cls: +:return:

+
+ +Expand source code + +
def get_related_energyml_modules_name(cls: Union[type, Any]) -> List[str]:
+    """
+    Return the list of all energyml modules related to another one.
+    For example resqml 2.0.1 is related to common 2.0
+    :param cls:
+    :return:
+    """
+    if isinstance(cls, type):
+        for related in RELATED_MODULES:
+            if cls.__module__ in related:
+                return related
+    else:
+        return get_related_energyml_modules_name(type(cls))
+    return []
+
+
+
+def get_sub_classes(cls: type) ‑> List[type] +
+
+

Return all classes that extends the class :param:cls. +:param cls: +:return:

+
+ +Expand source code + +
def get_sub_classes(cls: type) -> List[type]:
+    """
+    Return all classes that extends the class :param:`cls`.
+    :param cls:
+    :return:
+    """
+    sub_classes = []
+    for related in get_related_energyml_modules_name(cls):
+        try:
+            module = importlib.import_module(related)
+            for _, obj in inspect.getmembers(module):
+                if inspect.isclass(obj) and cls in obj.__bases__:
+                    sub_classes.append(obj)
+                    sub_classes = sub_classes + get_sub_classes(obj)
+        except ModuleNotFoundError:
+            pass
+    return list(dict.fromkeys(sub_classes))
+
+
+
+def list_classes(module_path: str) ‑> List +
+
+

List all accessible classes from a specific module +:param module_path: +:return:

+
+ +Expand source code + +
def list_classes(module_path: str) -> List:
+    """
+    List all accessible classes from a specific module
+    :param module_path:
+    :return:
+    """
+    try:
+        module = importlib.import_module(module_path)
+        class_list = []
+        for _, obj in inspect.getmembers(module):
+            if inspect.isclass(obj):
+                class_list.append(obj)
+        return class_list
+    except ModuleNotFoundError:
+        print(f"Err : module {module_path} not found")
+        return []
+
+
+
+def list_energyml_modules() +
+
+
+
+ +Expand source code + +
def list_energyml_modules():
+    try:
+        energyml_module = importlib.import_module("energyml")
+        modules = []
+        for obj in pkgutil.iter_modules(energyml_module.__path__):
+            # print(f"{obj.name}")
+            if obj.name in ENERGYML_MODULES_NAMES:
+                modules.append(obj.name)
+        return modules
+    except ModuleNotFoundError:
+        return []
+
+
+
+def reshape_version(version: str, nb_digit: int) ‑> str +
+
+

Reshape a project version to have only specific number of digits. If 0 < nbDigit < 4 then the reshape is done, +else, the original version is returned. +Example : reshapeVersion("v2.0.1", 2) ==> "2.0" and reshapeVersion("version2.0.1.3.2.5", 4) ==> "version2.0.1.3.2.5"

+
+ +Expand source code + +
def reshape_version(version: str, nb_digit: int) -> str:
+    """
+    Reshape a project version to have only specific number of digits. If 0 < nbDigit < 4 then the reshape is done,
+    else, the original version is returned.
+    Example : reshapeVersion("v2.0.1", 2) ==> "2.0" and reshapeVersion("version2.0.1.3.2.5", 4) ==> "version2.0.1.3.2.5"
+    """
+    p = re.compile(REGEX_PROJECT_VERSION)
+    m = p.search(version)
+    if m is not None:
+        n0 = m.group("n0")
+        n1 = m.group("n1")
+        n2 = m.group("n2")
+        if nb_digit == 1:
+            return n0
+        elif nb_digit == 2:
+            return n0 + ("." + n1 if n1 is not None else "")
+        elif nb_digit == 3:
+            return n0 + (
+                "." + n1 + ("." + n2 if n2 is not None else "")
+                if n1 is not None
+                else ""
+            )
+
+    return version
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/serialization.html b/energyml-utils/docs/src/energyml/utils/serialization.html new file mode 100644 index 0000000..bad0235 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/serialization.html @@ -0,0 +1,305 @@ + + + + + + +src.energyml.utils.serialization API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.serialization

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+from io import BytesIO
+from typing import Optional, Any
+
+import energyml
+from xsdata.exceptions import ParserError
+from xsdata.formats.dataclass.context import XmlContext
+from xsdata.formats.dataclass.parsers import XmlParser
+from xsdata.formats.dataclass.serializers import JsonSerializer
+from xsdata.formats.dataclass.serializers import XmlSerializer
+from xsdata.formats.dataclass.serializers.config import SerializerConfig
+
+from .introspection import get_class_from_name
+from .xml import get_class_name_from_xml, get_tree
+
+
+def read_energyml_xml_bytes_as_class(file: bytes, obj_class: type) -> Any:
+    """
+    Read an xml file into the instance of type :param:`obj_class`.
+    :param file:
+    :param obj_class:
+    :return:
+    """
+    parser = XmlParser()
+    try:
+        return parser.from_bytes(file, obj_class)
+    except ParserError as e:
+        print(f"Failed to parse file {file} as class {obj_class}")
+        raise e
+
+
+def read_energyml_xml_bytes(file: bytes) -> Any:
+    """
+    Read an xml file. The type of object is searched from the xml root name.
+    :param file:
+    :return:
+    """
+    return read_energyml_xml_bytes_as_class(
+        file, get_class_from_name(get_class_name_from_xml(get_tree(file)))
+    )
+
+
+def read_energyml_xml_io(file: BytesIO, obj_class: Optional[type] = None) -> Any:
+    if obj_class is not None:
+        return read_energyml_xml_bytes_as_class(file.getbuffer(), obj_class)
+    else:
+        return read_energyml_xml_bytes(file.getbuffer())
+
+
+def read_energyml_xml_str(file_content: str) -> Any:
+    parser = XmlParser()
+    # from energyml.resqml.v2_2.resqmlv2 import TriangulatedSetRepresentation
+    return parser.from_string(
+        file_content,
+        get_class_from_name(get_class_name_from_xml(get_tree(file_content))),
+    )  # , TriangulatedSetRepresentation)
+
+
+def read_energyml_xml_file(file_path: str) -> Any:
+    xml_content = ""
+    with open(file_path, "r") as f:
+        xml_content = f.read()
+    parser = XmlParser()
+    # from energyml.resqml.v2_2.resqmlv2 import TriangulatedSetRepresentation
+    # return parser.parse(file_path)  # , TriangulatedSetRepresentation)
+    return parser.parse(
+        file_path,
+        get_class_from_name(get_class_name_from_xml(get_tree(xml_content))),
+    )
+
+
+def serialize_xml(obj) -> str:
+    context = XmlContext(
+        # element_name_generator=text.camel_case,
+        # attribute_name_generator=text.kebab_case
+    )
+    serializer_config = SerializerConfig(indent="  ")
+    serializer = XmlSerializer(context=context, config=serializer_config)
+    return serializer.render(obj)
+
+
+def serialize_json(obj) -> str:
+    context = XmlContext(
+        # element_name_generator=text.camel_case,
+        # attribute_name_generator=text.kebab_case
+    )
+    serializer_config = SerializerConfig(indent="  ")
+    serializer = JsonSerializer(context=context, config=serializer_config)
+    return serializer.render(obj)
+
+
+
+
+
+
+
+

Functions

+
+
+def read_energyml_xml_bytes(file: bytes) ‑> Any +
+
+

Read an xml file. The type of object is searched from the xml root name. +:param file: +:return:

+
+ +Expand source code + +
def read_energyml_xml_bytes(file: bytes) -> Any:
+    """
+    Read an xml file. The type of object is searched from the xml root name.
+    :param file:
+    :return:
+    """
+    return read_energyml_xml_bytes_as_class(
+        file, get_class_from_name(get_class_name_from_xml(get_tree(file)))
+    )
+
+
+
+def read_energyml_xml_bytes_as_class(file: bytes, obj_class: type) ‑> Any +
+
+

Read an xml file into the instance of type :param:obj_class. +:param file: +:param obj_class: +:return:

+
+ +Expand source code + +
def read_energyml_xml_bytes_as_class(file: bytes, obj_class: type) -> Any:
+    """
+    Read an xml file into the instance of type :param:`obj_class`.
+    :param file:
+    :param obj_class:
+    :return:
+    """
+    parser = XmlParser()
+    try:
+        return parser.from_bytes(file, obj_class)
+    except ParserError as e:
+        print(f"Failed to parse file {file} as class {obj_class}")
+        raise e
+
+
+
+def read_energyml_xml_file(file_path: str) ‑> Any +
+
+
+
+ +Expand source code + +
def read_energyml_xml_file(file_path: str) -> Any:
+    xml_content = ""
+    with open(file_path, "r") as f:
+        xml_content = f.read()
+    parser = XmlParser()
+    # from energyml.resqml.v2_2.resqmlv2 import TriangulatedSetRepresentation
+    # return parser.parse(file_path)  # , TriangulatedSetRepresentation)
+    return parser.parse(
+        file_path,
+        get_class_from_name(get_class_name_from_xml(get_tree(xml_content))),
+    )
+
+
+
+def read_energyml_xml_io(file: _io.BytesIO, obj_class: Optional[type] = None) ‑> Any +
+
+
+
+ +Expand source code + +
def read_energyml_xml_io(file: BytesIO, obj_class: Optional[type] = None) -> Any:
+    if obj_class is not None:
+        return read_energyml_xml_bytes_as_class(file.getbuffer(), obj_class)
+    else:
+        return read_energyml_xml_bytes(file.getbuffer())
+
+
+
+def read_energyml_xml_str(file_content: str) ‑> Any +
+
+
+
+ +Expand source code + +
def read_energyml_xml_str(file_content: str) -> Any:
+    parser = XmlParser()
+    # from energyml.resqml.v2_2.resqmlv2 import TriangulatedSetRepresentation
+    return parser.from_string(
+        file_content,
+        get_class_from_name(get_class_name_from_xml(get_tree(file_content))),
+    )  # , TriangulatedSetRepresentation)
+
+
+
+def serialize_json(obj) ‑> str +
+
+
+
+ +Expand source code + +
def serialize_json(obj) -> str:
+    context = XmlContext(
+        # element_name_generator=text.camel_case,
+        # attribute_name_generator=text.kebab_case
+    )
+    serializer_config = SerializerConfig(indent="  ")
+    serializer = JsonSerializer(context=context, config=serializer_config)
+    return serializer.render(obj)
+
+
+
+def serialize_xml(obj) ‑> str +
+
+
+
+ +Expand source code + +
def serialize_xml(obj) -> str:
+    context = XmlContext(
+        # element_name_generator=text.camel_case,
+        # attribute_name_generator=text.kebab_case
+    )
+    serializer_config = SerializerConfig(indent="  ")
+    serializer = XmlSerializer(context=context, config=serializer_config)
+    return serializer.render(obj)
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/validation.html b/energyml-utils/docs/src/energyml/utils/validation.html new file mode 100644 index 0000000..e5a5fca --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/validation.html @@ -0,0 +1,984 @@ + + + + + + +src.energyml.utils.validation API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.validation

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import re
+from dataclasses import dataclass, field, Field
+from enum import Enum
+from typing import Any, List
+
+from .epc import (
+    get_obj_identifier, Epc,
+)
+from .introspection import (
+    get_class_fields,
+    get_object_attribute,
+    search_attribute_matching_type_with_path,
+    get_object_attribute_no_verif,
+    get_object_attribute_rgx,
+    get_matching_class_attribute_name, get_obj_uuid, get_obj_version, get_content_type_from_class,
+    get_qualified_type_from_class,
+)
+
+
+class ErrorType(Enum):
+    CRITICAL = "critical"
+    DEBUG = "debug"
+    INFO = "info"
+    WARNING = "warning"
+
+
+@dataclass
+class ValidationError:
+
+    msg: str = field(default="Validation error")
+
+    error_type: ErrorType = field(default=ErrorType.INFO)
+
+    def __str__(self):
+        return f"[{str(self.error_type).upper()}] : {self.msg}"
+
+
+@dataclass
+class ValidationObjectError(ValidationError):
+
+    target_obj: Any = field(default=None)
+
+    attribute_dot_path: str = field(default=None)
+
+    def __str__(self):
+        return f"{ValidationError.__str__(self)}\n\t{get_obj_identifier(self.target_obj)} : '{self.attribute_dot_path}'"
+
+
+@dataclass
+class MandatoryError(ValidationObjectError):
+    def __str__(self):
+        return f"{ValidationError.__str__(self)}\n\tMandatory value is None for {get_obj_identifier(self.target_obj)} : '{self.attribute_dot_path}'"
+
+
+def validate_epc(epc: Epc) -> List[ValidationError]:
+    """
+    Verify if all :param:`epc`'s objects are valid.
+    :param epc:
+    :return:
+    """
+    errs = []
+    for obj in epc.energyml_objects:
+        errs = errs + patterns_verification(obj)
+
+    errs = errs + dor_verification(epc.energyml_objects)
+
+    return errs
+
+
+def dor_verification(energyml_objects: List[Any]) -> List[ValidationError]:
+    """
+    Verification for DOR. An error is raised if DORs contains wrong information, or if a referenced object is unknown
+    in the :param:`epc`.
+    :param energyml_objects:
+    :return:
+    """
+    errs = []
+
+    dict_obj_identifier = {
+        get_obj_identifier(obj): obj for obj in energyml_objects
+    }
+    dict_obj_uuid = {}
+    for obj in energyml_objects:
+        uuid = get_obj_uuid(obj)
+        if uuid not in dict_obj_uuid:
+            dict_obj_uuid[uuid] = []
+        dict_obj_uuid[uuid].append(obj)
+
+    # TODO: chercher dans les objets les AbstractObject (en Witsml des sous objet peuvent etre aussi references)
+
+    for obj in energyml_objects:
+        dor_list = search_attribute_matching_type_with_path(
+            obj, "DataObjectReference"
+        )
+        for dor_path, dor in dor_list:
+            dor_target_id = get_obj_identifier(dor)
+            if dor_target_id not in dict_obj_identifier:
+                dor_uuid = get_obj_uuid(dor)
+                dor_version = get_obj_version(dor)
+                if dor_uuid not in dict_obj_uuid:
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Unkown object with uuid '{dor_uuid}'",
+                        )
+                    )
+                else:
+                    accessible_version = [
+                        get_obj_version(ref_obj)
+                        for ref_obj in dict_obj_uuid[dor_uuid]
+                    ]
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Unkown object version '{dor_version}'. "
+                            f"Version must be one of {accessible_version}",
+                        )
+                    )
+            else:
+                target = dict_obj_identifier[dor_target_id]
+                target_title = get_object_attribute_rgx(
+                    target, "citation.title"
+                )
+                target_content_type = get_content_type_from_class(target)
+                target_qualified_type = get_qualified_type_from_class(target)
+
+                dor_title = get_object_attribute_rgx(dor, "title")
+
+                if dor_title != target_title:
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Title should be '{target_title}' and not '{dor_title}'",
+                        )
+                    )
+
+                if (
+                    get_matching_class_attribute_name(dor, "content_type")
+                    is not None
+                ):
+                    dor_content_type = get_object_attribute_no_verif(
+                        dor, "content_type"
+                    )
+                    if dor_content_type != target_content_type:
+                        errs.append(
+                            ValidationObjectError(
+                                error_type=ErrorType.CRITICAL,
+                                target_obj=obj,
+                                attribute_dot_path=dor_path,
+                                msg=f"[DOR ERR] has wrong information. ContentType should be '{target_content_type}' and not '{dor_content_type}'",
+                            )
+                        )
+
+                if (
+                    get_matching_class_attribute_name(dor, "qualified_type")
+                    is not None
+                ):
+                    dor_qualified_type = get_object_attribute_no_verif(
+                        dor, "qualified_type"
+                    )
+                    if dor_qualified_type != target_qualified_type:
+                        errs.append(
+                            ValidationObjectError(
+                                error_type=ErrorType.CRITICAL,
+                                target_obj=obj,
+                                attribute_dot_path=dor_path,
+                                msg=f"[DOR ERR] has wrong information. QualifiedType should be '{target_qualified_type}' and not '{dor_qualified_type}'",
+                            )
+                        )
+
+    return errs
+
+
+def patterns_verification(obj: Any) -> List[ValidationError]:
+    """
+    Verification on object values, using the patterns defined in the original energyml xsd files.
+    :param obj:
+    :return:
+    """
+    return _patterns_verification(obj, obj, "")
+
+
+def _patterns_verification(
+    obj: Any, root_obj: Any, current_attribute_dot_path: str = ""
+) -> List[ValidationError]:
+    """
+    Verification on object values, using the patterns defined in the original energyml xsd files.
+    :param obj:
+    :param root_obj:
+    :param current_attribute_dot_path:
+    :return:
+    """
+    error_list = []
+
+    if isinstance(obj, list):
+        cpt = 0
+        for val in obj:
+            error_list = error_list + _patterns_verification(
+                val, root_obj, f"{current_attribute_dot_path}.{cpt}"
+            )
+            cpt = cpt + 1
+    elif isinstance(obj, dict):
+        for k, val in obj.items():
+            error_list = error_list + _patterns_verification(
+                val, root_obj, f"{current_attribute_dot_path}.{k}"
+            )
+    else:
+        # print(get_class_fields(obj))
+        for att_name, att_field in get_class_fields(obj).items():
+            # print(f"att_name : {att_field.metadata}")
+            error_list = error_list + validate_attribute(
+                get_object_attribute(obj, att_name, False),
+                root_obj,
+                att_field,
+                f"{current_attribute_dot_path}.{att_name}",
+            )
+
+    return error_list
+
+
+def validate_attribute(
+    value: Any, root_obj: Any, att_field: Field, path: str
+) -> List[ValidationError]:
+    errs = []
+
+    if value is None:
+        if att_field.metadata.get("required", False):
+            errs.append(
+                MandatoryError(
+                    error_type=ErrorType.CRITICAL,
+                    target_obj=root_obj,
+                    attribute_dot_path=path,
+                )
+            )
+    else:
+        min_length = att_field.metadata.get("min_length", None)
+        max_length = att_field.metadata.get("max_length", None)
+        pattern = att_field.metadata.get("pattern", None)
+        min_occurs = att_field.metadata.get("pattern", None)
+        min_inclusive = att_field.metadata.get("pattern", None)
+        # white_space
+
+        if max_length is not None:
+            length = len(value)
+            if length > max_length:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Max length was {max_length} but found {length}",
+                    )
+                )
+
+        if min_length is not None:
+            length = len(value)
+            if length < min_length:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Max length was {min_length} but found {length}",
+                    )
+                )
+
+        if min_occurs is not None:
+            if isinstance(value, list) and min_occurs > len(value):
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Min occurs was {min_occurs} but found {len(value)}",
+                    )
+                )
+
+        if min_inclusive is not None:
+            potential_err = ValidationObjectError(
+                error_type=ErrorType.CRITICAL,
+                target_obj=root_obj,
+                attribute_dot_path=path,
+                msg=f"Min occurs was {min_inclusive} but found {len(value)}",
+            )
+            if isinstance(value, list):
+                for val in value:
+                    if (
+                            (isinstance(val, str) and len(val) > min_inclusive)
+                            or ((isinstance(val, int) or isinstance(val, float)) and val > min_inclusive)
+                    ):
+                        errs.append(potential_err)
+
+        if pattern is not None:
+            if re.match(pattern, value) is None:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Pattern error. Value '{value}' was supposed to respect pattern '{pattern}'",
+                    )
+                )
+
+    return errs + _patterns_verification(
+        obj=value,
+        root_obj=root_obj,
+        current_attribute_dot_path=path,
+    )
+
+
+def correct_dor(energyml_objects: List[Any]) -> None:
+    """
+    Fix DOR errors (missing object_version, wrong title, wrong content-type/qualified-type ...)
+    :param energyml_objects:
+    :return:
+    """
+    dict_obj_identifier = {
+        get_obj_identifier(obj): obj for obj in energyml_objects
+    }
+    dict_obj_uuid = {}
+    for obj in energyml_objects:
+        uuid = get_obj_uuid(obj)
+        if uuid not in dict_obj_uuid:
+            dict_obj_uuid[uuid] = []
+        dict_obj_uuid[uuid].append(obj)
+
+    # TODO: chercher dans les objets les AbstractObject (en Witsml des sous objet peuvent etre aussi references)
+
+    for obj in energyml_objects:
+        dor_list = search_attribute_matching_type_with_path(
+            obj, "DataObjectReference"
+        )
+        for dor_path, dor in dor_list:
+            dor_target_id = get_obj_identifier(dor)
+            if dor_target_id in dict_obj_identifier:
+                target = dict_obj_identifier[dor_target_id]
+                target_title = get_object_attribute_rgx(
+                    target, "citation.title"
+                )
+                target_content_type = get_content_type_from_class(target)
+                target_qualified_type = get_qualified_type_from_class(target)
+
+                dor_title = get_object_attribute_rgx(dor, "title")
+
+                if dor_title != target_title:
+                    dor.title = target_title
+
+                if (
+                    get_matching_class_attribute_name(dor, "content_type")
+                    is not None
+                ):
+                    dor_content_type = get_object_attribute_no_verif(
+                        dor, "content_type"
+                    )
+                    if dor_content_type != target_content_type:
+                        dor.content_type = target_content_type
+
+                if (
+                    get_matching_class_attribute_name(dor, "qualified_type")
+                    is not None
+                ):
+                    dor_qualified_type = get_object_attribute_no_verif(
+                        dor, "qualified_type"
+                    )
+                    if dor_qualified_type != target_qualified_type:
+                        dor.qualified_type = target_qualified_type
+
+
+
+
+
+
+
+

Functions

+
+
+def correct_dor(energyml_objects: List[Any]) ‑> None +
+
+

Fix DOR errors (missing object_version, wrong title, wrong content-type/qualified-type …) +:param energyml_objects: +:return:

+
+ +Expand source code + +
def correct_dor(energyml_objects: List[Any]) -> None:
+    """
+    Fix DOR errors (missing object_version, wrong title, wrong content-type/qualified-type ...)
+    :param energyml_objects:
+    :return:
+    """
+    dict_obj_identifier = {
+        get_obj_identifier(obj): obj for obj in energyml_objects
+    }
+    dict_obj_uuid = {}
+    for obj in energyml_objects:
+        uuid = get_obj_uuid(obj)
+        if uuid not in dict_obj_uuid:
+            dict_obj_uuid[uuid] = []
+        dict_obj_uuid[uuid].append(obj)
+
+    # TODO: chercher dans les objets les AbstractObject (en Witsml des sous objet peuvent etre aussi references)
+
+    for obj in energyml_objects:
+        dor_list = search_attribute_matching_type_with_path(
+            obj, "DataObjectReference"
+        )
+        for dor_path, dor in dor_list:
+            dor_target_id = get_obj_identifier(dor)
+            if dor_target_id in dict_obj_identifier:
+                target = dict_obj_identifier[dor_target_id]
+                target_title = get_object_attribute_rgx(
+                    target, "citation.title"
+                )
+                target_content_type = get_content_type_from_class(target)
+                target_qualified_type = get_qualified_type_from_class(target)
+
+                dor_title = get_object_attribute_rgx(dor, "title")
+
+                if dor_title != target_title:
+                    dor.title = target_title
+
+                if (
+                    get_matching_class_attribute_name(dor, "content_type")
+                    is not None
+                ):
+                    dor_content_type = get_object_attribute_no_verif(
+                        dor, "content_type"
+                    )
+                    if dor_content_type != target_content_type:
+                        dor.content_type = target_content_type
+
+                if (
+                    get_matching_class_attribute_name(dor, "qualified_type")
+                    is not None
+                ):
+                    dor_qualified_type = get_object_attribute_no_verif(
+                        dor, "qualified_type"
+                    )
+                    if dor_qualified_type != target_qualified_type:
+                        dor.qualified_type = target_qualified_type
+
+
+
+def dor_verification(energyml_objects: List[Any]) ‑> List[ValidationError] +
+
+

Verification for DOR. An error is raised if DORs contains wrong information, or if a referenced object is unknown +in the :param:epc. +:param energyml_objects: +:return:

+
+ +Expand source code + +
def dor_verification(energyml_objects: List[Any]) -> List[ValidationError]:
+    """
+    Verification for DOR. An error is raised if DORs contains wrong information, or if a referenced object is unknown
+    in the :param:`epc`.
+    :param energyml_objects:
+    :return:
+    """
+    errs = []
+
+    dict_obj_identifier = {
+        get_obj_identifier(obj): obj for obj in energyml_objects
+    }
+    dict_obj_uuid = {}
+    for obj in energyml_objects:
+        uuid = get_obj_uuid(obj)
+        if uuid not in dict_obj_uuid:
+            dict_obj_uuid[uuid] = []
+        dict_obj_uuid[uuid].append(obj)
+
+    # TODO: chercher dans les objets les AbstractObject (en Witsml des sous objet peuvent etre aussi references)
+
+    for obj in energyml_objects:
+        dor_list = search_attribute_matching_type_with_path(
+            obj, "DataObjectReference"
+        )
+        for dor_path, dor in dor_list:
+            dor_target_id = get_obj_identifier(dor)
+            if dor_target_id not in dict_obj_identifier:
+                dor_uuid = get_obj_uuid(dor)
+                dor_version = get_obj_version(dor)
+                if dor_uuid not in dict_obj_uuid:
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Unkown object with uuid '{dor_uuid}'",
+                        )
+                    )
+                else:
+                    accessible_version = [
+                        get_obj_version(ref_obj)
+                        for ref_obj in dict_obj_uuid[dor_uuid]
+                    ]
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Unkown object version '{dor_version}'. "
+                            f"Version must be one of {accessible_version}",
+                        )
+                    )
+            else:
+                target = dict_obj_identifier[dor_target_id]
+                target_title = get_object_attribute_rgx(
+                    target, "citation.title"
+                )
+                target_content_type = get_content_type_from_class(target)
+                target_qualified_type = get_qualified_type_from_class(target)
+
+                dor_title = get_object_attribute_rgx(dor, "title")
+
+                if dor_title != target_title:
+                    errs.append(
+                        ValidationObjectError(
+                            error_type=ErrorType.CRITICAL,
+                            target_obj=obj,
+                            attribute_dot_path=dor_path,
+                            msg=f"[DOR ERR] has wrong information. Title should be '{target_title}' and not '{dor_title}'",
+                        )
+                    )
+
+                if (
+                    get_matching_class_attribute_name(dor, "content_type")
+                    is not None
+                ):
+                    dor_content_type = get_object_attribute_no_verif(
+                        dor, "content_type"
+                    )
+                    if dor_content_type != target_content_type:
+                        errs.append(
+                            ValidationObjectError(
+                                error_type=ErrorType.CRITICAL,
+                                target_obj=obj,
+                                attribute_dot_path=dor_path,
+                                msg=f"[DOR ERR] has wrong information. ContentType should be '{target_content_type}' and not '{dor_content_type}'",
+                            )
+                        )
+
+                if (
+                    get_matching_class_attribute_name(dor, "qualified_type")
+                    is not None
+                ):
+                    dor_qualified_type = get_object_attribute_no_verif(
+                        dor, "qualified_type"
+                    )
+                    if dor_qualified_type != target_qualified_type:
+                        errs.append(
+                            ValidationObjectError(
+                                error_type=ErrorType.CRITICAL,
+                                target_obj=obj,
+                                attribute_dot_path=dor_path,
+                                msg=f"[DOR ERR] has wrong information. QualifiedType should be '{target_qualified_type}' and not '{dor_qualified_type}'",
+                            )
+                        )
+
+    return errs
+
+
+
+def patterns_verification(obj: Any) ‑> List[ValidationError] +
+
+

Verification on object values, using the patterns defined in the original energyml xsd files. +:param obj: +:return:

+
+ +Expand source code + +
def patterns_verification(obj: Any) -> List[ValidationError]:
+    """
+    Verification on object values, using the patterns defined in the original energyml xsd files.
+    :param obj:
+    :return:
+    """
+    return _patterns_verification(obj, obj, "")
+
+
+
+def validate_attribute(value: Any, root_obj: Any, att_field: dataclasses.Field, path: str) ‑> List[ValidationError] +
+
+
+
+ +Expand source code + +
def validate_attribute(
+    value: Any, root_obj: Any, att_field: Field, path: str
+) -> List[ValidationError]:
+    errs = []
+
+    if value is None:
+        if att_field.metadata.get("required", False):
+            errs.append(
+                MandatoryError(
+                    error_type=ErrorType.CRITICAL,
+                    target_obj=root_obj,
+                    attribute_dot_path=path,
+                )
+            )
+    else:
+        min_length = att_field.metadata.get("min_length", None)
+        max_length = att_field.metadata.get("max_length", None)
+        pattern = att_field.metadata.get("pattern", None)
+        min_occurs = att_field.metadata.get("pattern", None)
+        min_inclusive = att_field.metadata.get("pattern", None)
+        # white_space
+
+        if max_length is not None:
+            length = len(value)
+            if length > max_length:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Max length was {max_length} but found {length}",
+                    )
+                )
+
+        if min_length is not None:
+            length = len(value)
+            if length < min_length:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Max length was {min_length} but found {length}",
+                    )
+                )
+
+        if min_occurs is not None:
+            if isinstance(value, list) and min_occurs > len(value):
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Min occurs was {min_occurs} but found {len(value)}",
+                    )
+                )
+
+        if min_inclusive is not None:
+            potential_err = ValidationObjectError(
+                error_type=ErrorType.CRITICAL,
+                target_obj=root_obj,
+                attribute_dot_path=path,
+                msg=f"Min occurs was {min_inclusive} but found {len(value)}",
+            )
+            if isinstance(value, list):
+                for val in value:
+                    if (
+                            (isinstance(val, str) and len(val) > min_inclusive)
+                            or ((isinstance(val, int) or isinstance(val, float)) and val > min_inclusive)
+                    ):
+                        errs.append(potential_err)
+
+        if pattern is not None:
+            if re.match(pattern, value) is None:
+                errs.append(
+                    ValidationObjectError(
+                        error_type=ErrorType.CRITICAL,
+                        target_obj=root_obj,
+                        attribute_dot_path=path,
+                        msg=f"Pattern error. Value '{value}' was supposed to respect pattern '{pattern}'",
+                    )
+                )
+
+    return errs + _patterns_verification(
+        obj=value,
+        root_obj=root_obj,
+        current_attribute_dot_path=path,
+    )
+
+
+
+def validate_epc(epc: Epc) ‑> List[ValidationError] +
+
+

Verify if all :param:epc's objects are valid. +:param epc: +:return:

+
+ +Expand source code + +
def validate_epc(epc: Epc) -> List[ValidationError]:
+    """
+    Verify if all :param:`epc`'s objects are valid.
+    :param epc:
+    :return:
+    """
+    errs = []
+    for obj in epc.energyml_objects:
+        errs = errs + patterns_verification(obj)
+
+    errs = errs + dor_verification(epc.energyml_objects)
+
+    return errs
+
+
+
+
+
+

Classes

+
+
+class ErrorType +(*args, **kwds) +
+
+

Create a collection of name/value pairs.

+

Example enumeration:

+
>>> class Color(Enum):
+...     RED = 1
+...     BLUE = 2
+...     GREEN = 3
+
+

Access them by:

+
    +
  • attribute access::
  • +
+
>>> Color.RED
+<Color.RED: 1>
+
+
    +
  • value lookup:
  • +
+
>>> Color(1)
+<Color.RED: 1>
+
+
    +
  • name lookup:
  • +
+
>>> Color['RED']
+<Color.RED: 1>
+
+

Enumerations can be iterated over, and know how many members they have:

+
>>> len(Color)
+3
+
+
>>> list(Color)
+[<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]
+
+

Methods can be added to enumerations, and members can have their own +attributes – see the documentation for details.

+
+ +Expand source code + +
class ErrorType(Enum):
+    CRITICAL = "critical"
+    DEBUG = "debug"
+    INFO = "info"
+    WARNING = "warning"
+
+

Ancestors

+
    +
  • enum.Enum
  • +
+

Class variables

+
+
var CRITICAL
+
+
+
+
var DEBUG
+
+
+
+
var INFO
+
+
+
+
var WARNING
+
+
+
+
+
+
+class MandatoryError +(msg: str = 'Validation error', error_type: ErrorType = ErrorType.INFO, target_obj: Any = None, attribute_dot_path: str = None) +
+
+

MandatoryError(msg: str = 'Validation error', error_type: src.energyml.utils.validation.ErrorType = , target_obj: Any = None, attribute_dot_path: str = None)

+
+ +Expand source code + +
@dataclass
+class MandatoryError(ValidationObjectError):
+    def __str__(self):
+        return f"{ValidationError.__str__(self)}\n\tMandatory value is None for {get_obj_identifier(self.target_obj)} : '{self.attribute_dot_path}'"
+
+

Ancestors

+ +
+
+class ValidationError +(msg: str = 'Validation error', error_type: ErrorType = ErrorType.INFO) +
+
+

ValidationError(msg: str = 'Validation error', error_type: src.energyml.utils.validation.ErrorType = )

+
+ +Expand source code + +
@dataclass
+class ValidationError:
+
+    msg: str = field(default="Validation error")
+
+    error_type: ErrorType = field(default=ErrorType.INFO)
+
+    def __str__(self):
+        return f"[{str(self.error_type).upper()}] : {self.msg}"
+
+

Subclasses

+ +

Class variables

+
+
var error_typeErrorType
+
+
+
+
var msg : str
+
+
+
+
+
+
+class ValidationObjectError +(msg: str = 'Validation error', error_type: ErrorType = ErrorType.INFO, target_obj: Any = None, attribute_dot_path: str = None) +
+
+

ValidationObjectError(msg: str = 'Validation error', error_type: src.energyml.utils.validation.ErrorType = , target_obj: Any = None, attribute_dot_path: str = None)

+
+ +Expand source code + +
@dataclass
+class ValidationObjectError(ValidationError):
+
+    target_obj: Any = field(default=None)
+
+    attribute_dot_path: str = field(default=None)
+
+    def __str__(self):
+        return f"{ValidationError.__str__(self)}\n\t{get_obj_identifier(self.target_obj)} : '{self.attribute_dot_path}'"
+
+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attribute_dot_path : str
+
+
+
+
var target_obj : Any
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/energyml/utils/xml.html b/energyml-utils/docs/src/energyml/utils/xml.html new file mode 100644 index 0000000..2740f59 --- /dev/null +++ b/energyml-utils/docs/src/energyml/utils/xml.html @@ -0,0 +1,501 @@ + + + + + + +src.energyml.utils.xml API documentation + + + + + + + + + + + +
+
+
+

Module src.energyml.utils.xml

+
+
+
+ +Expand source code + +
# Copyright (c) 2023-2024 Geosiris.
+# SPDX-License-Identifier: Apache-2.0
+import re
+from io import BytesIO
+from typing import Optional, Any, Union
+
+from lxml import etree as ETREE  # type: Any
+
+ENERGYML_NAMESPACES = {
+    "eml": "http://www.energistics.org/energyml/data/commonv2",
+    "prodml": "http://www.energistics.org/energyml/data/prodmlv2",
+    "witsml": "http://www.energistics.org/energyml/data/witsmlv2",
+    "resqml": "http://www.energistics.org/energyml/data/resqmlv2",
+}
+"""
+dict of all energyml namespaces
+"""  # pylint: disable=W0105
+
+ENERGYML_NAMESPACES_PACKAGE = {
+    "eml": ["http://www.energistics.org/energyml/data/commonv2"],
+    "prodml": ["http://www.energistics.org/energyml/data/prodmlv2"],
+    "witsml": ["http://www.energistics.org/energyml/data/witsmlv2"],
+    "resqml": ["http://www.energistics.org/energyml/data/resqmlv2"],
+    "opc": [
+        "http://schemas.openxmlformats.org/package/2006/content-types",
+        "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
+    ],
+}
+"""
+dict of all energyml namespace packages
+"""  # pylint: disable=W0105
+
+REGEX_UUID_NO_GRP = (
+    r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
+)
+REGEX_UUID = r"(?P<uuid>" + REGEX_UUID_NO_GRP + ")"
+REGEX_DOMAIN_VERSION = r"(?P<domainVersion>(?P<versionNum>([\d]+[\._])*\d)\s*(?P<dev>dev\s*(?P<devNum>[\d]+))?)"
+REGEX_DOMAIN_VERSION_FLAT = r"(?P<domainVersion>(?P<versionNumFlat>([\d]+)*\d)\s*(?P<dev>dev\s*(?P<devNum>[\d]+))?)"
+
+
+# ContentType
+REGEX_MIME_TYPE_MEDIA = r"(?P<media>application|audio|font|example|image|message|model|multipart|text|video)"
+REGEX_CT_ENERGYML_DOMAIN = r"(?P<energymlDomain>x-(?P<domain>[\w]+)\+xml)"
+REGEX_CT_XML_DOMAIN = r"(?P<xmlRawDomain>(x\-)?(?P<xmlDomain>.+)\+xml)"
+REGEX_CT_TOKEN_VERSION = r"version=" + REGEX_DOMAIN_VERSION
+REGEX_CT_TOKEN_TYPE = r"type=(?P<type>[\w\_]+)"
+
+REGEX_CONTENT_TYPE = (
+        REGEX_MIME_TYPE_MEDIA + "/"
+        + "(?P<rawDomain>(" + REGEX_CT_ENERGYML_DOMAIN + ")|(" + REGEX_CT_XML_DOMAIN + r")|([\w-]+\.?)+)"
+        + "(;((" + REGEX_CT_TOKEN_VERSION + ")|(" + REGEX_CT_TOKEN_TYPE + ")))*"
+)
+REGEX_QUALIFIED_TYPE = (
+        r"(?P<domain>[a-zA-Z]+)" + REGEX_DOMAIN_VERSION_FLAT + r"\.(?P<type>[\w_]+)"
+)
+# =========
+
+REGEX_SCHEMA_VERSION = (
+        r"(?P<name>[eE]ml|[cC]ommon|[rR]esqml|[wW]itsml|[pP]rodml)?\s*v?"
+        + REGEX_DOMAIN_VERSION
+        + r"\s*$"
+)
+
+REGEX_ENERGYML_FILE_NAME_OLD = r"(?P<type>[\w]+)_" + REGEX_UUID_NO_GRP + r"\.xml$"
+REGEX_ENERGYML_FILE_NAME_NEW = (
+        REGEX_UUID_NO_GRP + r"\.(?P<objectVersion>\d+(\.\d+)*)\.xml$"
+)
+REGEX_ENERGYML_FILE_NAME = (
+    rf"^(.*/)?({REGEX_ENERGYML_FILE_NAME_OLD})|({REGEX_ENERGYML_FILE_NAME_NEW})"
+)
+
+REGEX_XML_HEADER = r"^\s*\<\?xml\s+((encoding\s*=\s*\"(?P<encoding>[^\"]+)\"|version\s*=\s*\"(?P<version>[^\"]+)\"|standalone\s*=\s*\"(?P<standalone>[^\"]+)\")\s+)+"
+
+
+def get_pkg_from_namespace(namespace: str) -> Optional[str]:
+    for (k, v) in ENERGYML_NAMESPACES_PACKAGE.items():
+        if namespace in v:
+            return k
+    return None
+
+
+def is_energyml_content_type(content_type: str) -> bool:
+    ct = parse_content_type(content_type)
+    return ct.group("domain") is not None
+
+
+def get_root_namespace(tree: ETREE.Element) -> str:
+    return tree.nsmap[tree.prefix]
+
+
+def get_class_name_from_xml(tree: ETREE.Element) -> str:
+    root_namespace = get_root_namespace(tree)
+    pkg = get_pkg_from_namespace(root_namespace)
+    if pkg is None:
+        print(f"No pkg found for elt {tree}")
+    else:
+        if pkg == "opc":
+            return "energyml.opc.opc." + get_root_type(tree)
+        else:
+            schema_version = find_schema_version_in_element(tree).replace(".", "_").replace("-", "_")
+            if pkg == "resqml" and schema_version == "2_0":
+                schema_version = "2_0_1"
+            return ("energyml." + pkg
+                    + ".v" + schema_version
+                    + "."
+                    + root_namespace[root_namespace.rindex("/") + 1:]
+                    + "." + get_root_type(tree)
+                    )
+
+
+def get_xml_encoding(xml_content: str) -> Optional[str]:
+    try:
+        m = re.search(REGEX_XML_HEADER, xml_content)
+        return m.group("encoding")
+    except AttributeError:
+        return "utf-8"
+
+
+def get_tree(xml_content: Union[bytes, str]) -> ETREE.Element:
+    xml_bytes = xml_content
+    if isinstance(xml_bytes, str):
+        xml_bytes = xml_content.encode(encoding=get_xml_encoding(xml_content).strip().lower())
+
+    return ETREE.parse(BytesIO(xml_bytes)).getroot()
+
+
+def energyml_xpath(tree: ETREE.Element, xpath: str) -> Optional[list]:
+    """A xpath research that knows energyml namespaces"""
+    try:
+        return ETREE.XPath(xpath, namespaces=ENERGYML_NAMESPACES)(tree)
+    except TypeError:
+        return None
+
+
+def search_element_has_child_xpath(tree: ETREE.Element, child_name: str) -> list:
+    """
+    Search elements that has a child named (xml tag) as 'child_name'.
+    Warning : child_name must contain the namespace (see. ENERGYML_NAMESPACES)
+    """
+    return list(x for x in energyml_xpath(tree, f"//{child_name}/.."))
+
+
+def get_uuid(tree: ETREE.Element) -> str:
+    _uuids = tree.xpath("@uuid")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@UUID")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@uid")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@UID")
+    return _uuids[0]
+
+
+def get_root_type(tree: ETREE.Element) -> str:
+    """ Returns the type (xml tag) of the element without the namespace """
+    return tree.xpath("local-name()")
+
+
+def find_schema_version_in_element(tree: ETREE.ElementTree) -> str:
+    """Find the "SchemaVersion" inside an xml content of a energyml file
+
+    :param tree: An energyml xml file content.
+    :type tree: bytes
+
+    :returns: The SchemaVersion that contains only the version number. For example, if the xml
+        file contains : SchemaVersion="Resqml 2.0.1"
+            the result will be : "2.0.1"
+    :rtype: str
+    """
+    _schema_version = tree.xpath("@schemaVersion")
+    if _schema_version is None:
+        _schema_version = tree.xpath("@SchemaVersion")
+
+    if _schema_version is not None:
+        match_version = re.search(r"\d+(\.\d+)*", _schema_version[0])
+        if match_version is not None:
+            return match_version.group(0)
+    return ""
+
+
+def parse_content_type(ct: str):
+    return re.search(REGEX_CONTENT_TYPE, ct)
+
+
+
+
+
+

Global variables

+
+
var ENERGYML_NAMESPACES
+
+

dict of all energyml namespaces

+
+
var ENERGYML_NAMESPACES_PACKAGE
+
+

dict of all energyml namespace packages

+
+
+
+
+

Functions

+
+
+def energyml_xpath(tree: , xpath: str) ‑> Optional[list] +
+
+

A xpath research that knows energyml namespaces

+
+ +Expand source code + +
def energyml_xpath(tree: ETREE.Element, xpath: str) -> Optional[list]:
+    """A xpath research that knows energyml namespaces"""
+    try:
+        return ETREE.XPath(xpath, namespaces=ENERGYML_NAMESPACES)(tree)
+    except TypeError:
+        return None
+
+
+
+def find_schema_version_in_element(tree: ) ‑> str +
+
+

Find the "SchemaVersion" inside an xml content of a energyml file

+

:param tree: An energyml xml file content. +:type tree: bytes

+

:returns: The SchemaVersion that contains only the version number. For example, if the xml +file contains : SchemaVersion="Resqml 2.0.1" +the result will be : "2.0.1" +:rtype: str

+
+ +Expand source code + +
def find_schema_version_in_element(tree: ETREE.ElementTree) -> str:
+    """Find the "SchemaVersion" inside an xml content of a energyml file
+
+    :param tree: An energyml xml file content.
+    :type tree: bytes
+
+    :returns: The SchemaVersion that contains only the version number. For example, if the xml
+        file contains : SchemaVersion="Resqml 2.0.1"
+            the result will be : "2.0.1"
+    :rtype: str
+    """
+    _schema_version = tree.xpath("@schemaVersion")
+    if _schema_version is None:
+        _schema_version = tree.xpath("@SchemaVersion")
+
+    if _schema_version is not None:
+        match_version = re.search(r"\d+(\.\d+)*", _schema_version[0])
+        if match_version is not None:
+            return match_version.group(0)
+    return ""
+
+
+
+def get_class_name_from_xml(tree: ) ‑> str +
+
+
+
+ +Expand source code + +
def get_class_name_from_xml(tree: ETREE.Element) -> str:
+    root_namespace = get_root_namespace(tree)
+    pkg = get_pkg_from_namespace(root_namespace)
+    if pkg is None:
+        print(f"No pkg found for elt {tree}")
+    else:
+        if pkg == "opc":
+            return "energyml.opc.opc." + get_root_type(tree)
+        else:
+            schema_version = find_schema_version_in_element(tree).replace(".", "_").replace("-", "_")
+            if pkg == "resqml" and schema_version == "2_0":
+                schema_version = "2_0_1"
+            return ("energyml." + pkg
+                    + ".v" + schema_version
+                    + "."
+                    + root_namespace[root_namespace.rindex("/") + 1:]
+                    + "." + get_root_type(tree)
+                    )
+
+
+
+def get_pkg_from_namespace(namespace: str) ‑> Optional[str] +
+
+
+
+ +Expand source code + +
def get_pkg_from_namespace(namespace: str) -> Optional[str]:
+    for (k, v) in ENERGYML_NAMESPACES_PACKAGE.items():
+        if namespace in v:
+            return k
+    return None
+
+
+
+def get_root_namespace(tree: ) ‑> str +
+
+
+
+ +Expand source code + +
def get_root_namespace(tree: ETREE.Element) -> str:
+    return tree.nsmap[tree.prefix]
+
+
+
+def get_root_type(tree: ) ‑> str +
+
+

Returns the type (xml tag) of the element without the namespace

+
+ +Expand source code + +
def get_root_type(tree: ETREE.Element) -> str:
+    """ Returns the type (xml tag) of the element without the namespace """
+    return tree.xpath("local-name()")
+
+
+
+def get_tree(xml_content: Union[bytes, str]) ‑>  +
+
+
+
+ +Expand source code + +
def get_tree(xml_content: Union[bytes, str]) -> ETREE.Element:
+    xml_bytes = xml_content
+    if isinstance(xml_bytes, str):
+        xml_bytes = xml_content.encode(encoding=get_xml_encoding(xml_content).strip().lower())
+
+    return ETREE.parse(BytesIO(xml_bytes)).getroot()
+
+
+
+def get_uuid(tree: ) ‑> str +
+
+
+
+ +Expand source code + +
def get_uuid(tree: ETREE.Element) -> str:
+    _uuids = tree.xpath("@uuid")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@UUID")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@uid")
+    if len(_uuids) <= 0:
+        _uuids = tree.xpath("@UID")
+    return _uuids[0]
+
+
+
+def get_xml_encoding(xml_content: str) ‑> Optional[str] +
+
+
+
+ +Expand source code + +
def get_xml_encoding(xml_content: str) -> Optional[str]:
+    try:
+        m = re.search(REGEX_XML_HEADER, xml_content)
+        return m.group("encoding")
+    except AttributeError:
+        return "utf-8"
+
+
+
+def is_energyml_content_type(content_type: str) ‑> bool +
+
+
+
+ +Expand source code + +
def is_energyml_content_type(content_type: str) -> bool:
+    ct = parse_content_type(content_type)
+    return ct.group("domain") is not None
+
+
+
+def parse_content_type(ct: str) +
+
+
+
+ +Expand source code + +
def parse_content_type(ct: str):
+    return re.search(REGEX_CONTENT_TYPE, ct)
+
+
+
+def search_element_has_child_xpath(tree: , child_name: str) ‑> list +
+
+

Search elements that has a child named (xml tag) as 'child_name'. +Warning : child_name must contain the namespace (see. ENERGYML_NAMESPACES)

+
+ +Expand source code + +
def search_element_has_child_xpath(tree: ETREE.Element, child_name: str) -> list:
+    """
+    Search elements that has a child named (xml tag) as 'child_name'.
+    Warning : child_name must contain the namespace (see. ENERGYML_NAMESPACES)
+    """
+    return list(x for x in energyml_xpath(tree, f"//{child_name}/.."))
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/docs/src/index.html b/energyml-utils/docs/src/index.html new file mode 100644 index 0000000..50b221a --- /dev/null +++ b/energyml-utils/docs/src/index.html @@ -0,0 +1,60 @@ + + + + + + +src API documentation + + + + + + + + + + + +
+
+
+

Package src

+
+
+
+
+

Sub-modules

+
+
src.energyml
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/energyml-utils/pyproject.toml b/energyml-utils/pyproject.toml index 0c0961b..d24ebcf 100644 --- a/energyml-utils/pyproject.toml +++ b/energyml-utils/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [tool.poetry] name = "energyml-utils" -version = "0.0.1.dev19+3ba1ef6" # Set at build time +version = "0.0.1.dev2" # Set at build time description = "Energyml helper" authors = [ "Valentin Gauthier " @@ -70,10 +70,7 @@ flake8 = "^4.0.0" black = "^22.3.0" pylint = "^2.7.2" click = ">=8.1.3, <=8.1.3" # upper version than 8.0.2 fail with black - -[tool.poetry.group.dev.dependencies] -pytest = "^8.1.1" -pytest-cov = "^4.1.0" +pdoc3 = "^0.10.0" [tool.coverage.run] branch = true @@ -103,7 +100,7 @@ exclude = ''' max-line-length = "88" [tool.poetry-dynamic-versioning] -enable = true +enable = false vcs = "git" style = "pep440" format-jinja = """ diff --git a/energyml-utils/src/energyml/utils/__init__.py b/energyml-utils/src/energyml/utils/__init__.py index fdee8bf..0749f5f 100644 --- a/energyml-utils/src/energyml/utils/__init__.py +++ b/energyml-utils/src/energyml/utils/__init__.py @@ -1,2 +1,21 @@ # Copyright (c) 2023-2024 Geosiris. -# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +# SPDX-License-Identifier: Apache-2.0 + +""" +The energyml.utils module. +It contains tools for energyml management. + +Please check the following module (depending on your need): + - energyml-opc + - energyml-common2-0 + - energyml-common2-1 + - energyml-common2-2 + - energyml-common2-3 + - energyml-resqml2-0-1 + - energyml-resqml2-2-dev3 + - energyml-resqml2-2 + - energyml-witsml2-0 + - energyml-witsml2-1 + - energyml-prodml2-0 + - energyml-prodml2-2 +""" \ No newline at end of file diff --git a/energyml-utils/src/energyml/utils/data/__init__.py b/energyml-utils/src/energyml/utils/data/__init__.py index fdee8bf..c2ed8e7 100644 --- a/energyml-utils/src/energyml/utils/data/__init__.py +++ b/energyml-utils/src/energyml/utils/data/__init__.py @@ -1,2 +1,8 @@ # Copyright (c) 2023-2024 Geosiris. -# SPDX-License-Identifier: Apache-2.0 \ No newline at end of file +# SPDX-License-Identifier: Apache-2.0 +""" +The data module. + +Contains functions to help the read of specific entities like Grid2DRepresentation, TriangulatedSetRepresentation etc. +It also contains functions to export data into OFF/OBJ format. +""" \ No newline at end of file diff --git a/energyml-utils/src/energyml/utils/epc.py b/energyml-utils/src/energyml/utils/epc.py index c528505..e7208d4 100644 --- a/energyml-utils/src/energyml/utils/epc.py +++ b/energyml-utils/src/energyml/utils/epc.py @@ -1,5 +1,14 @@ # Copyright (c) 2023-2024 Geosiris. # SPDX-License-Identifier: Apache-2.0 +""" +This example module shows various types of documentation available for use +with pydoc. To generate HTML documentation for this module issue the +command: + + pydoc -w foo + +""" + import datetime import re import zipfile