Skip to content

Commit

Permalink
generate primitives and datatypes instances
Browse files Browse the repository at this point in the history
Signed-off-by: Federico M. Facca <[email protected]>

annotate datatype

Signed-off-by: Federico M. Facca <[email protected]>
  • Loading branch information
chicco785 committed Nov 28, 2023
1 parent 94e2493 commit 6947f4d
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 91 deletions.
7 changes: 6 additions & 1 deletion CIMgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import os
import textwrap
from time import time

from distutils.dir_util import remove_tree
from pathlib import Path
import xmltodict
from bs4 import BeautifulSoup

Expand Down Expand Up @@ -709,6 +710,10 @@ def cim_generate(directory, outputPath, version, langPack):
:param outputPath: CGMES version, e.g. version = "cgmes_v2_4_15"
:param langPack: python module containing language specific functions
"""

# clean directory
remove_tree(Path(outputPath))

profiles_array = []

t0 = time()
Expand Down
3 changes: 3 additions & 0 deletions modernpython/cimdatatype_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ..utils.datatypes import CIMDatatype
from ..utils.profile import Profile
from .enum import *
205 changes: 124 additions & 81 deletions modernpython/langPack.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,56 +36,75 @@ 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()}"
# Check if the current class has a parent class
""" if class_map[class_name].superClass():
if class_map[class_name].superClass() in class_map:
return "modernpython." + version + "." + class_map[class_name].superClass()
elif class_map[class_name].superClass() == "Base" or class_map[class_name].superClass() == None:
return location(version)
else:
return location(version) """


partials = {}

def _primitive_to_data_type(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? I could not find a datatype in python that holds only month and day.
if datatype.lower() == "date":
return "date"
# as of today no CIM model is using only time.
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]:
python_type = 'None'
unit = 'UnitSymbol.none'
multiplier = 'UnitMultiplier.none'
for attribute in attributes:
if 'about' in attribute and attribute['about'] and "value" in attribute['about'] and 'class_name' in attribute:
python_type = _primitive_to_data_type(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 (python_type, unit, multiplier)


# set the cim datatype for a field
def _set_cim_data_type(text, render) -> str:
attribute = eval(render(text))
if is_primitive_class(attribute["class_name"]):
return "data_type = " + attribute["class_name"] + ","
elif is_primitive_class(attribute["class_name"]) or attribute["class_name"] == "String":
return "data_type = String,"
elif is_cim_data_type_class(attribute["class_name"]):
return "data_type = " + attribute["class_name"] + ","
return ""

# computes the data type
def _compute_data_type(attribute):
if "label" in attribute and attribute["label"] == "mRID":
return "str"
elif "range" in attribute:
# return "'"+attribute["range"].split("#")[1]+"'"
return attribute["range"].split("#")[1]
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_data_type(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"]):
Expand Down Expand Up @@ -143,7 +162,7 @@ def _set_imports(text, render):
classes = set()
try:
res = eval(rendered)
except Exception as e:
except Exception:
pass
if res:
for val in res:
Expand Down Expand Up @@ -192,22 +211,22 @@ def _get_type_and_default(text, renderer) -> tuple[str, str]:
# default_factory.
attribute = eval(renderer(text))
datatype = _compute_data_type(attribute)
type = datatype
field_type = datatype
default = 'default=None'
if "multiplicity" in attribute:
multiplicity = attribute["multiplicity"]
if multiplicity in ["M:0..1"]:
type = "Optional[" + datatype + "]"
field_type = "Optional[" + datatype + "]"
elif multiplicity in ["M:0..n"]:
type = "Optional[List[" + datatype + "]]"
field_type = "Optional[List[" + datatype + "]]"
elif multiplicity in ["M:1..n", "M:2..n"]:
type = "List[" + datatype + "]"
field_type = "List[" + datatype + "]"
elif multiplicity in ["M:1"] and attribute['label'] == 'PowerSystemResources':
# Most probably there is a bug in the RDF that states multiplicity
# M:1 but should be M:1..N
type = "List[" + datatype + "]"
field_type = "List[" + datatype + "]"
else:
type = datatype
field_type = datatype

if "label" in attribute and attribute["label"] == "mRID":
default = "default_factory=uuid.uuid4"
Expand All @@ -219,15 +238,15 @@ def _get_type_and_default(text, renderer) -> tuple[str, str]:
default = 'default_factory=list'
elif multiplicity in ["M:0..n"] or multiplicity in ["M:1..n"]:
default = 'default_factory=list'
elif type == 'int':
elif field_type == 'int':
default = 'default=0'
elif type == 'str':
elif field_type == 'str':
default = 'default=""'
elif type == 'float':
elif field_type == 'float':
default = 'default=0.0'
elif type == 'bool':
elif field_type == 'bool':
default = 'default=False'
return (type, default)
return (field_type, default)


def set_enum_classes(new_enum_classes):
Expand Down Expand Up @@ -263,24 +282,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)
Expand Down Expand Up @@ -328,6 +342,57 @@ def run_template_schema(version_path, class_details, templates):
class_details["setImports"] = _set_imports
class_details["setValidator"] = _set_validator
class_details["setNormalizedName"] = _set_normalized_name
class_details["setCimDataType"] = _set_cim_data_type
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_primitive(version_path, class_details, templates):
for template_info in templates:
class_file =Path(version_path, "resources", "primitive" + 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_data_type(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,
Expand All @@ -349,25 +414,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}")
6 changes: 6 additions & 0 deletions modernpython/primitive_header.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from datetime import date, datetime, time
from ..utils.datatypes import Primitive
from ..utils.profile import Profile
from .enum import *

String = Primitive(name="String",type=str, profiles=[Profile.EQBD, Profile.OP, Profile.SSH, Profile.EQ, Profile.DY, Profile.DL, Profile.SV, Profile.SC, ])
2 changes: 2 additions & 0 deletions modernpython/schema_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from ..utils.profile import Profile, BaseProfile
from ..utils.validation import cyclic_references_validator
from .enum import *
from .primitive import *
from .cimdatatype import *

@dataclass(config=GeoDataclassConfig)
class PositionPoint(Base):
Expand Down
11 changes: 11 additions & 0 deletions modernpython/templates/cimdatatype_template.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

"""
Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen
"""

{{class_name}} = CIMDatatype(name="{{class_name}}", type={{data_type}}, symbol={{unit}}, multiplier={{multiplier}}, profiles=[{{#class_origin}}Profile.{{origin}},{{/class_origin}}])

"""
{{{wrapped_class_comment}}}
"""

2 changes: 1 addition & 1 deletion modernpython/templates/cimpy_class_template.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class {{class_name}}({{sub_class_of}}):
"""

