Skip to content

Commit

Permalink
Primitives, enum and CIM_datatype (#39)
Browse files Browse the repository at this point in the history
## Enum classes
Correctly defining classes with defined values, using Enum. (Issue: #38
)
Examples: `UnitSymbol` or `UnitMultiplier` 

## Primitives classes
The current model generates classes for primitives, which are then
rarely used to define the other classes.
Instead of creating individual classes for each primitives, a generic
`Primitive` class is created.
Examples: 
`Float = Primitive(name="Float", type=float, profiles=[Profile.EQBD,
Profile.OP, Profile.SSH, Profile.EQ, Profile.DY, Profile.DL, Profile.SV,
Profile.SC, ])`

## Common CIMDatatype class
The current model generates datatypes that are then never used in the
cim classes (example: `CurrentFlow`).
Instead of creating multiple unused classes with the same attributes
(multiplier, unit, value), this pr creates a generic `CIMDatatype`
class. (Issue: #38 )
Examples: 
`CurrentFlow = CIMDatatype(name="CurrentFlow", type=float,
symbol=UnitSymbol.A, multiplier=UnitMultiplier.none,
profiles=[Profile.SSH,Profile.EQ,Profile.SV,Profile.SC,])`

## Annotation of the data type
As said above, the newly created datatypes are added as annotation
(`data_type`) when creating the other classes.
```
@DataClass
class CurrentLimit(OperationalLimit):
    """
    Operational limit on current.

    normalValue: The normal value for limit on current flow. The attribute shall be a positive value or zero.
    value: Limit on current flow. The attribute shall be a positive value or zero.
    """

    normalValue : float = Field(default=0.0, data_type = CurrentFlow, json_schema_extra={"in_profiles":[Profile.EQ, ]}) 

    value : float = Field(default=0.0, data_type = CurrentFlow, json_schema_extra={"in_profiles":[Profile.SSH, ]}) 



    @cached_property
    def possible_profiles(self)->set[BaseProfile]:
        """
        A resource can be used by multiple profiles. This is the set of profiles
        where this element can be found.
        """
        return { Profile.EQ, Profile.SSH,  }
```
  • Loading branch information
m-mirz authored Nov 23, 2024
2 parents cf5b986 + 29833d0 commit 13af877
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ __pycache__/
.vscode/*
*/.DS_Store
.DS_Store
tests/
102 changes: 76 additions & 26 deletions cimgen/languages/modernpython/lang_pack.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import os
import re
from distutils.dir_util import copy_tree
from pathlib import Path
Expand Down Expand Up @@ -35,9 +34,12 @@ def location(version): # NOSONAR
base = {"base_class": "Base", "class_location": location}

# These are the files that are used to generate the python files.
template_files = [{"filename": "cimpy_class_template.mustache", "ext": ".py"}]
constants_template_files = [{"filename": "cimpy_constants_template.mustache", "ext": ".py"}]
profile_template_files = [{"filename": "cimpy_cgmesProfile_template.mustache", "ext": ".py"}]
class_template_file = {"filename": "class_template.mustache", "ext": ".py"}
constants_template_file = {"filename": "constants_template.mustache", "ext": ".py"}
profile_template_file = {"filename": "profile_template.mustache", "ext": ".py"}
enum_template_file = {"filename": "enum_template.mustache", "ext": ".py"}
primitive_template_file = {"filename": "primitive_template.mustache", "ext": ".py"}
datatype_template_file = {"filename": "datatype_template.mustache", "ext": ".py"}


def get_class_location(class_name, class_map, version): # NOSONAR
Expand Down Expand Up @@ -76,23 +78,73 @@ def _get_type_and_default(text, render) -> tuple[str, str]:
return ("str", 'default=""')


def _get_python_type(datatype):
if datatype.lower() == "integer":
return "int"
if datatype.lower() == "boolean":
return "bool"
if datatype.lower() == "datetime":
return "datetime"
if datatype.lower() == "date":
return "date"
if datatype.lower() == "time":
return "time"
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() == "string":
return "str"
else:
# everything else should be a float
return "float"


def _set_imports(attributes):
import_set = set()
for attribute in attributes:
if attribute["is_datatype_attribute"] or attribute["is_primitive_attribute"]:
import_set.add(attribute["attribute_class"])
return sorted(import_set)


def _set_datatype_attributes(attributes) -> dict:
datatype_attributes = {}
datatype_attributes["python_type"] = "None"
import_set = set()
for attribute in attributes:
if "value" in attribute.get("about", "") and "attribute_class" in attribute:
datatype_attributes["python_type"] = _get_python_type(attribute["attribute_class"])
if "isFixed" in attribute:
import_set.add(attribute["attribute_class"])
datatype_attributes["isFixed_imports"] = sorted(import_set)
return datatype_attributes


def run_template(output_path, class_details):
if class_details["is_a_primitive_class"] or class_details["is_a_datatype_class"]:
return
for template_info in template_files:
resource_file = Path(
os.path.join(
output_path,
"resources",
class_details["class_name"] + template_info["ext"],
)
)
if not resource_file.exists():
if not (parent := resource_file.parent).exists():
parent.mkdir()
if class_details["is_a_primitive_class"]:
# Primitives are never used in the in memory representation but only for
# the schema
template = primitive_template_file
class_details["python_type"] = _get_python_type(class_details["class_name"])
elif class_details["is_a_datatype_class"]:
# Datatypes based on primitives are never used in the in memory
# representation but only for the schema
template = datatype_template_file
class_details.update(_set_datatype_attributes(class_details["attributes"]))
elif class_details["is_an_enum_class"]:
template = enum_template_file
else:
template = class_template_file
class_details["setDefault"] = _set_default
class_details["setType"] = _set_type
_write_templated_file(resource_file, class_details, template_info["filename"])
class_details["imports"] = _set_imports(class_details["attributes"])
resource_file = _create_file(output_path, class_details, template)
_write_templated_file(resource_file, class_details, template["filename"])


def _create_file(output_path, class_details, template) -> str:
resource_file = Path(output_path) / "resources" / (class_details["class_name"] + template["ext"])
resource_file.parent.mkdir(exist_ok=True)
return str(resource_file)


def _write_templated_file(class_file, class_details, template_filename):
Expand All @@ -109,17 +161,15 @@ def _write_templated_file(class_file, class_details, template_filename):


def _create_constants(output_path: str, cim_namespace: str):
for template_info in constants_template_files:
class_file = os.path.join(output_path, "utils", "constants" + template_info["ext"])
class_details = {"cim_namespace": cim_namespace}
_write_templated_file(class_file, class_details, template_info["filename"])
class_file = Path(output_path) / "utils" / ("constants" + constants_template_file["ext"])
class_details = {"cim_namespace": cim_namespace}
_write_templated_file(class_file, class_details, constants_template_file["filename"])


def _create_cgmes_profile(output_path: str, profile_details: list):
for template_info in profile_template_files:
class_file = os.path.join(output_path, "utils", "profile" + template_info["ext"])
class_details = {"profiles": profile_details}
_write_templated_file(class_file, class_details, template_info["filename"])
class_file = Path(output_path) / "utils" / ("profile" + profile_template_file["ext"])
class_details = {"profiles": profile_details}
_write_templated_file(class_file, class_details, profile_template_file["filename"])


def resolve_headers(path: str, version: str):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ from pydantic.dataclasses import dataclass

from ..utils.profile import BaseProfile, Profile
from {{class_location}} import {{sub_class_of}}
{{#imports}}
from .{{.}} import {{.}}
{{/imports}}


@dataclass
Expand Down Expand Up @@ -37,6 +40,12 @@ class {{class_name}}({{sub_class_of}}):
"is_enum_attribute": {{#is_enum_attribute}}True{{/is_enum_attribute}}{{^is_enum_attribute}}False{{/is_enum_attribute}},
"is_list_attribute": {{#is_list_attribute}}True{{/is_list_attribute}}{{^is_list_attribute}}False{{/is_list_attribute}},
"is_primitive_attribute": {{#is_primitive_attribute}}True{{/is_primitive_attribute}}{{^is_primitive_attribute}}False{{/is_primitive_attribute}},
{{#is_datatype_attribute}}
"attribute_class": {{attribute_class}},
{{/is_datatype_attribute}}
{{#is_primitive_attribute}}
"attribute_class": {{attribute_class}},
{{/is_primitive_attribute}}
},
)

Expand Down
27 changes: 27 additions & 0 deletions cimgen/languages/modernpython/templates/datatype_template.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen
"""

from ..utils.datatypes import CIMDatatype
from ..utils.profile import Profile
{{#isFixed_imports}}
from .{{.}} import {{.}}
{{/isFixed_imports}}


{{class_name}} = CIMDatatype(
name="{{class_name}}",
type={{python_type}},
{{#attributes}}
{{#isFixed}}
{{label}}={{attribute_class}}.{{isFixed}},
{{/isFixed}}
{{/attributes}}
profiles=[{{#class_origin}}
Profile.{{origin}},{{/class_origin}}
],
)

"""
{{{wrapped_class_comment}}}
"""
15 changes: 15 additions & 0 deletions cimgen/languages/modernpython/templates/enum_template.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""
Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen
"""

from enum import Enum


class {{class_name}}(str, Enum):
"""
{{{class_comment}}} # noqa: E501
"""

{{#enum_instances}}
{{label}} = "{{label}}"{{#comment}} # {{comment}}{{/comment}} # noqa: E501
{{/enum_instances}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""
Generated from the CGMES files via cimgen: https://github.com/sogno-platform/cimgen
"""

from datetime import date, datetime, time
from ..utils.datatypes import Primitive
from ..utils.profile import Profile

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

"""
{{{wrapped_class_comment}}}
"""
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class BaseProfile(str, Enum):
"""
Profile parent. Use it if you need your own profiles.

All pycgmes objects requiring a Profile are actually asking for a `BaseProfile`. As
All CGMES objects requiring a Profile are actually asking for a `BaseProfile`. As
Enum with fields cannot be inherited or composed, just create your own CustomProfile without
trying to extend Profile. It will work.
"""
Expand Down
4 changes: 2 additions & 2 deletions cimgen/languages/modernpython/utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

from pydantic.dataclasses import dataclass

from pycgmes.utils.constants import NAMESPACES
from .constants import NAMESPACES

from ..utils.config import cgmes_resource_config
from .config import cgmes_resource_config
from .profile import BaseProfile


Expand Down
6 changes: 3 additions & 3 deletions cimgen/languages/modernpython/utils/chevron_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import chevron

from pycgmes.utils.base import Base
from pycgmes.utils.constants import NAMESPACES
from pycgmes.utils.profile import BaseProfile, Profile
from .base import Base
from .constants import NAMESPACES
from .profile import BaseProfile, Profile


class ChevronWriter:
Expand Down
29 changes: 29 additions & 0 deletions cimgen/languages/modernpython/utils/datatypes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from pydantic import Field
from typing import List, Optional, Union

from .constants import NAMESPACES
from pydantic.dataclasses import dataclass

from .config import cgmes_resource_config
from .profile import BaseProfile
from ..resources.UnitMultiplier import UnitMultiplier
from ..resources.UnitSymbol import UnitSymbol
from ..resources.Currency import Currency


@dataclass(config=cgmes_resource_config)
class Primitive:

name: str = Field(frozen=True)
type: object = Field(frozen=True)
namespace: str = Field(frozen=True, default=NAMESPACES["cim"])
profiles: List[BaseProfile] = Field(frozen=True)


@dataclass(config=cgmes_resource_config)
class CIMDatatype(Primitive):

multiplier: Optional[UnitMultiplier] = Field(default=None, frozen=True)
unit: Optional[Union[UnitSymbol, Currency]] = Field(default=None, frozen=True)
denominatorMultiplier: Optional[UnitMultiplier] = Field(default=None, frozen=True)
denominatorUnit: Optional[UnitSymbol] = Field(default=None, frozen=True)

0 comments on commit 13af877

Please sign in to comment.