2323from khiops .core .exceptions import KhiopsJSONError
2424from khiops .core .internals .common import (
2525 deprecation_message ,
26+ is_dict_like ,
27+ is_list_like ,
2628 is_string_like ,
2729 type_error_message ,
2830)
@@ -43,10 +45,6 @@ def _format_name(name):
4345
4446 Otherwise, it returns the name between backquoted (backquotes within are doubled)
4547 """
46- # Check that the type of name is string or bytes
47- if not is_string_like (name ):
48- raise TypeError (type_error_message ("name" , name , "string-like" ))
49-
5048 # Check if the name is an identifier
5149 # Python isalnum is not used because of utf-8 encoding (accentuated chars
5250 # are considered alphanumeric)
@@ -81,6 +79,57 @@ def _quote_value(value):
8179 return quoted_value
8280
8381
82+ def _check_name (name ):
83+ """Ensures the variable name is consistent
84+ with the Khiops core name constraints
85+
86+ Plain string or bytes are both accepted as input.
87+ The Khiops core forbids a name
88+ - with a length outside the [1,128] interval
89+ - containing a simple (Unix) carriage-return (\n )
90+ - with leading and trailing spaces
91+ (\s in Perl-Compatible-Regular-Expressions syntax).
92+ This function must check at least these constraints.
93+ """
94+ # Check that the type of name is string or bytes
95+ if not is_string_like (name ):
96+ raise TypeError (type_error_message ("name" , name , "string-like" ))
97+
98+ # Check the name complies with the Khiops core constraints
99+ if isinstance (name , str ):
100+ return len (name ) <= 128 and "\n " not in name and name == name .strip ()
101+ else :
102+ assert isinstance (name , bytes )
103+ return len (name ) <= 128 and b"\n " not in name and name == name .strip ()
104+
105+
106+ def _is_valid_type (type_str ):
107+ """Checks whether the type is known"""
108+ return (
109+ _is_native_type (type_str )
110+ or _is_object_type (type_str )
111+ or type_str in ["TextList" , "Structure" ]
112+ ) # internal types
113+
114+
115+ def _is_native_type (type_str ):
116+ """Checks whether the type is native (not internal or relational)"""
117+ return type_str in [
118+ "Categorical" ,
119+ "Numerical" ,
120+ "Time" ,
121+ "Date" ,
122+ "Timestamp" ,
123+ "TimestampTZ" ,
124+ "Text" ,
125+ ]
126+
127+
128+ def _is_object_type (type_str ):
129+ """Checks whether the type is an object one (relational)"""
130+ return type_str in ["Entity" , "Table" ]
131+
132+
84133class DictionaryDomain (KhiopsJSONObject ):
85134 """Main class containing the information of a Khiops dictionary file
86135
@@ -769,6 +818,130 @@ def add_variable(self, variable):
769818 self .variables .append (variable )
770819 self ._variables_by_name [variable .name ] = variable
771820
821+ def add_variable_from_spec (
822+ self ,
823+ name ,
824+ type ,
825+ label = None ,
826+ used = True ,
827+ object_type = None ,
828+ structure_type = None ,
829+ rule = None ,
830+ meta_data = None ,
831+ ):
832+ """Adds a variable to this dictionary using a complete specification
833+
834+ Parameters
835+ ----------
836+ name : str
837+ Variable name
838+ type : str
839+ Variable type, See `Variable`
840+ label : str, optional
841+ Label of the variable.
842+ used : bool, default ``True``
843+ Usage status of the variable.
844+ object_type : str, optional
845+ Object type. Ignored if variable type not in ["Entity", "Table"]
846+ structure_type : str, optional
847+ Structure type. Ignored if variable type is not "Structure"
848+ rule : str, optional
849+ Variable rule (in verbatim).
850+ meta_data : dict, optional
851+ A Python dictionary which holds the metadata specification
852+ with the following keys:
853+ - keys : list, default []
854+ list of meta-data keys
855+ - values : list, default []
856+ list of meta-data values.
857+ The values can be str, bool, float or int.
858+
859+ Raises
860+ ------
861+ `ValueError`
862+ - If the variable name is empty or does not comply
863+ with the formatting constraints.
864+ - If there is already a variable with the same name.
865+ - If the given variable type is unknown.
866+ - If a native type is given 'object_type' or 'structure_type'
867+ - If the 'meta_data' is not a dictionary
868+ """
869+ # Values and Types checks
870+ if not name :
871+ raise ValueError (
872+ "Cannot add to dictionary unnamed variable " f"(name = '{ name } ')"
873+ )
874+ if name in self ._variables_by_name :
875+ raise ValueError (f"Dictionary already has a variable named '{ name } '" )
876+ if not _is_valid_type (type ):
877+ raise ValueError (f"Invalid type '{ type } '" )
878+ if _is_native_type (type ):
879+ if object_type or structure_type :
880+ raise ValueError (
881+ f"Native type '{ type } ' "
882+ "cannot have 'object_type' or 'structure_type'"
883+ )
884+ if _is_object_type (type ) and object_type is None :
885+ raise ValueError (f"'object_type' must be provided for type '{ type } '" )
886+ if meta_data is not None :
887+ if not is_dict_like (meta_data ):
888+ raise TypeError (type_error_message ("meta_data" , meta_data , "dict-like" ))
889+ if "keys" not in meta_data or "values" not in meta_data :
890+ raise ValueError (
891+ "'meta_data' does not contain "
892+ "the mandatory keys 'keys' and 'values'"
893+ )
894+ if not is_list_like (meta_data ["keys" ]):
895+ raise TypeError (
896+ type_error_message (
897+ "meta_data['keys']" , meta_data ["keys" ], "list-like"
898+ )
899+ )
900+ if not is_list_like (meta_data ["values" ]):
901+ raise TypeError (
902+ type_error_message (
903+ "meta_data['values']" , meta_data ["values" ], "list-like"
904+ )
905+ )
906+ if len (meta_data ["keys" ]) != len (meta_data ["values" ]):
907+ raise ValueError (
908+ "'meta_data' keys and values " "do not have the same size"
909+ )
910+ if label is not None :
911+ if not is_string_like (label ):
912+ raise TypeError (type_error_message ("label" , label , "string-like" ))
913+ if object_type is not None :
914+ if not is_string_like (object_type ):
915+ raise TypeError (
916+ type_error_message ("object_type" , object_type , "string-like" )
917+ )
918+ if structure_type is not None :
919+ if not is_string_like (structure_type ):
920+ raise TypeError (
921+ type_error_message ("structure_type" , structure_type , "string-like" )
922+ )
923+ if rule is not None :
924+ if not is_string_like (rule ):
925+ raise TypeError (type_error_message ("rule" , rule , "string-like" ))
926+
927+ # Variable initialization
928+ variable = Variable ()
929+ variable .name = name
930+ variable .type = type
931+ variable .used = used
932+ if meta_data is not None :
933+ for key , value in zip (meta_data ["keys" ], meta_data ["values" ]):
934+ variable .meta_data .add_value (key , value )
935+ if label is not None :
936+ variable .label = label
937+ if object_type is not None :
938+ variable .object_type = object_type
939+ if structure_type is not None :
940+ variable .structure_type = structure_type
941+ if rule is not None :
942+ variable .rule = Rule (verbatim = rule )
943+ self .add_variable (variable )
944+
772945 def remove_variable (self , variable_name ):
773946 """Removes the specified variable from this dictionary
774947
@@ -1017,7 +1190,9 @@ def __init__(self, json_data=None):
10171190 raise TypeError (type_error_message ("json_data" , json_data , dict ))
10181191
10191192 # Main attributes
1020- self .name = ""
1193+ # The variable name is protected attribute accessible only via a property
1194+ # to ensure it is always valid
1195+ self ._name = ""
10211196 self .label = ""
10221197 self .comments = []
10231198 self .used = True
@@ -1058,7 +1233,7 @@ def __init__(self, json_data=None):
10581233 self .type = json_data .get ("type" )
10591234
10601235 # Initialize complement of the type
1061- if self .type in ( "Entity" , "Table" ):
1236+ if _is_object_type ( self .type ):
10621237 self .object_type = json_data .get ("objectType" )
10631238 elif self .type == "Structure" :
10641239 self .structure_type = json_data .get ("structureType" )
@@ -1072,7 +1247,7 @@ def __init__(self, json_data=None):
10721247 self .meta_data = MetaData (json_meta_data )
10731248
10741249 def __repr__ (self ):
1075- """Returns a human readable string representation"""
1250+ """Returns a human- readable string representation"""
10761251 return f"Variable ({ self .name } )"
10771252
10781253 def __str__ (self ):
@@ -1081,6 +1256,19 @@ def __str__(self):
10811256 self .write (writer )
10821257 return str (stream .getvalue (), encoding = "utf8" , errors = "replace" )
10831258
1259+ @property
1260+ def name (self ):
1261+ return self ._name
1262+
1263+ @name .setter
1264+ def name (self , value ):
1265+ if not _check_name (value ):
1266+ raise ValueError (
1267+ f"Variable name '{ value } ' cannot be accepted "
1268+ "(invalid length or characters)"
1269+ )
1270+ self ._name = value
1271+
10841272 def copy (self ):
10851273 """Copies this variable instance
10861274
@@ -1179,7 +1367,7 @@ def full_type(self):
11791367 basic.
11801368 """
11811369 full_type = self .type
1182- if self .type in ( "Entity" , "Table" ):
1370+ if _is_object_type ( self .type ):
11831371 full_type += f"({ self .object_type } )"
11841372 elif self .type == "Structure" :
11851373 full_type += f"({ self .structure_type } )"
0 commit comments