2
2
import os
3
3
import textwrap
4
4
import warnings
5
+ import re
5
6
from time import time
6
7
7
8
import xmltodict
@@ -338,7 +339,7 @@ def wrap_and_clean(txt: str, width: int = 120, initial_indent="", subsequent_ind
338
339
339
340
short_package_name = {}
340
341
package_listed_by_short_name = {}
341
-
342
+ cim_namespace = ""
342
343
profiles = {}
343
344
344
345
@@ -372,7 +373,7 @@ def _entry_types_version_2(rdfs_entry: RDFSEntry) -> list:
372
373
entry_types .append ("profile_name_v2_4" )
373
374
if (
374
375
rdfs_entry .stereotype () == "http://iec.ch/TC57/NonStandard/UML#attribute" # NOSONAR
375
- and rdfs_entry .label ()[ 0 : 7 ] == "baseURI"
376
+ and rdfs_entry .label (). startswith ( "entsoeURI" )
376
377
):
377
378
entry_types .append ("profile_iri_v2_4" )
378
379
if rdfs_entry .label () == "shortName" :
@@ -402,27 +403,31 @@ def _add_class(classes_map, rdfs_entry):
402
403
classes_map [rdfs_entry .label ()] = CIMComponentDefinition (rdfs_entry )
403
404
404
405
405
- def _add_profile_to_packages (profile_name , short_profile_name , profile_iri ):
406
+ def _add_profile_to_packages (profile_name , short_profile_name , profile_uri_list ):
406
407
"""
407
- Add or append profile_iri
408
+ Add profile_uris
408
409
"""
409
- if profile_name not in profiles and profile_iri :
410
- profiles [profile_name ] = [ profile_iri ]
410
+ if profile_name not in profiles and profile_uri_list :
411
+ profiles [profile_name ] = profile_uri_list
411
412
else :
412
- profiles [profile_name ].append ( profile_iri )
413
- if short_profile_name not in package_listed_by_short_name and profile_iri :
414
- package_listed_by_short_name [short_profile_name ] = [ profile_iri ]
413
+ profiles [profile_name ].extend ( profile_uri_list )
414
+ if short_profile_name not in package_listed_by_short_name and profile_uri_list :
415
+ package_listed_by_short_name [short_profile_name ] = profile_uri_list
415
416
else :
416
- package_listed_by_short_name [short_profile_name ].append ( profile_iri )
417
+ package_listed_by_short_name [short_profile_name ].extend ( profile_uri_list )
417
418
418
419
419
420
def _parse_rdf (input_dic , version , lang_pack ):
420
421
classes_map = {}
421
422
profile_name = ""
422
- profile_iri = None
423
+ profile_uri_list = []
423
424
attributes = []
424
425
instances = []
425
426
427
+ global cim_namespace
428
+ if not cim_namespace :
429
+ cim_namespace = input_dic ["rdf:RDF" ].get ("$xmlns:cim" )
430
+
426
431
# Generates list with dictionaries as elements
427
432
descriptions = input_dic ["rdf:RDF" ]["rdf:Description" ]
428
433
@@ -447,13 +452,13 @@ def _parse_rdf(input_dic, version, lang_pack):
447
452
if "short_profile_name_v3" in rdfs_entry_types :
448
453
short_profile_name = rdfsEntry .keyword ()
449
454
if "profile_iri_v2_4" in rdfs_entry_types and rdfsEntry .fixed ():
450
- profile_iri = rdfsEntry .fixed ()
455
+ profile_uri_list . append ( rdfsEntry .fixed () )
451
456
if "profile_iri_v3" in rdfs_entry_types :
452
- profile_iri = rdfsEntry .version_iri ()
457
+ profile_uri_list . append ( rdfsEntry .version_iri () )
453
458
454
459
short_package_name [profile_name ] = short_profile_name
455
460
package_listed_by_short_name [short_profile_name ] = []
456
- _add_profile_to_packages (profile_name , short_profile_name , profile_iri )
461
+ _add_profile_to_packages (profile_name , short_profile_name , profile_uri_list )
457
462
# Add attributes to corresponding class
458
463
for attribute in attributes :
459
464
clarse = attribute ["domain" ]
@@ -478,6 +483,9 @@ def _parse_rdf(input_dic, version, lang_pack):
478
483
# chevron
479
484
def _write_python_files (elem_dict , lang_pack , output_path , version ):
480
485
486
+ # Setup called only once: make output directory, create base class, create profile class, etc.
487
+ lang_pack .setup (output_path , _get_profile_details (package_listed_by_short_name ), cim_namespace )
488
+
481
489
float_classes = {}
482
490
enum_classes = {}
483
491
@@ -491,6 +499,8 @@ def _write_python_files(elem_dict, lang_pack, output_path, version):
491
499
lang_pack .set_float_classes (float_classes )
492
500
lang_pack .set_enum_classes (enum_classes )
493
501
502
+ recommended_class_profiles = _get_recommended_class_profiles (elem_dict )
503
+
494
504
for class_name in elem_dict .keys ():
495
505
496
506
class_details = {
@@ -504,6 +514,7 @@ def _write_python_files(elem_dict, lang_pack, output_path, version):
504
514
"langPack" : lang_pack ,
505
515
"sub_class_of" : elem_dict [class_name ].superClass (),
506
516
"sub_classes" : elem_dict [class_name ].subClasses (),
517
+ "recommended_class_profile" : recommended_class_profiles [class_name ],
507
518
}
508
519
509
520
# extract comments
@@ -547,8 +558,6 @@ def format_class(_range, _dataType):
547
558
548
559
549
560
def _write_files (class_details , output_path , version ):
550
- class_details ["langPack" ].setup (output_path , package_listed_by_short_name )
551
-
552
561
if class_details ["sub_class_of" ] is None :
553
562
# If class has no subClassOf key it is a subclass of the Base class
554
563
class_details ["sub_class_of" ] = class_details ["langPack" ].base ["base_class" ]
@@ -762,3 +771,94 @@ def cim_generate(directory, output_path, version, lang_pack):
762
771
lang_pack .resolve_headers (output_path )
763
772
764
773
logger .info ("Elapsed Time: {}s\n \n " .format (time () - t0 ))
774
+
775
+
776
+ def _get_profile_details (cgmes_profile_uris ):
777
+ profile_details = []
778
+ sorted_profile_keys = _get_sorted_profile_keys (cgmes_profile_uris .keys ())
779
+ for index , profile in enumerate (sorted_profile_keys ):
780
+ profile_info = {
781
+ "index" : index ,
782
+ "short_name" : profile ,
783
+ "long_name" : _extract_profile_long_name (cgmes_profile_uris [profile ]),
784
+ "uris" : [{"uri" : uri } for uri in cgmes_profile_uris [profile ]],
785
+ }
786
+ profile_details .append (profile_info )
787
+ return profile_details
788
+
789
+
790
+ def _extract_profile_long_name (profile_uris ):
791
+ # Extract name from uri, e.g. "Topology" from "http://iec.ch/TC57/2013/61970-456/Topology/4"
792
+ # Examples of other possible uris: "http://entsoe.eu/CIM/Topology/4/1", "http://iec.ch/TC57/ns/CIM/Topology-EU/3.0"
793
+ # If more than one uri given, extract common part (e.g. "Equipment" from "EquipmentCore" and "EquipmentOperation")
794
+ long_name = ""
795
+ for uri in profile_uris :
796
+ match = re .search (r"/([^/-]*)(-[^/]*)?(/\d+)?/[\d.]+?$" , uri )
797
+ if match :
798
+ name = match .group (1 )
799
+ if long_name :
800
+ for idx in range (1 , len (long_name )):
801
+ if idx >= len (name ) or long_name [idx ] != name [idx ]:
802
+ long_name = long_name [:idx ]
803
+ break
804
+ else :
805
+ long_name = name
806
+ return long_name
807
+
808
+
809
+ def _get_sorted_profile_keys (profile_key_list ):
810
+ """Sort profiles alphabetically, but "EQ" to the first place.
811
+
812
+ Profiles should be always used in the same order when they are written into the enum class Profile.
813
+ The same order should be used if one of several possible profiles is to be selected.
814
+
815
+ :param profile_key_list: List of short profile names.
816
+ :return: Sorted list of short profile names.
817
+ """
818
+ return sorted (profile_key_list , key = lambda x : x == "EQ" and "0" or x )
819
+
820
+
821
+ def _get_recommended_class_profiles (elem_dict ):
822
+ """Get the recommended profiles for all classes.
823
+
824
+ This function searches for the recommended profile of each class.
825
+ If the class contains attributes for different profiles not all data of the object could be written into one file.
826
+ To write the data to as few as possible files the class profile should be that with most of the attributes.
827
+ But some classes contain a lot of rarely used special attributes, i.e. attributes for a special profile
828
+ (e.g. TopologyNode has many attributes for TopologyBoundary, but the class profile should be Topology).
829
+ That's why attributes that only belong to one profile are skipped in the search algorithm.
830
+
831
+ :param elem_dict: Information about all classes.
832
+ Used are here possible class profiles (elem_dict[class_name].origins()),
833
+ possible attribute profiles (elem_dict[class_name].attributes()[*]["attr_origin"])
834
+ and the superclass of each class (elem_dict[class_name].superClass()).
835
+ :return: Mapping of class to profile.
836
+ """
837
+ recommended_class_profiles = {}
838
+ for class_name in elem_dict .keys ():
839
+ class_origin = elem_dict [class_name ].origins ()
840
+ class_profiles = [origin ["origin" ] for origin in class_origin ]
841
+ if len (class_profiles ) == 1 :
842
+ recommended_class_profiles [class_name ] = class_profiles [0 ]
843
+ continue
844
+
845
+ # Count profiles of all attributes of this class and its superclasses
846
+ profile_count_map = {}
847
+ name = class_name
848
+ while name :
849
+ for attribute in _find_multiple_attributes (elem_dict [name ].attributes ()):
850
+ profiles = [origin ["origin" ] for origin in attribute ["attr_origin" ]]
851
+ ambiguous_profile = len (profiles ) > 1
852
+ for profile in profiles :
853
+ if ambiguous_profile and profile in class_profiles :
854
+ profile_count_map .setdefault (profile , []).append (attribute ["label" ])
855
+ name = elem_dict [name ].superClass ()
856
+
857
+ # Set the profile with most attributes as recommended profile for this class
858
+ if profile_count_map :
859
+ max_count = max (len (v ) for v in profile_count_map .values ())
860
+ filtered_profiles = [k for k , v in profile_count_map .items () if len (v ) == max_count ]
861
+ recommended_class_profiles [class_name ] = _get_sorted_profile_keys (filtered_profiles )[0 ]
862
+ else :
863
+ recommended_class_profiles [class_name ] = _get_sorted_profile_keys (class_profiles )[0 ]
864
+ return recommended_class_profiles
0 commit comments