diff --git a/modernpython/cimdatatype_header.py b/modernpython/cimdatatype_header.py new file mode 100644 index 0000000..280725c --- /dev/null +++ b/modernpython/cimdatatype_header.py @@ -0,0 +1,3 @@ +from ..utils.datatypes import CIMDatatype +from ..utils.profile import Profile +from .enum import * diff --git a/modernpython/langPack.py b/modernpython/langPack.py index 60dd11f..a4bf9f9 100644 --- a/modernpython/langPack.py +++ b/modernpython/langPack.py @@ -36,9 +36,9 @@ def location(version): base = {"base_class": "Base", "class_location": location} template_files = [{"filename": "cimpy_class_template.mustache", "ext": ".py"}] -enum_template_files = [{"filename": "pydantic_enum_template.mustache", "ext": ".py"}] - -required_profiles = ["EQ", "GL"] #temporary +enum_template_files = [{"filename": "enum_class_template.mustache", "ext": ".py"}] +primitive_template_files = [{"filename": "primitive_template.mustache", "ext": ".py"}] +cimdatatype_template_files = [{"filename": "cimdatatype_template.mustache", "ext": ".py"}] def get_class_location(class_name, class_map, version): return f".{class_map[class_name].superClass()}" @@ -54,6 +54,43 @@ def get_class_location(class_name, class_map, version): partials = {} +def _primitive_to_dataType(datatype): + if datatype.lower() == "integer": + return "int" + if datatype.lower() == "boolean": + return "bool" + if datatype.lower() == "string": + return "str" + if datatype.lower() == "datetime": + return "datetime" + if datatype.lower() == "monthday": + return "str" # TO BE FIXED? + if datatype.lower() == "date": + return "date" + if datatype.lower() == "time": + return "time" + if datatype.lower() == "float": + return "float" + if datatype.lower() == "string": + return "str" + else: + # this actually never happens + return "float" + +def _compute_cim_data_type(attributes) -> tuple[str, str, str]: + type = None + unit = None + multiplier = None + for attribute in attributes: + if 'about' in attribute and attribute['about'] and "value" in attribute['about'] and 'class_name' in attribute: + type = _primitive_to_dataType(attribute['class_name']) + if 'about' in attribute and attribute['about'] and "multiplier" in attribute['about'] and 'isFixed' in attribute: + multiplier = "UnitMultiplier."+attribute['isFixed'] + if 'about' in attribute and attribute['about'] and "unit" in attribute['about'] and 'isFixed' in attribute: + unit = "UnitSymbol."+attribute['isFixed'] + return (type, unit, multiplier) + + # computes the data type def _compute_data_type(attribute): if "label" in attribute and attribute["label"] == "mRID": @@ -64,28 +101,7 @@ def _compute_data_type(attribute): elif "dataType" in attribute and "class_name" in attribute: # for whatever weird reason String is not created as class from CIMgen if is_primitive_class(attribute["class_name"]) or attribute["class_name"] == "String": - datatype = attribute["dataType"].split("#")[1].lower() - if datatype == "integer": - return "int" - if datatype == "boolean": - return "bool" - if datatype == "string": - return "str" - if datatype == "datetime": - return "datetime" - if datatype == "monthday": - return "str" # TO BE FIXED? - if datatype == "date": - return "str" # TO BE FIXED? - if datatype == "time": - return "time" - if datatype == "float": - return "float" - if datatype == "string": - return "str" - else: - # this actually never happens - return "float" + return _primitive_to_dataType(attribute["dataType"].split("#")[1]) # the assumption is that cim data type e.g. Voltage, ActivePower, always # maps to a float elif is_cim_data_type_class(attribute["class_name"]): @@ -263,24 +279,19 @@ def has_unit_attribute(attributes): return True return False -def is_required_profile(class_origin): - for origin in class_origin: - if origin["origin"] in required_profiles: - return True - return False - def run_template(version_path, class_details): - if ( + if class_details["class_name"] == 'PositionPoint': + #this class is created manually to support types conversions + return + elif class_details["is_a_primitive"] is True: # Primitives are never used in the in memory representation but only for # the schema - class_details["is_a_primitive"] is True + run_template_primitive(version_path, class_details, primitive_template_files) + elif class_details["is_a_cim_data_type"] is True: # Datatypes based on primitives are never used in the in memory # representation but only for the schema - or class_details["is_a_cim_data_type"] == True - or class_details["class_name"] == 'PositionPoint' - ): - return - elif class_details["has_instances"] == True: + run_template_cimdatatype(version_path, class_details, cimdatatype_template_files) + elif class_details["has_instances"] is True: run_template_enum(version_path, class_details, enum_template_files) else: run_template_schema(version_path, class_details, template_files) @@ -337,6 +348,56 @@ def run_template_schema(version_path, class_details, templates): output = chevron.render(**args) file.write(output) +def run_template_primitive(version_path, class_details, templates): + for template_info in templates: + class_file =Path(version_path, "resources", "primitives" + template_info["ext"]) + if not os.path.exists(class_file): + if not (parent:=class_file.parent).exists(): + parent.mkdir() + with open(class_file, "w", encoding="utf-8") as file: + schema_file_path = os.path.join( + os.getcwd(), "modernpython", "primitive_header.py" + ) + schema_file = open(schema_file_path, "r") + file.write(schema_file.read()) + with open(class_file, "a", encoding="utf-8") as file: + template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) + class_details["data_type"] = _primitive_to_dataType(class_details["class_name"]) + with open(template_path, encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) + +def run_template_cimdatatype(version_path, class_details, templates): + for template_info in templates: + class_file =Path(version_path, "resources", "cimdatatype" + template_info["ext"]) + if not os.path.exists(class_file): + if not (parent:=class_file.parent).exists(): + parent.mkdir() + with open(class_file, "w", encoding="utf-8") as file: + schema_file_path = os.path.join( + os.getcwd(), "modernpython", "cimdatatype_header.py" + ) + schema_file = open(schema_file_path, "r") + file.write(schema_file.read()) + with open(class_file, "a", encoding="utf-8") as file: + template_path = os.path.join(os.getcwd(), "modernpython/templates", template_info["filename"]) + class_details["data_type"] = _compute_cim_data_type(class_details["attributes"])[0] + class_details["unit"] = _compute_cim_data_type(class_details["attributes"])[1] + class_details["multiplier"] = _compute_cim_data_type(class_details["attributes"])[2] + with open(template_path, encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) + def resolve_headers(dest: str, version: str): """Add all classes in __init__.py""" @@ -349,25 +410,3 @@ def resolve_headers(dest: str, version: str): with open(dest / "__init__.py", "a", encoding="utf-8") as header_file: header_file.write("# pylint: disable=too-many-lines,missing-module-docstring\n") header_file.write(f"CGMES_VERSION='{version_number}'\n") - - # # Under this, add all imports in init. Disabled becasue loading 600 unneeded classes is slow. - # _all = ["CGMES_VERSION"] - - # for include_name in sorted(dest.glob("*.py")): - # stem = include_name.stem - # if stem in[ "__init__", "Base"]: - # continue - # _all.append(stem) - # header_file.write(f"from .{stem} import {stem}\n") - - # header_file.write( - # "\n".join( - # [ - # "# This is not needed per se, but by referencing all imports", - # "# this prevents a potential autoflake from cleaning up the whole file.", - # "# FYA, if __all__ is present, only what's in there will be import with a import *", - # "", - # ] - # ) - # ) - # header_file.write(f"__all__={_all}") diff --git a/modernpython/primitive_header.py b/modernpython/primitive_header.py new file mode 100644 index 0000000..50355ee --- /dev/null +++ b/modernpython/primitive_header.py @@ -0,0 +1,4 @@ +from datetime import date, datetime, time +from ..utils.datatypes import Primitive +from ..utils.profile import Profile +from .enum import * diff --git a/modernpython/templates/cimdatatype_template.mustache b/modernpython/templates/cimdatatype_template.mustache new file mode 100644 index 0000000..6ca8c7e --- /dev/null +++ b/modernpython/templates/cimdatatype_template.mustache @@ -0,0 +1,11 @@ + +""" +Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen +""" + +{{class_name}} = CIMDatatype("{{class_name}}", {{data_type}}, {{unit}}, {{multiplier}}, [{{#class_origin}}Profile.{{origin}},{{/class_origin}}]) + +""" +{{{wrapped_class_comment}}} +""" + diff --git a/modernpython/templates/pydantic_enum_template.mustache b/modernpython/templates/enum_class_template.mustache similarity index 100% rename from modernpython/templates/pydantic_enum_template.mustache rename to modernpython/templates/enum_class_template.mustache diff --git a/modernpython/templates/primitive_template.mustache b/modernpython/templates/primitive_template.mustache new file mode 100644 index 0000000..6dd305c --- /dev/null +++ b/modernpython/templates/primitive_template.mustache @@ -0,0 +1,11 @@ + +""" +Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen +""" + +{{class_name}} = Primitive("{{class_name}}",{{data_type}}, [{{#class_origin}}Profile.{{origin}}, {{/class_origin}}]) + +""" +{{{wrapped_class_comment}}} +""" + diff --git a/modernpython/utils/base.py b/modernpython/utils/base.py index 3fb558e..fd9dfcf 100644 --- a/modernpython/utils/base.py +++ b/modernpython/utils/base.py @@ -43,16 +43,10 @@ def to_dict(self, with_class: bool = True) -> dict[str, "CgmesAttributeTypes"]: """ attrs = {f.name: getattr(self, f.name) for f in fields(self)} - attrs["__class__"] = self.apparent_name() if with_class: - attrs["__class__"] = self.resource_name + attrs["__class__"] = self.apparent_name() return attrs - @cached_property - def resource_name(self) -> str: - """Returns the resource type.""" - return self.__class__.__name__ - @cached_property def namespace(self) -> str: """Returns the namespace. By default, the namespace is the cim namespace for all resources. @@ -71,7 +65,7 @@ def apparent_name(cls) -> str: def cgmes_attribute_names_in_profile(self, profile: BaseProfile | None) -> set[Field]: """ - Returns all fields accross the parent tree which are in the profile in parameter. + Returns all fields across the parent tree which are in the profile in parameter. Mostly useful during export to find all the attributes relevant to one profile only. diff --git a/modernpython/utils/datatypes.py b/modernpython/utils/datatypes.py new file mode 100644 index 0000000..620a4bd --- /dev/null +++ b/modernpython/utils/datatypes.py @@ -0,0 +1,51 @@ +import importlib +from dataclasses import Field, fields +from functools import cached_property +from typing import Any, TypeAlias, TypedDict + +from .constants import NAMESPACES +from pydantic.dataclasses import dataclass + +from .dataclassconfig import DataclassConfig +from .profile import BaseProfile +from ..resources.enum import UnitMultiplier, UnitSymbol + +@dataclass(config=DataclassConfig) +class Primitive: + + def __init__(self, name: str, type, profiles: set[BaseProfile]): + self.name = name + self.type = type + self.profiles = profiles + + def getName(self) -> str: + return self.name + + def getType(self): + return self.type + + @cached_property + def namespace(self) -> str: + """Returns the namespace. By default, the namespace is the cim namespace for all resources. + Custom resources can override this. + """ + return NAMESPACES["cim"] + + @cached_property + def getProfiles(self) -> set[BaseProfile]: + return self.profiles + +@dataclass(config=DataclassConfig) +class CIMDatatype(Primitive): + + def __init__(self, + name: str, type, symbol: UnitSymbol, multiplier: UnitMultiplier, profiles: set[BaseProfile]): + super().__init__(name, type, profiles) + self.multiplier = multiplier + self.symbol = symbol + + def getMultiplier(self) -> UnitMultiplier: + return self.multiplier + + def getSymbole(self) -> UnitSymbol: + return self.symbol \ No newline at end of file