@@ -31,8 +31,8 @@ def get_clams_pyver():
3131 import clams
3232 return clams .__version__
3333 except ImportError :
34- version_fname = os . path . join ( os . path . dirname ( __file__ ), '..' , '..' , ' VERSION' )
35- if os . path . exists (version_fname ):
34+ version_fname = Path ( __file__ ). joinpath ( '../../ VERSION' )
35+ if version_fname . exists ():
3636 with open (version_fname ) as version_f :
3737 return version_f .read ().strip ()
3838 else :
@@ -59,13 +59,21 @@ def get_mmif_specver():
5959 return mmif .__specver__
6060
6161
62+ def pop_titles (js ):
63+ for prop in js .get ('properties' , {}).values ():
64+ prop .pop ('title' , None )
65+
66+
67+ def jsonschema_versioning (js ):
68+ js ['$schema' ] = pydantic .json_schema .GenerateJsonSchema .schema_dialect
69+ js ['$comment' ] = f"clams-python SDK { get_clams_pyver ()} was used to generate this schema"
70+
71+
6272class _BaseModel (pydantic .BaseModel ):
6373
64- class Config :
65- @staticmethod
66- def json_schema_extra (schema , model ) -> None :
67- for prop in schema .get ('properties' , {}).values ():
68- prop .pop ('title' , None )
74+ model_config = {
75+ "json_schema_extra" : pop_titles
76+ }
6977
7078
7179class Output (_BaseModel ):
@@ -97,17 +105,27 @@ class Output(_BaseModel):
97105 {},
98106 description = "(optional) Specification for type properties, if any. ``\" *\" `` indicates any value."
99107 )
108+
109+ def __init__ (self , ** kwargs ):
110+ super ().__init__ (** kwargs )
111+
112+ @pydantic .field_validator ('at_type' , mode = 'after' ) # because pydantic v2 doesn't auto-convert url to string
113+ @classmethod
114+ def stringify (cls , val ):
115+ return str (val )
100116
101- @pydantic .validator ('at_type' , pre = True )
117+ @pydantic .field_validator ('at_type' , mode = 'before' )
118+ @classmethod
102119 def at_type_must_be_str (cls , v ):
103120 if not isinstance (v , str ):
104121 return str (v )
105122 return v
106123
107- class Config :
108- title = 'CLAMS Output Specification'
109- extra = 'forbid'
110- allow_population_by_field_name = True
124+ model_config = {
125+ 'title' : 'CLAMS Output Specification' ,
126+ 'extra' : 'forbid' ,
127+ 'validate_by_name' : True ,
128+ }
111129
112130 def add_description (self , description : str ):
113131 """
@@ -127,20 +145,21 @@ class Input(Output):
127145
128146 Developers should take diligent care to include all input types and their properties in the app metadata.
129147 """
130- required : bool = pydantic .Field (
148+ required : Optional [ bool ] = pydantic .Field (
131149 None ,
132150 description = "(optional, True by default) Indicating whether this input type is mandatory or optional."
133151 )
134152
135- def __init__ (self , * args , * *kwargs ):
136- super ().__init__ (* args , * *kwargs )
153+ def __init__ (self , ** kwargs ):
154+ super ().__init__ (** kwargs )
137155 if self .required is None :
138156 self .required = True
139157
140- class Config :
141- title = 'CLAMS Input Specification'
142- extra = 'forbid'
143- allow_population_by_field_name = True
158+ model_config = {
159+ 'title' : 'CLAMS Input Specification' ,
160+ 'extra' : 'forbid' ,
161+ 'validate_by_name' : True ,
162+ }
144163
145164
146165class RuntimeParameter (_BaseModel ):
@@ -178,12 +197,13 @@ class RuntimeParameter(_BaseModel):
178197 "desired dictionary is ``{'key1': 'value1', 'key2': 'value2'}``, the default value (used when "
179198 "initializing a parameter) should be ``['key1:value1','key2:value2']``\n ."
180199 )
181- choices : List [real_valued_primitives ] = pydantic .Field (
200+ choices : Optional [ List [real_valued_primitives ] ] = pydantic .Field (
182201 None ,
183202 description = "(optional) List of string values that can be accepted."
184203 )
185- default : Union [real_valued_primitives , List [real_valued_primitives ]] = pydantic .Field (
204+ default : Optional [ Union [real_valued_primitives , List [real_valued_primitives ] ]] = pydantic .Field (
186205 None ,
206+ union_mode = 'left_to_right' ,
187207 description = "(optional) Default value for the parameter.\n \n "
188208 "Notes for developers: \n \n "
189209 "Setting a default value makes a parameter `optional`. \n \n "
@@ -208,9 +228,10 @@ def __init__(self, **kwargs):
208228 if self .multivalued and self .default is not None and not isinstance (self .default , list ):
209229 self .default = [self .default ]
210230
211- class Config :
212- title = 'CLAMS App Runtime Parameter'
213- extra = 'forbid'
231+ model_config = {
232+ 'title' : 'CLAMS App Runtime Parameter' ,
233+ 'extra' : 'forbid' ,
234+ }
214235
215236
216237class AppMetadata (pydantic .BaseModel ):
@@ -236,26 +257,27 @@ class AppMetadata(pydantic.BaseModel):
236257 description = "A longer description of the app (what it does, how to use, etc.)."
237258 )
238259 app_version : str = pydantic .Field (
239- default_factory = generate_app_version ,
260+ '' , # instead of using default_factory, I will use model_validator to set the default value
261+ # this will work around the limitation of exclude_defaults=True condition when serializing
240262 description = "(AUTO-GENERATED, DO NOT SET MANUALLY)\n \n "
241263 "Version of the app.\n \n "
242264 "When the metadata is generated using clams-python SDK, this field is automatically filled in"
243265 )
244266 mmif_version : str = pydantic .Field (
245- default_factory = get_mmif_specver ,
267+ '' , # same as above
246268 description = "(AUTO-GENERATED, DO NOT SET MANUALLY)\n \n "
247269 "Version of MMIF specification the app.\n \n "
248270 "When the metadata is generated using clams-python SDK, this field is automatically filled in."
249271 )
250- analyzer_version : str = pydantic .Field (
272+ analyzer_version : Optional [ str ] = pydantic .Field (
251273 None ,
252274 description = "(optional) Version of an analyzer software, if the app is working as a wrapper for one. "
253275 )
254276 app_license : str = pydantic .Field (
255277 ...,
256278 description = "License information of the app."
257279 )
258- analyzer_license : str = pydantic .Field (
280+ analyzer_license : Optional [ str ] = pydantic .Field (
259281 None ,
260282 description = "(optional) License information of an analyzer software, if the app works as a wrapper for one. "
261283 )
@@ -298,7 +320,7 @@ class AppMetadata(pydantic.BaseModel):
298320 [],
299321 description = "List of runtime parameters. Can be empty."
300322 )
301- dependencies : List [str ] = pydantic .Field (
323+ dependencies : Optional [ List [str ] ] = pydantic .Field (
302324 None ,
303325 description = "(optional) List of software dependencies of the app. \n \n "
304326 "This list is completely optional, as in most cases such dependencies are specified in a separate "
@@ -307,36 +329,38 @@ class AppMetadata(pydantic.BaseModel):
307329 "List items must be strings, not any kind of structured data. Thus, it is recommended to include "
308330 "a package name and its version in the string value at the minimum (e.g., ``clams-python==1.2.3``)."
309331 )
310- more : Dict [str , str ] = pydantic .Field (
332+ more : Optional [ Dict [str , str ] ] = pydantic .Field (
311333 None ,
312334 description = "(optional) A string-to-string map that can be used to store any additional metadata of the app."
313335 )
314336
315- class Config :
316- title = "CLAMS AppMetadata"
317- extra = 'forbid'
318- allow_population_by_field_name = True
319-
320- @staticmethod
321- def json_schema_extra (schema , model ) -> None :
322- for prop in schema .get ('properties' , {}).values ():
323- prop .pop ('title' , None )
324- schema ['$schema' ] = "http://json-schema.org/draft-07/schema#" # currently pydantic doesn't natively support the $schema field. See https://github.com/samuelcolvin/pydantic/issues/1478
325- schema ['$comment' ] = f"clams-python SDK { get_clams_pyver ()} was used to generate this schema" # this is only to hold version information
326-
327- @pydantic .validator ('identifier' , pre = True )
337+ model_config = {
338+ 'title' : 'CLAMS AppMetadata' ,
339+ 'extra' : 'forbid' ,
340+ 'validate_by_name' : True ,
341+ 'json_schema_extra' : lambda schema , model : [adjust (schema ) for adjust in [pop_titles , jsonschema_versioning ]],
342+ }
343+
344+ @pydantic .model_validator (mode = 'after' )
345+ @classmethod
346+ def assign_versions (cls , data ):
347+ if data .app_version == '' :
348+ data .app_version = generate_app_version ()
349+ if data .mmif_version == '' :
350+ data .mmif_version = get_mmif_specver ()
351+ return data
352+
353+ @pydantic .field_validator ('identifier' , mode = 'before' )
354+ @classmethod
328355 def append_version (cls , val ):
329356 prefix = f'{ app_directory_baseurl if "/" not in val else "" } '
330357 suffix = generate_app_version ()
331358 return '/' .join (map (lambda x : x .strip ('/' ), filter (None , (prefix , val , suffix ))))
332359
333- @pydantic .validator ('mmif_version' , pre = True )
334- def auto_mmif_version (cls , val ):
335- return get_mmif_specver ()
336-
337- @pydantic .validator ('app_version' , pre = True )
338- def auto_app_version (cls , val ):
339- return generate_app_version ()
360+ @pydantic .field_validator ('url' , 'identifier' , mode = 'after' ) # because pydantic v2 doesn't auto-convert url to string
361+ @classmethod
362+ def stringify (cls , val ):
363+ return str (val )
340364
341365 def _check_input_duplicate (self , a_input ):
342366 for elem in self .input :
@@ -400,9 +424,7 @@ def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **propertie
400424 :param properties: additional property specifications
401425 :return: the newly added Output object
402426 """
403- new = Output (at_type = at_type )
404- if len (properties ) > 0 :
405- new .properties = properties
427+ new = Output (at_type = at_type , properties = properties )
406428 if new not in self .output :
407429 self .output .append (new )
408430 else :
@@ -412,7 +434,7 @@ def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **propertie
412434 def add_parameter (self , name : str , description : str , type : param_value_types ,
413435 choices : Optional [List [real_valued_primitives ]] = None ,
414436 multivalued : bool = False ,
415- default : Union [real_valued_primitives , List [real_valued_primitives ]] = None ):
437+ default : Union [None , real_valued_primitives , List [real_valued_primitives ]] = None ):
416438 """
417439 Helper method to add an element to the ``parameters`` list.
418440 """
@@ -456,10 +478,7 @@ def add_more(self, key: str, value: str):
456478 raise ValueError ("Key and value should not be empty!" )
457479
458480 def jsonify (self , pretty = False ):
459- if pretty :
460- return self .json (exclude_defaults = True , by_alias = True , indent = 2 )
461- else :
462- return self .json (exclude_defaults = True , by_alias = True )
481+ return self .model_dump_json (exclude_defaults = True , by_alias = True , indent = 2 if pretty else None )
463482
464483
465484if __name__ == '__main__' :
0 commit comments