2323from khiops .core .exceptions import KhiopsJSONError
2424from khiops .core .internals .common import (
2525 deprecation_message ,
26+ is_dict_like ,
2627 is_string_like ,
2728 type_error_message ,
2829)
@@ -43,10 +44,6 @@ def _format_name(name):
4344
4445 Otherwise, it returns the name between backquoted (backquotes within are doubled)
4546 """
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-
5047 # Check if the name is an identifier
5148 # Python isalnum is not used because of utf-8 encoding (accentuated chars
5249 # are considered alphanumeric)
@@ -81,6 +78,70 @@ def _quote_value(value):
8178 return quoted_value
8279
8380
81+ def _check_name (name ):
82+ """Ensures the variable name is consistent
83+ with the Khiops core name constraints
84+
85+ Plain string or bytes are both accepted as input.
86+ The Khiops core forbids a name
87+ - with a length outside the [1,128] interval
88+ - containing a simple (Unix) carriage-return (\n )
89+ - with leading and trailing spaces.
90+ This function must check at least these constraints.
91+
92+ Parameters
93+ ----------
94+ name : str
95+ Name to be validated.
96+ Raises
97+ ------
98+ `ValueError`
99+ If the provided name does not comply with the formatting constraints.
100+ """
101+ # Check that the type of name is string or bytes
102+ if not is_string_like (name ):
103+ raise TypeError (type_error_message ("name" , name , "string-like" ))
104+
105+ # Check the name complies with the Khiops core constraints
106+ if isinstance (name , str ):
107+ contains_carriage_return = "\n " in name
108+ else :
109+ assert isinstance (name , bytes )
110+ contains_carriage_return = b"\n " in name
111+ if len (name ) > 128 or contains_carriage_return or name != name .strip ():
112+ raise ValueError (
113+ f"Variable name '{ name } ' cannot be accepted "
114+ "(invalid length or characters)"
115+ )
116+
117+
118+ def _is_valid_type (type_str ):
119+ """Checks whether the type is known"""
120+ return (
121+ _is_native_type (type_str )
122+ or _is_object_type (type_str )
123+ or type_str in ["TextList" , "Structure" ]
124+ ) # internal types
125+
126+
127+ def _is_native_type (type_str ):
128+ """Checks whether the type is native (not internal or relational)"""
129+ return type_str in [
130+ "Categorical" ,
131+ "Numerical" ,
132+ "Time" ,
133+ "Date" ,
134+ "Timestamp" ,
135+ "TimestampTZ" ,
136+ "Text" ,
137+ ]
138+
139+
140+ def _is_object_type (type_str ):
141+ """Checks whether the type is an object one (relational)"""
142+ return type_str in ["Entity" , "Table" ]
143+
144+
84145class DictionaryDomain (KhiopsJSONObject ):
85146 """Main class containing the information of a Khiops dictionary file
86147
@@ -769,6 +830,102 @@ def add_variable(self, variable):
769830 self .variables .append (variable )
770831 self ._variables_by_name [variable .name ] = variable
771832
833+ def add_variable_from_spec (
834+ self ,
835+ name ,
836+ type ,
837+ label = "" ,
838+ used = True ,
839+ object_type = None ,
840+ structure_type = None ,
841+ rule = None ,
842+ meta_data = None ,
843+ ):
844+ """Adds a variable to this dictionary using a complete specification
845+
846+ Parameters
847+ ----------
848+ name : str
849+ Variable name.
850+ type : str
851+ Variable type. See `Variable`.
852+ label : str, default ""
853+ Label of the variable.
854+ used : bool, default ``True``
855+ Usage status of the variable.
856+ object_type : str, optional
857+ Object type. Ignored if variable type not in ["Entity", "Table"].
858+ structure_type : str, optional
859+ Structure type. Ignored if variable type is not "Structure".
860+ rule : `Rule`, optional
861+ Variable rule.
862+ meta_data : dict, optional
863+ A Python dictionary which holds the metadata specification.
864+ The dictionary keys are str. The values can be str, bool, float or int.
865+
866+ Raises
867+ ------
868+ `ValueError`
869+ - If the variable name is empty or does not comply
870+ with the formatting constraints.
871+ - If there is already a variable with the same name.
872+ - If the given variable type is unknown.
873+ - If a native type is given 'object_type' or 'structure_type'.
874+ - If the 'meta_data' is not a dictionary.
875+ """
876+ # Values and Types checks
877+ if not name :
878+ raise ValueError (
879+ "Cannot add to dictionary unnamed variable " f"(name = '{ name } ')"
880+ )
881+ if name in self ._variables_by_name :
882+ raise ValueError (f"Dictionary already has a variable named '{ name } '" )
883+ if not _is_valid_type (type ):
884+ raise ValueError (f"Invalid type '{ type } '" )
885+ if _is_native_type (type ):
886+ if object_type or structure_type :
887+ raise ValueError (
888+ f"Native type '{ type } ' "
889+ "cannot have 'object_type' or 'structure_type'"
890+ )
891+ if _is_object_type (type ) and object_type is None :
892+ raise ValueError (f"'object_type' must be provided for type '{ type } '" )
893+ if type == "Structure" and structure_type is None :
894+ raise ValueError (f"'structure_type' must be provided for type '{ type } '" )
895+ if meta_data is not None :
896+ if not is_dict_like (meta_data ):
897+ raise TypeError (type_error_message ("meta_data" , meta_data , "dict-like" ))
898+ if object_type is not None :
899+ if not is_string_like (object_type ):
900+ raise TypeError (
901+ type_error_message ("object_type" , object_type , "string-like" )
902+ )
903+ if structure_type is not None :
904+ if not is_string_like (structure_type ):
905+ raise TypeError (
906+ type_error_message ("structure_type" , structure_type , "string-like" )
907+ )
908+ if rule is not None :
909+ if not isinstance (rule , Rule ):
910+ raise TypeError (type_error_message ("rule" , rule , Rule ))
911+
912+ # Variable initialization
913+ variable = Variable ()
914+ variable .name = name
915+ variable .type = type
916+ variable .used = used
917+ if meta_data is not None :
918+ for key , value in meta_data .items ():
919+ variable .meta_data .add_value (key , value )
920+ variable .label = label
921+ if object_type is not None :
922+ variable .object_type = object_type
923+ if structure_type is not None :
924+ variable .structure_type = structure_type
925+ if rule is not None :
926+ variable .set_rule (rule )
927+ self .add_variable (variable )
928+
772929 def remove_variable (self , variable_name ):
773930 """Removes the specified variable from this dictionary
774931
@@ -1017,7 +1174,9 @@ def __init__(self, json_data=None):
10171174 raise TypeError (type_error_message ("json_data" , json_data , dict ))
10181175
10191176 # Main attributes
1020- self .name = ""
1177+ # The variable name is protected attribute accessible only via a property
1178+ # to ensure it is always valid
1179+ self ._name = ""
10211180 self .label = ""
10221181 self .comments = []
10231182 self .used = True
@@ -1058,7 +1217,7 @@ def __init__(self, json_data=None):
10581217 self .type = json_data .get ("type" )
10591218
10601219 # Initialize complement of the type
1061- if self .type in ( "Entity" , "Table" ):
1220+ if _is_object_type ( self .type ):
10621221 self .object_type = json_data .get ("objectType" )
10631222 elif self .type == "Structure" :
10641223 self .structure_type = json_data .get ("structureType" )
@@ -1072,7 +1231,7 @@ def __init__(self, json_data=None):
10721231 self .meta_data = MetaData (json_meta_data )
10731232
10741233 def __repr__ (self ):
1075- """Returns a human readable string representation"""
1234+ """Returns a human- readable string representation"""
10761235 return f"Variable ({ self .name } )"
10771236
10781237 def __str__ (self ):
@@ -1081,6 +1240,15 @@ def __str__(self):
10811240 self .write (writer )
10821241 return str (stream .getvalue (), encoding = "utf8" , errors = "replace" )
10831242
1243+ @property
1244+ def name (self ):
1245+ return self ._name
1246+
1247+ @name .setter
1248+ def name (self , value ):
1249+ _check_name (value )
1250+ self ._name = value
1251+
10841252 def copy (self ):
10851253 """Copies this variable instance
10861254
@@ -1179,7 +1347,7 @@ def full_type(self):
11791347 basic.
11801348 """
11811349 full_type = self .type
1182- if self .type in ( "Entity" , "Table" ):
1350+ if _is_object_type ( self .type ):
11831351 full_type += f"({ self .object_type } )"
11841352 elif self .type == "Structure" :
11851353 full_type += f"({ self .structure_type } )"
0 commit comments