{{#attributes}}
{{#setNormalizedName}}{{label}}{{/setNormalizedName}} : {{#setType}}{{.}}{{/setType}} = Field({{#setDefault}}{{.}}{{/setDefault}}, in_profiles = [{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}], alias = "{{label}}")
{{#setNormalizedName}}{{label}}{{/setNormalizedName}} : {{#setType}}{{.}}{{/setType}} = Field({{#setDefault}}{{.}}{{/setDefault}}, in_profiles = [{{#attr_origin}}Profile.{{origin}}, {{/attr_origin}}], {{#setCimDataType}}{{.}}{{/setCimDataType}}alias = "{{label}}")
{{/attributes}}
{{#attributes}}
{{#setValidator}}{{.}}{{/setValidator}}
Expand Down
11 changes: 11 additions & 0 deletions modernpython/templates/primitive_template.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

"""
Generated from the CGMES 3 files via cimgen: https://github.com/sogno-platform/cimgen
"""

{{class_name}} = Primitive(name="{{class_name}}", type={{data_type}}, profiles=[{{#class_origin}}Profile.{{origin}}, {{/class_origin}}])

"""
{{{wrapped_class_comment}}}
"""

10 changes: 2 additions & 8 deletions modernpython/utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 6947f4d

Please sign in to comment.