From 5259110400ba50840da7fd3b2feabdaff55915bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 11 Aug 2024 16:36:56 +0200 Subject: [PATCH 1/6] Add generation of CGMESProfile class, add profile URIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 63 ++++++++++--- cimgen/languages/cpp/lang_pack.py | 48 ++++++---- cimgen/languages/java/lang_pack.py | 49 +++++----- cimgen/languages/javascript/lang_pack.py | 93 ++++++++----------- .../handlebars_cgmesProfile_template.mustache | 16 +++- cimgen/languages/modernpython/lang_pack.py | 15 +-- cimgen/languages/python/lang_pack.py | 82 ++++++++-------- .../cimpy_cgmesProfile_template.mustache | 38 ++++++++ .../templates/cimpy_class_template.mustache | 75 ++++++++------- 9 files changed, 291 insertions(+), 188 deletions(-) create mode 100644 cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 0e0d7313..97377fbc 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -2,6 +2,7 @@ import os import textwrap import warnings +import re from time import time import xmltodict @@ -402,24 +403,24 @@ def _add_class(classes_map, rdfs_entry): classes_map[rdfs_entry.label()] = CIMComponentDefinition(rdfs_entry) -def _add_profile_to_packages(profile_name, short_profile_name, profile_iri): +def _add_profile_to_packages(profile_name, short_profile_name, profile_uri_list): """ - Add or append profile_iri + Add profile_uris """ - if profile_name not in profiles and profile_iri: - profiles[profile_name] = [profile_iri] + if profile_name not in profiles and profile_uri_list: + profiles[profile_name] = profile_uri_list else: - profiles[profile_name].append(profile_iri) - if short_profile_name not in package_listed_by_short_name and profile_iri: - package_listed_by_short_name[short_profile_name] = [profile_iri] + profiles[profile_name].extend(profile_uri_list) + if short_profile_name not in package_listed_by_short_name and profile_uri_list: + package_listed_by_short_name[short_profile_name] = profile_uri_list else: - package_listed_by_short_name[short_profile_name].append(profile_iri) + package_listed_by_short_name[short_profile_name].extend(profile_uri_list) def _parse_rdf(input_dic, version, lang_pack): classes_map = {} profile_name = "" - profile_iri = None + profile_uri_list = [] attributes = [] instances = [] @@ -447,13 +448,13 @@ def _parse_rdf(input_dic, version, lang_pack): if "short_profile_name_v3" in rdfs_entry_types: short_profile_name = rdfsEntry.keyword() if "profile_iri_v2_4" in rdfs_entry_types and rdfsEntry.fixed(): - profile_iri = rdfsEntry.fixed() + profile_uri_list.append(rdfsEntry.fixed()) if "profile_iri_v3" in rdfs_entry_types: - profile_iri = rdfsEntry.version_iri() + profile_uri_list.append(rdfsEntry.version_iri()) short_package_name[profile_name] = short_profile_name package_listed_by_short_name[short_profile_name] = [] - _add_profile_to_packages(profile_name, short_profile_name, profile_iri) + _add_profile_to_packages(profile_name, short_profile_name, profile_uri_list) # Add attributes to corresponding class for attribute in attributes: clarse = attribute["domain"] @@ -478,6 +479,9 @@ def _parse_rdf(input_dic, version, lang_pack): # chevron def _write_python_files(elem_dict, lang_pack, output_path, version): + # Setup called only once: make output directory, create base class, create profile class, etc. + lang_pack.setup(output_path, _get_profile_details(package_listed_by_short_name)) + float_classes = {} enum_classes = {} @@ -547,8 +551,6 @@ def format_class(_range, _dataType): def _write_files(class_details, output_path, version): - class_details["langPack"].setup(output_path, package_listed_by_short_name) - if class_details["sub_class_of"] is None: # If class has no subClassOf key it is a subclass of the Base class class_details["sub_class_of"] = class_details["langPack"].base["base_class"] @@ -762,3 +764,36 @@ def cim_generate(directory, output_path, version, lang_pack): lang_pack.resolve_headers(output_path) logger.info("Elapsed Time: {}s\n\n".format(time() - t0)) + + +def _get_profile_details(cgmes_profile_uris): + profile_details = [] + sorted_profile_keys = sorted(cgmes_profile_uris.keys(), key=lambda x: x == "EQ" and "0" or x) + for index, profile in enumerate(sorted_profile_keys): + profile_info = { + "index": index, + "short_name": profile, + "long_name": _extract_profile_long_name(cgmes_profile_uris[profile]), + "uris": [{"uri": uri} for uri in cgmes_profile_uris[profile]], + } + profile_details.append(profile_info) + return profile_details + + +def _extract_profile_long_name(profile_uris): + # Extract name from uri, e.g. "Topology" from "http://iec.ch/TC57/2013/61970-456/Topology/4" + # Examples of other possible uris: "http://entsoe.eu/CIM/Topology/4/1", "http://iec.ch/TC57/ns/CIM/Topology-EU/3.0" + # If more than one uri given, extract common part (e.g. "Equipment" from "EquipmentCore" and "EquipmentOperation") + long_name = "" + for uri in profile_uris: + match = re.search(r"/([^/-]*)(-[^/]*)?(/\d+)?/[\d.]+?$", uri) + if match: + name = match.group(1) + if long_name: + for idx in range(1, len(long_name)): + if idx >= len(name) or long_name[idx] != name[idx]: + long_name = long_name[:idx] + break + else: + long_name = name + return long_name diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index d81ccaae..895505d5 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -8,13 +8,17 @@ def location(version): return "BaseClass.hpp" +# Setup called only once: make output directory, create base class, create profile class, etc. # This just makes sure we have somewhere to write the classes. -# cgmes_profile_info details which uri belongs in each profile. +# cgmes_profile_details contains index, names and uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(version_path, cgmes_profile_info): - if not os.path.exists(version_path): - os.makedirs(version_path) +def setup(output_path, cgmes_profile_details): + if not os.path.exists(output_path): + os.makedirs(output_path) + else: + for filename in os.listdir(output_path): + os.remove(os.path.join(output_path, filename)) base = {"base_class": "BaseClass", "class_location": location} @@ -53,7 +57,7 @@ def get_class_location(class_name, class_map, version): # This is the function that runs the template. -def run_template(outputPath, class_details): +def run_template(output_path, class_details): if class_details["is_a_float"]: templates = float_template_files @@ -72,18 +76,22 @@ def run_template(outputPath, class_details): return for template_info in templates: - class_file = os.path.join(outputPath, class_details["class_name"] + template_info["ext"]) - if not os.path.exists(class_file): - with open(class_file, "w", encoding="utf-8") as file: - templates = files("cimgen.languages.cpp.templates") - with templates.joinpath(template_info["filename"]).open(encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) + class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"]) + _write_templated_file(class_file, class_details, template_info["filename"]) + + +def _write_templated_file(class_file, class_details, template_filename): + with open(class_file, "w", encoding="utf-8") as file: + class_details["setDefault"] = _set_default + templates = files("cimgen.languages.cpp.templates") + with templates.joinpath(template_filename).open(encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) # This function just allows us to avoid declaring a variable called 'switch', @@ -491,7 +499,7 @@ def _create_header_include_file(directory, header_include_filename, header, foot f.writelines(header) -def resolve_headers(outputPath): +def resolve_headers(output_path): class_list_header = [ "#ifndef CIMCLASSLIST_H\n", "#define CIMCLASSLIST_H\n", @@ -505,7 +513,7 @@ def resolve_headers(outputPath): ] _create_header_include_file( - outputPath, + output_path, "CIMClassList.hpp", class_list_header, class_list_footer, @@ -518,7 +526,7 @@ def resolve_headers(outputPath): iec61970_footer = ['#include "UnknownType.hpp"\n', "#endif"] _create_header_include_file( - outputPath, + output_path, "IEC61970.hpp", iec61970_header, iec61970_footer, diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 06eedb93..0beb4eb5 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -8,18 +8,22 @@ def location(version): return "BaseClass" +# Setup called only once: make output directory, create base class, create profile class, etc. # This just makes sure we have somewhere to write the classes. -# cgmes_profile_info details which uri belongs in each profile. +# cgmes_profile_details contains index, names und uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(version_path, cgmes_profile_info): - if not os.path.exists(version_path): - os.makedirs(version_path) +def setup(output_path, cgmes_profile_details): + if not os.path.exists(output_path): + os.makedirs(output_path) + else: + for filename in os.listdir(output_path): + os.remove(os.path.join(output_path, filename)) base = {"base_class": "BaseClass", "class_location": location} -# These are the files that are used to generate the header and object files. +# These are the files that are used to generate the java files. # There is a template set for the large number of classes that are floats. They # have unit, multiplier and value attributes in the schema, but only appear in # the file as a float string. @@ -41,7 +45,7 @@ def get_class_location(class_name, class_map, version): # This is the function that runs the template. -def run_template(outputPath, class_details): +def run_template(output_path, class_details): class_details["primitives"] = [] for attr in class_details["attributes"]: @@ -64,19 +68,22 @@ def run_template(outputPath, class_details): return for template_info in templates: - class_file = os.path.join(outputPath, class_details["class_name"] + template_info["ext"]) - if not os.path.exists(class_file): - with open(class_file, "w", encoding="utf-8") as file: - class_details["setDefault"] = _set_default - templates = files("cimgen.languages.java.templates") - with templates.joinpath(template_info["filename"]).open(encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) + class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"]) + _write_templated_file(class_file, class_details, template_info["filename"]) + + +def _write_templated_file(class_file, class_details, template_filename): + with open(class_file, "w", encoding="utf-8") as file: + class_details["setDefault"] = _set_default + templates = files("cimgen.languages.java.templates") + with templates.joinpath(template_filename).open(encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) # This function just allows us to avoid declaring a variable called 'switch', @@ -416,7 +423,7 @@ def _create_header_include_file(directory, header_include_filename, header, foot f.writelines(header) -def resolve_headers(outputPath): +def resolve_headers(output_path): class_list_header = [ "package cim4j;\n", "import java.util.Map;\n", @@ -432,7 +439,7 @@ def resolve_headers(outputPath): class_list_footer = [" );\n", "}\n"] _create_header_include_file( - outputPath, + output_path, "CIMClassMap.java", class_list_header, class_list_footer, diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index d605e99b..a90d28e7 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -1,6 +1,5 @@ import os import chevron -import json import sys from importlib.resources import files @@ -9,43 +8,29 @@ def location(version): return "BaseClass.hpp" +# Setup called only once: make output directory, create base class, create profile class, etc. # This function makes sure we have somewhere to write the classes. -# cgmes_profile_info details which uri belongs in each profile. -# We use that to creating the header data for the profiles. -def setup(version_path, cgmes_profile_info): - if not os.path.exists(version_path): - os.makedirs(version_path) - class_file = os.path.join(version_path, "CGMESProfile.js") - short_names = {} - for index, key in enumerate(cgmes_profile_info): - short_names[key] = index - - cgmes_profile_string = json.dumps(cgmes_profile_info, indent=2) - cgmes_shortname_string = json.dumps(short_names, indent=2) - cgmes_object = { - "profileList": cgmes_profile_string, - "shortNames": cgmes_shortname_string, - } - write_templated_file(class_file, cgmes_object, "handlebars_cgmesProfile_template.mustache") +# cgmes_profile_details contains index, names und uris for each profile. +# We use that to create the header data for the profiles. +def setup(output_path, cgmes_profile_details): + if not os.path.exists(output_path): + os.makedirs(output_path) + else: + for filename in os.listdir(output_path): + os.remove(os.path.join(output_path, filename)) + _create_base(output_path) + _create_cgmes_profile(output_path, cgmes_profile_details) base = {"base_class": "BaseClass", "class_location": location} -# These are the files that are used to generate the header and object files. -# There is a template set for the large number of classes that are floats. They -# have unit, multiplier and value attributes in the schema, but only appear in -# the file as a float string. +# These are the files that are used to generate the javascript files. template_files = [{"filename": "handlebars_template.mustache", "ext": ".js"}] +base_template_files = [{"filename": "handlebars_baseclass_template.mustache", "ext": ".js"}] +profile_template_files = [{"filename": "handlebars_cgmesProfile_template.mustache", "ext": ".js"}] partials = {} -entsoeURIs = [] - - -def neq(one, two): - print(one, two) - return one != two - # We need to keep track of which class types are secretly float # primitives. We will use a different template to create the class @@ -145,16 +130,7 @@ def selectPrimitiveRenderFunction(primitive): # This is the function that runs the template. -def run_template(outputPath, class_details): - - nameLength = len(class_details["class_name"]) - if class_details["class_name"][nameLength - 7 :] == "Version": - for attribute in class_details["attributes"]: - if "entsoeURI" in attribute["about"]: - if attribute["isFixed"] is object: - entsoeURIs.append({"key": attribute["about"], "value": attribute["isFixed"]["_"]}) - else: - entsoeURIs.append({"key": attribute["about"], "value": attribute["isFixed"]}) +def run_template(output_path, class_details): class_details["is_not_terminal"] = class_details["class_name"] != "Terminal" for attr in class_details["attributes"]: @@ -181,21 +157,34 @@ def run_template(outputPath, class_details): class_details["renderAttribute"] = renderAttribute for template_info in template_files: - class_file = os.path.join(outputPath, class_details["class_name"] + template_info["ext"]) - write_templated_file(class_file, class_details, template_info["filename"]) + class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"]) + _write_templated_file(class_file, class_details, template_info["filename"]) + + +def _write_templated_file(class_file, class_details, template_filename): + with open(class_file, "w", encoding="utf-8") as file: + templates = files("cimgen.languages.javascript.templates") + with templates.joinpath(template_filename).open(encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) + - class_file = os.path.join(outputPath, "BaseClass.js") - write_templated_file(class_file, {"URI": entsoeURIs}, "handlebars_baseclass_template.mustache") +# creates the Base class file, all classes inherit from this class +def _create_base(output_path): + for template_info in base_template_files: + class_file = os.path.join(output_path, "BaseClass" + template_info["ext"]) + _write_templated_file(class_file, {}, template_info["filename"]) -def write_templated_file(class_file, class_details, template_filename): - if not os.path.exists(class_file): - with open(class_file, "w", encoding="utf-8") as file: - templates = files("cimgen.languages.javascript.templates") - with templates.joinpath(template_filename).open(encoding="utf-8") as f: - args = {"data": class_details, "template": f, "partials_dict": partials} - output = chevron.render(**args) - file.write(output) +def _create_cgmes_profile(output_path, profile_details): + for template_info in profile_template_files: + class_file = os.path.join(output_path, "CGMESProfile" + template_info["ext"]) + _write_templated_file(class_file, {"profiles": profile_details}, template_info["filename"]) def is_an_unused_attribute(attr_details, debug=False): @@ -226,5 +215,5 @@ def _get_rid_of_hash(name): return name -def resolve_headers(outputPath): +def resolve_headers(output_path): pass diff --git a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache index 305a4581..c8b7a39e 100644 --- a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache +++ b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache @@ -1,7 +1,19 @@ class CGMESProfile { - static profileUrls = {{{profileList}}}; - static shortNames = {{{shortNames}}}; + static profileUrls = { +{{#profiles}} + "{{short_name}}": [ +{{#uris}} + "{{uri}}", +{{/uris}} + ], +{{/profiles}} + }; + static shortNames = { +{{#profiles}} + "{{short_name}}": {{index}}, +{{/profiles}} + }; } export default CGMESProfile diff --git a/cimgen/languages/modernpython/lang_pack.py b/cimgen/languages/modernpython/lang_pack.py index ef6c6721..165b9d10 100644 --- a/cimgen/languages/modernpython/lang_pack.py +++ b/cimgen/languages/modernpython/lang_pack.py @@ -10,17 +10,19 @@ logger = logging.getLogger(__name__) +# Setup called only once: make output directory, create base class, create profile class, etc. # This makes sure we have somewhere to write the classes, and # creates a couple of files the python implementation needs. -# cgmes_profile_info details which uri belongs in each profile. +# cgmes_profile_details contains index, names und uris for each profile. # We don't use that here because we aren't creating the header # data for the separate profiles. -def setup(version_path, cgmes_profile_info): # NOSONAR - # version_path is actually the output_path +def setup(output_path, cgmes_profile_details): # NOSONAR + for file in Path(output_path).glob("**/*.py"): + file.unlink() # Add all hardcoded utils and create parent dir source_dir = Path(__file__).parent / "utils" - dest_dir = Path(version_path) / "utils" + dest_dir = Path(output_path) / "utils" copy_tree(str(source_dir), str(dest_dir)) @@ -31,6 +33,7 @@ def location(version): 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"}] @@ -83,12 +86,12 @@ def set_float_classes(new_float_classes): return -def run_template(version_path, class_details): +def run_template(output_path, class_details): for template_info in template_files: resource_file = Path( os.path.join( - version_path, + output_path, "resources", class_details["class_name"] + template_info["ext"], ) diff --git a/cimgen/languages/python/lang_pack.py b/cimgen/languages/python/lang_pack.py index a6ae58ac..1af4668c 100644 --- a/cimgen/languages/python/lang_pack.py +++ b/cimgen/languages/python/lang_pack.py @@ -7,16 +7,19 @@ logger = logging.getLogger(__name__) +# Setup called only once: make output directory, create base class, create profile class, etc. # This makes sure we have somewhere to write the classes, and # creates a couple of files the python implementation needs. -# cgmes_profile_info details which uri belongs in each profile. -# We don't use that here because we aren't creating the header -# data for the separate profiles. -def setup(version_path, cgmes_profile_info): - if not os.path.exists(version_path): - os.makedirs(version_path) - _create_init(version_path) - _create_base(version_path) +# cgmes_profile_details contains index, names and uris for each profile. +# We use that to create the header data for the profiles. +def setup(output_path, cgmes_profile_details): + if not os.path.exists(output_path): + os.makedirs(output_path) + else: + for filename in os.listdir(output_path): + os.remove(os.path.join(output_path, filename)) + _create_base(output_path) + _create_cgmes_profile(output_path, cgmes_profile_details) def location(version): @@ -25,7 +28,9 @@ def location(version): 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"}] +profile_template_files = [{"filename": "cimpy_cgmesProfile_template.mustache", "ext": ".py"}] def get_class_location(class_name, class_map, version): @@ -76,53 +81,51 @@ def set_float_classes(new_float_classes): return -def run_template(version_path, class_details): +def run_template(output_path, class_details): for template_info in template_files: - class_file = os.path.join(version_path, class_details["class_name"] + template_info["ext"]) - if not os.path.exists(class_file): - with open(class_file, "w", encoding="utf-8") as file: - class_details["setDefault"] = _set_default - templates = files("cimgen.languages.python.templates") - with templates.joinpath(template_info["filename"]).open(encoding="utf-8") as f: - args = { - "data": class_details, - "template": f, - "partials_dict": partials, - } - output = chevron.render(**args) - file.write(output) - - -def _create_init(path): - init_file = path + "/__init__.py" - with open(init_file, "w", encoding="utf-8"): - pass + class_file = os.path.join(output_path, class_details["class_name"] + template_info["ext"]) + _write_templated_file(class_file, class_details, template_info["filename"]) + + +def _write_templated_file(class_file, class_details, template_filename): + with open(class_file, "w", encoding="utf-8") as file: + class_details["setDefault"] = _set_default + templates = files("cimgen.languages.python.templates") + with templates.joinpath(template_filename).open(encoding="utf-8") as f: + args = { + "data": class_details, + "template": f, + "partials_dict": partials, + } + output = chevron.render(**args) + file.write(output) # creates the Base class file, all classes inherit from this class def _create_base(path): base_path = path + "/Base.py" base = [ - "from enum import Enum\n\n", - "\n", - "class Base():\n", + "class Base:\n", ' """\n', " Base Class for CIM\n", ' """\n\n', - ' cgmesProfile = Enum("cgmesProfile", {"EQ": 0, "SSH": 1, "TP": 2, "SV": 3, "DY": 4, "GL": 5, "DL": 6, "TP_BD": 7, "EQ_BD": 8})', # noqa: E501 - "\n\n", - " def __init__(self, *args, **kw_args):\n", - " pass\n", - "\n", " def printxml(self, dict={}):\n", " return dict\n", ] - with open(base_path, "w", encoding="utf-8") as f: for line in base: f.write(line) +def _create_cgmes_profile(output_path, profile_details): + for template_info in profile_template_files: + class_file = os.path.join(output_path, "CGMESProfile" + template_info["ext"]) + _write_templated_file(class_file, {"profiles": profile_details}, template_info["filename"]) + + +class_blacklist = ["CGMESProfile"] + + def resolve_headers(path): filenames = glob.glob(path + "/*.py") include_names = [] @@ -130,5 +133,8 @@ def resolve_headers(path): include_names.append(os.path.splitext(os.path.basename(filename))[0]) with open(path + "/__init__.py", "w", encoding="utf-8") as header_file: for include_name in include_names: - header_file.write("from " + "." + include_name + " import " + include_name + " as " + include_name + "\n") + if include_name not in class_blacklist: + header_file.write( + "from " + "." + include_name + " import " + include_name + " as " + include_name + "\n" + ) header_file.close() diff --git a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache new file mode 100644 index 00000000..f6dd801f --- /dev/null +++ b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache @@ -0,0 +1,38 @@ +from enum import Enum + + +# Mapping between the profiles and their short names +short_profile_name = { +{{#profiles}} + "{{long_name}}": "{{short_name}}", +{{/profiles}} +} +long_profile_name = { +{{#profiles}} + "{{short_name}}": "{{long_name}}", +{{/profiles}} +} +profile_uris = { +{{#profiles}} + "{{short_name}}": [ +{{#uris}} + "{{uri}}", +{{/uris}} + ], +{{/profiles}} +} + + +class Profile(Enum): + """Enum containing all CGMES profiles and their export priority.""" + +{{#profiles}} + {{short_name}} = {{index}} +{{/profiles}} + + def long_name(self): + return long_profile_name[self.name] + + @classmethod + def from_long_name(cls, long_name): + return cls[short_profile_name[long_name]] diff --git a/cimgen/languages/python/templates/cimpy_class_template.mustache b/cimgen/languages/python/templates/cimpy_class_template.mustache index 20667b3b..86af85c6 100644 --- a/cimgen/languages/python/templates/cimpy_class_template.mustache +++ b/cimgen/languages/python/templates/cimpy_class_template.mustache @@ -1,39 +1,44 @@ from .{{sub_class_of}} import {{sub_class_of}} +from .CGMESProfile import Profile class {{class_name}}({{sub_class_of}}): - ''' - {{{class_comment}}} - - {{#attributes}}:{{label}}: {{{comment}}} Default: {{#setDefault}}{{dataType}}{{/setDefault}} - {{/attributes}} - ''' - - cgmesProfile = {{sub_class_of}}.cgmesProfile - - possibleProfileList = {'class': [{{#class_origin}}cgmesProfile.{{origin}}.value, {{/class_origin}}], - {{#attributes}}'{{label}}': [{{#attr_origin}}cgmesProfile.{{origin}}.value, {{/attr_origin}}], - {{/attributes}} } - - serializationProfile = {} - - {{#super_init}}__doc__ += '\n Documentation of parent class {{sub_class_of}}: \n' + {{sub_class_of}}.__doc__ {{/super_init}} - - def __init__(self, {{#attributes}}{{label}} = {{#setDefault}}{{dataType}}{{/setDefault}}, {{/attributes}} {{#super_init}}*args, **kw_args{{/super_init}}): - {{#super_init}} - super().__init__(*args, **kw_args) - {{/super_init}} - - {{#attributes}} - self.{{label}} = {{label}} - {{/attributes}} - {{^attributes}} -pass - {{/attributes}} - - def __str__(self): - str = 'class={{class_name}}\n' - attributes = self.__dict__ - for key in attributes.keys(): - str = str + key + '={}\n'.format(attributes[key]) - return str + """ + {{{class_comment}}} + +{{#attributes}} + :{{label}}: {{{comment}}} Default: {{#setDefault}}{{dataType}}{{/setDefault}} +{{/attributes}} + """ + + possibleProfileList = { + "class": [{{#class_origin}}Profile.{{origin}}.value, {{/class_origin}}], +{{#attributes}} + "{{label}}": [{{#attr_origin}}Profile.{{origin}}.value, {{/attr_origin}}], +{{/attributes}} + } + + serializationProfile = {} + +{{#super_init}} + __doc__ += "\nDocumentation of parent class {{sub_class_of}}:\n" + {{sub_class_of}}.__doc__ +{{/super_init}} + + def __init__(self{{#attributes}}, {{label}} = {{#setDefault}}{{dataType}}{{/setDefault}}{{/attributes}}{{#super_init}}, *args, **kw_args{{/super_init}}): +{{#super_init}} + super().__init__(*args, **kw_args) +{{/super_init}} + +{{#attributes}} + self.{{label}} = {{label}} +{{/attributes}} +{{^attributes}} + pass +{{/attributes}} + + def __str__(self): + str = "class={{class_name}}\n" + attributes = self.__dict__ + for key in attributes.keys(): + str = str + key + "={}\n".format(attributes[key]) + return str From 2a109f9d65d1987bd11c465e2093e6323699ee84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 11 Aug 2024 16:40:16 +0200 Subject: [PATCH 2/6] Change profile URIs in the generated CGMESProfile class for cgmes_v2_4_15 from baseURIs to entsoeURIs (e.g. http://entsoe.eu/CIM/Topology/4/1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 97377fbc..8958ee22 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -373,7 +373,7 @@ def _entry_types_version_2(rdfs_entry: RDFSEntry) -> list: entry_types.append("profile_name_v2_4") if ( rdfs_entry.stereotype() == "http://iec.ch/TC57/NonStandard/UML#attribute" # NOSONAR - and rdfs_entry.label()[0:7] == "baseURI" + and rdfs_entry.label().startswith("entsoeURI") ): entry_types.append("profile_iri_v2_4") if rdfs_entry.label() == "shortName": From 5ab6f4d6852c60e3ecbe62c0bffa631591e75302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Sun, 11 Aug 2024 18:21:37 +0200 Subject: [PATCH 3/6] Add cim namespace to the generated CGMESProfile class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 8 ++++++-- cimgen/languages/cpp/lang_pack.py | 2 +- cimgen/languages/java/lang_pack.py | 2 +- cimgen/languages/javascript/lang_pack.py | 12 ++++++++---- .../handlebars_cgmesProfile_template.mustache | 2 ++ cimgen/languages/modernpython/lang_pack.py | 2 +- cimgen/languages/python/lang_pack.py | 12 ++++++++---- .../templates/cimpy_cgmesProfile_template.mustache | 3 +++ 8 files changed, 30 insertions(+), 13 deletions(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 8958ee22..1167fdf9 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -339,7 +339,7 @@ def wrap_and_clean(txt: str, width: int = 120, initial_indent="", subsequent_ind short_package_name = {} package_listed_by_short_name = {} - +cim_namespace = "" profiles = {} @@ -424,6 +424,10 @@ def _parse_rdf(input_dic, version, lang_pack): attributes = [] instances = [] + global cim_namespace + if not cim_namespace: + cim_namespace = input_dic["rdf:RDF"].get("$xmlns:cim") + # Generates list with dictionaries as elements descriptions = input_dic["rdf:RDF"]["rdf:Description"] @@ -480,7 +484,7 @@ def _parse_rdf(input_dic, version, lang_pack): def _write_python_files(elem_dict, lang_pack, output_path, version): # Setup called only once: make output directory, create base class, create profile class, etc. - lang_pack.setup(output_path, _get_profile_details(package_listed_by_short_name)) + lang_pack.setup(output_path, _get_profile_details(package_listed_by_short_name), cim_namespace) float_classes = {} enum_classes = {} diff --git a/cimgen/languages/cpp/lang_pack.py b/cimgen/languages/cpp/lang_pack.py index 895505d5..624847c1 100644 --- a/cimgen/languages/cpp/lang_pack.py +++ b/cimgen/languages/cpp/lang_pack.py @@ -13,7 +13,7 @@ def location(version): # cgmes_profile_details contains index, names and uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(output_path, cgmes_profile_details): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): if not os.path.exists(output_path): os.makedirs(output_path) else: diff --git a/cimgen/languages/java/lang_pack.py b/cimgen/languages/java/lang_pack.py index 0beb4eb5..170777f4 100644 --- a/cimgen/languages/java/lang_pack.py +++ b/cimgen/languages/java/lang_pack.py @@ -13,7 +13,7 @@ def location(version): # cgmes_profile_details contains index, names und uris for each profile. # We don't use that here because we aren't exporting into # separate profiles. -def setup(output_path, cgmes_profile_details): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): if not os.path.exists(output_path): os.makedirs(output_path) else: diff --git a/cimgen/languages/javascript/lang_pack.py b/cimgen/languages/javascript/lang_pack.py index a90d28e7..13638718 100644 --- a/cimgen/languages/javascript/lang_pack.py +++ b/cimgen/languages/javascript/lang_pack.py @@ -12,14 +12,14 @@ def location(version): # This function makes sure we have somewhere to write the classes. # cgmes_profile_details contains index, names und uris for each profile. # We use that to create the header data for the profiles. -def setup(output_path, cgmes_profile_details): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): if not os.path.exists(output_path): os.makedirs(output_path) else: for filename in os.listdir(output_path): os.remove(os.path.join(output_path, filename)) _create_base(output_path) - _create_cgmes_profile(output_path, cgmes_profile_details) + _create_cgmes_profile(output_path, cgmes_profile_details, cim_namespace) base = {"base_class": "BaseClass", "class_location": location} @@ -181,10 +181,14 @@ def _create_base(output_path): _write_templated_file(class_file, {}, template_info["filename"]) -def _create_cgmes_profile(output_path, profile_details): +def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace: str): for template_info in profile_template_files: class_file = os.path.join(output_path, "CGMESProfile" + template_info["ext"]) - _write_templated_file(class_file, {"profiles": profile_details}, template_info["filename"]) + class_details = { + "profiles": profile_details, + "cim_namespace": cim_namespace, + } + _write_templated_file(class_file, class_details, template_info["filename"]) def is_an_unused_attribute(attr_details, debug=False): diff --git a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache index c8b7a39e..70d7c6de 100644 --- a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache +++ b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache @@ -17,3 +17,5 @@ class CGMESProfile { } export default CGMESProfile + +export const CIM_NAMESPACE = "{{cim_namespace}}" diff --git a/cimgen/languages/modernpython/lang_pack.py b/cimgen/languages/modernpython/lang_pack.py index 165b9d10..fe35fc82 100644 --- a/cimgen/languages/modernpython/lang_pack.py +++ b/cimgen/languages/modernpython/lang_pack.py @@ -16,7 +16,7 @@ # cgmes_profile_details contains index, names und uris for each profile. # We don't use that here because we aren't creating the header # data for the separate profiles. -def setup(output_path, cgmes_profile_details): # NOSONAR +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): # NOSONAR for file in Path(output_path).glob("**/*.py"): file.unlink() diff --git a/cimgen/languages/python/lang_pack.py b/cimgen/languages/python/lang_pack.py index 1af4668c..f9ed15b0 100644 --- a/cimgen/languages/python/lang_pack.py +++ b/cimgen/languages/python/lang_pack.py @@ -12,14 +12,14 @@ # creates a couple of files the python implementation needs. # cgmes_profile_details contains index, names and uris for each profile. # We use that to create the header data for the profiles. -def setup(output_path, cgmes_profile_details): +def setup(output_path: str, cgmes_profile_details: list, cim_namespace: str): if not os.path.exists(output_path): os.makedirs(output_path) else: for filename in os.listdir(output_path): os.remove(os.path.join(output_path, filename)) _create_base(output_path) - _create_cgmes_profile(output_path, cgmes_profile_details) + _create_cgmes_profile(output_path, cgmes_profile_details, cim_namespace) def location(version): @@ -117,10 +117,14 @@ def _create_base(path): f.write(line) -def _create_cgmes_profile(output_path, profile_details): +def _create_cgmes_profile(output_path: str, profile_details: list, cim_namespace: str): for template_info in profile_template_files: class_file = os.path.join(output_path, "CGMESProfile" + template_info["ext"]) - _write_templated_file(class_file, {"profiles": profile_details}, template_info["filename"]) + class_details = { + "profiles": profile_details, + "cim_namespace": cim_namespace, + } + _write_templated_file(class_file, class_details, template_info["filename"]) class_blacklist = ["CGMESProfile"] diff --git a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache index f6dd801f..e857b056 100644 --- a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache +++ b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache @@ -36,3 +36,6 @@ class Profile(Enum): @classmethod def from_long_name(cls, long_name): return cls[short_profile_name[long_name]] + + +cim_namespace = "{{cim_namespace}}" From 07743c5017e8dd76f12c9ed502d1b362d20eb3f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Mon, 12 Aug 2024 12:41:24 +0200 Subject: [PATCH 4/6] Tell Sonar that namespaces are safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- .../templates/handlebars_cgmesProfile_template.mustache | 6 +++--- .../python/templates/cimpy_cgmesProfile_template.mustache | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache index 70d7c6de..d5495fba 100644 --- a/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache +++ b/cimgen/languages/javascript/templates/handlebars_cgmesProfile_template.mustache @@ -1,10 +1,10 @@ class CGMESProfile { - static profileUrls = { + static profileUrls = { // Those are strings, not real addresses, hence the NOSONAR. {{#profiles}} "{{short_name}}": [ {{#uris}} - "{{uri}}", + "{{uri}}", // NOSONAR {{/uris}} ], {{/profiles}} @@ -18,4 +18,4 @@ class CGMESProfile { export default CGMESProfile -export const CIM_NAMESPACE = "{{cim_namespace}}" +export const CIM_NAMESPACE = "{{cim_namespace}}" // NOSONAR diff --git a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache index e857b056..5298801f 100644 --- a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache +++ b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache @@ -12,11 +12,11 @@ long_profile_name = { "{{short_name}}": "{{long_name}}", {{/profiles}} } -profile_uris = { +profile_uris = { # Those are strings, not real addresses, hence the NOSONAR. {{#profiles}} "{{short_name}}": [ {{#uris}} - "{{uri}}", + "{{uri}}", # NOSONAR {{/uris}} ], {{/profiles}} @@ -38,4 +38,4 @@ class Profile(Enum): return cls[short_profile_name[long_name]] -cim_namespace = "{{cim_namespace}}" +cim_namespace = "{{cim_namespace}}" # NOSONAR From e9a488551cd6abbce6349ca54e98f6facd11bffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Mon, 12 Aug 2024 13:03:56 +0200 Subject: [PATCH 5/6] Add method Profile.uris() to CGMESProfile.py to get the list of profile URIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- .../python/templates/cimpy_cgmesProfile_template.mustache | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache index 5298801f..93e908be 100644 --- a/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache +++ b/cimgen/languages/python/templates/cimpy_cgmesProfile_template.mustache @@ -33,6 +33,9 @@ class Profile(Enum): def long_name(self): return long_profile_name[self.name] + def uris(self): + return profile_uris[self.name] + @classmethod def from_long_name(cls, long_name): return cls[short_profile_name[long_name]] From 42a07c20ca1252c3fc587d0b707225fa92f51f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnther?= Date: Thu, 15 Aug 2024 15:22:15 +0200 Subject: [PATCH 6/6] Add recommended class profile to all generated classes (currently only used for python) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Günther --- cimgen/cimgen.py | 63 ++++++++++++++++++- .../templates/cimpy_class_template.mustache | 2 + 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cimgen/cimgen.py b/cimgen/cimgen.py index 1167fdf9..4f5056a0 100644 --- a/cimgen/cimgen.py +++ b/cimgen/cimgen.py @@ -499,6 +499,8 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): lang_pack.set_float_classes(float_classes) lang_pack.set_enum_classes(enum_classes) + recommended_class_profiles = _get_recommended_class_profiles(elem_dict) + for class_name in elem_dict.keys(): class_details = { @@ -512,6 +514,7 @@ def _write_python_files(elem_dict, lang_pack, output_path, version): "langPack": lang_pack, "sub_class_of": elem_dict[class_name].superClass(), "sub_classes": elem_dict[class_name].subClasses(), + "recommended_class_profile": recommended_class_profiles[class_name], } # extract comments @@ -772,7 +775,7 @@ def cim_generate(directory, output_path, version, lang_pack): def _get_profile_details(cgmes_profile_uris): profile_details = [] - sorted_profile_keys = sorted(cgmes_profile_uris.keys(), key=lambda x: x == "EQ" and "0" or x) + sorted_profile_keys = _get_sorted_profile_keys(cgmes_profile_uris.keys()) for index, profile in enumerate(sorted_profile_keys): profile_info = { "index": index, @@ -801,3 +804,61 @@ def _extract_profile_long_name(profile_uris): else: long_name = name return long_name + + +def _get_sorted_profile_keys(profile_key_list): + """Sort profiles alphabetically, but "EQ" to the first place. + + Profiles should be always used in the same order when they are written into the enum class Profile. + The same order should be used if one of several possible profiles is to be selected. + + :param profile_key_list: List of short profile names. + :return: Sorted list of short profile names. + """ + return sorted(profile_key_list, key=lambda x: x == "EQ" and "0" or x) + + +def _get_recommended_class_profiles(elem_dict): + """Get the recommended profiles for all classes. + + This function searches for the recommended profile of each class. + If the class contains attributes for different profiles not all data of the object could be written into one file. + To write the data to as few as possible files the class profile should be that with most of the attributes. + But some classes contain a lot of rarely used special attributes, i.e. attributes for a special profile + (e.g. TopologyNode has many attributes for TopologyBoundary, but the class profile should be Topology). + That's why attributes that only belong to one profile are skipped in the search algorithm. + + :param elem_dict: Information about all classes. + Used are here possible class profiles (elem_dict[class_name].origins()), + possible attribute profiles (elem_dict[class_name].attributes()[*]["attr_origin"]) + and the superclass of each class (elem_dict[class_name].superClass()). + :return: Mapping of class to profile. + """ + recommended_class_profiles = {} + for class_name in elem_dict.keys(): + class_origin = elem_dict[class_name].origins() + class_profiles = [origin["origin"] for origin in class_origin] + if len(class_profiles) == 1: + recommended_class_profiles[class_name] = class_profiles[0] + continue + + # Count profiles of all attributes of this class and its superclasses + profile_count_map = {} + name = class_name + while name: + for attribute in _find_multiple_attributes(elem_dict[name].attributes()): + profiles = [origin["origin"] for origin in attribute["attr_origin"]] + ambiguous_profile = len(profiles) > 1 + for profile in profiles: + if ambiguous_profile and profile in class_profiles: + profile_count_map.setdefault(profile, []).append(attribute["label"]) + name = elem_dict[name].superClass() + + # Set the profile with most attributes as recommended profile for this class + if profile_count_map: + max_count = max(len(v) for v in profile_count_map.values()) + filtered_profiles = [k for k, v in profile_count_map.items() if len(v) == max_count] + recommended_class_profiles[class_name] = _get_sorted_profile_keys(filtered_profiles)[0] + else: + recommended_class_profiles[class_name] = _get_sorted_profile_keys(class_profiles)[0] + return recommended_class_profiles diff --git a/cimgen/languages/python/templates/cimpy_class_template.mustache b/cimgen/languages/python/templates/cimpy_class_template.mustache index 86af85c6..a461f9c0 100644 --- a/cimgen/languages/python/templates/cimpy_class_template.mustache +++ b/cimgen/languages/python/templates/cimpy_class_template.mustache @@ -20,6 +20,8 @@ class {{class_name}}({{sub_class_of}}): serializationProfile = {} + recommendedClassProfile = Profile.{{recommended_class_profile}}.value + {{#super_init}} __doc__ += "\nDocumentation of parent class {{sub_class_of}}:\n" + {{sub_class_of}}.__doc__ {{/super_init}}