@@ -49,7 +49,10 @@ def convert_app_version(av):
4949 name = av ["applicationName" ]
5050 identifier = name + "/" + av ["version" ]
5151 desc = json .loads (av ["descriptor" ])
52- return {"name" :name ,"identifier" :identifier ,"descriptor" :desc ,"rawtext" :av ["descriptor" ],"resources" :av ["resources" ],"tags" :av ["tags" ],"is_visible" :av ["visible" ],"settings" :av ["settings" ]}
52+ result = {"name" :name ,"identifier" :identifier ,"descriptor" :desc ,"rawtext" :av ["descriptor" ],"resources" :av ["resources" ],"tags" :av ["tags" ],"is_visible" :av ["visible" ],"settings" :av ["settings" ],"doi" :av ["doi" ]}
53+ if "source" in av :
54+ result ["source" ] = av ["source" ]
55+ return result
5356
5457# get_apps(): get a list of apps and descriptors from a VIP-portal instance
5558def get_apps () -> list :
@@ -64,7 +67,7 @@ def get_app(identifier) -> object:
6467 appver = vip .generic_get ("admin/appVersions/" + urllib .parse .quote (identifier ))
6568 except RuntimeError as e :
6669 # XXX this is not a very good way to test whether an app exists or not:
67- # something that explicitly searches for and idea and returns true/false
70+ # something that explicitly searches for an id and returns true/false
6871 # with 200 OK would be more reliable.
6972 # print(e) # Error 8000 from VIP
7073 return None
@@ -196,7 +199,28 @@ def get_files_from_dir(dirname: str, silent=False) -> dict:
196199 files [identifier ] = file
197200 return files
198201
199- # same as above, but from a CSV index
202+ # add optional fields from a csv row to a "file" dict
203+ def csv_add_fields (file , csv_header , row , identifier , silent = False ):
204+ if "resources" in csv_header :
205+ val = row [csv_header ["resources" ]]
206+ try :
207+ file ["resources" ] = parse_strlist (val )
208+ except argparse .ArgumentTypeError :
209+ if not silent :
210+ printerr ("warning: %s: ignoring invalid resources '%s'"
211+ % (identifier , val ))
212+ if "settings" in csv_header :
213+ val = row [csv_header ["settings" ]]
214+ try :
215+ file ["settings" ] = parse_map (val )
216+ except argparse .ArgumentTypeError as e :
217+ if not silent :
218+ printerr ("warning: %s: ignoring invalid settings '%s'"
219+ % (identifier , val ))
220+ if "source" in csv_header :
221+ file ["source" ] = row [csv_header ["source" ]]
222+
223+ # same as get_files_from_dir(), but from a CSV index
200224# each app is expected to have three strings: name,version,descriptorPath,
201225# resolved from the CSV header
202226def get_files_from_index (indexfile : str , silent = False ) -> dict :
@@ -246,24 +270,24 @@ def get_files_from_index(indexfile: str, silent=False) -> dict:
246270 continue
247271 if appversion != desc ["tool-version" ]:
248272 printerr ("%s: skipped: app version '%s' doesn't match descriptor '%s'"
249- % (filepath , appname , desc ["tool-version" ]))
273+ % (filepath , appversion , desc ["tool-version" ]))
250274 continue
251275 # check for normalized descriptor filename (just a warning)
252276 normname = descriptor_filename (appname , appversion )
253277 if os .path .basename (filepath ) != normname and not silent :
254278 printerr ("warning: %s: incorrect descriptor filename, should be '%s'"
255279 % (filepath , normname ))
280+ # add extra fields from csv row
281+ csv_add_fields (file , csv_header , row , identifier , silent = silent )
256282 # add file to list
257283 files [identifier ] = file
258284 return files
259285
260286# helper class for the default values of extra fields in apps and appversions
261287class AppFields :
262- # XXX TODO: preserve doi, new "origin" field?
263- # XXX somewhat obscure behavior: owner/group/citation/public are app level,
264- # not appversion, and can't be changed on update (see import_file())
265- # This should either be ajusted to that all fields can be edited (impacting
266- # GET requests), or made more explicit in command-line args.
288+ # Note a non-obvious behavior: owner/group/citation/public are app-level,
289+ # not appversion-level, and can't be changed on update (see import_file())
290+ # This should be kept explicit in command-line args.
267291
268292 # default values for a new app:
269293 owner = None
@@ -274,6 +298,8 @@ class AppFields:
274298 tags = []
275299 settings = {}
276300 is_visible = True
301+ doi = None
302+ source = ""
277303 # app is defined on update only, and contains the existing appversion
278304 # args is defined everytime, and contains user-provided values
279305 def __init__ (self , app = None , args = None ):
@@ -283,6 +309,9 @@ def __init__(self, app=None, args=None):
283309 self .resources = app ["resources" ]
284310 self .tags = app ["tags" ]
285311 self .settings = app ["settings" ]
312+ self .doi = app ["doi" ]
313+ if "source" in app :
314+ self .source = app ["source" ]
286315 # override with args values when defined
287316 # note an important difference between None and "" here:
288317 # . None means the arg was not specified, so its value is unchanged
@@ -296,11 +325,15 @@ def __init__(self, app=None, args=None):
296325 if args .public != None :
297326 self .public = args .public
298327 if args .groups != None :
299- self .groups = [] if args .groups == "" else args . groups . split ( "," )
328+ self .groups = args .groups
300329 if args .resources != None :
301- self .resources = [] if args .resources == "" else args . resources . split ( "," )
330+ self .resources = args .resources
302331 if args .visible != None :
303332 self .is_visible = args .visible
333+ if args .settings != None :
334+ self .settings = args .settings
335+ if args .source != None :
336+ self .source = args .source
304337
305338# import an app from a descriptor file to a VIP-portal instance
306339# file is assumed already loaded and checked, VIP-portal will re-check anyways
@@ -312,7 +345,7 @@ def import_file(file, fields, is_overwrite=False, dry_run=True, verbose=False):
312345 descriptor = file ["rawtext" ]
313346 app = {"name" :appname ,"applicationGroups" :fields .groups ,"owner" :fields .owner ,"citation" :fields .citation ,"public" :fields .public }
314347 app_url = "admin/applications/" + urllib .parse .quote (appname )
315- appver = {"applicationName" :appname ,"version" :version ,"descriptor" :descriptor ,"visible" :fields .is_visible ,"resources" :fields .resources ,"tags" :fields .tags ,"settings" :fields .settings }
348+ appver = {"applicationName" :appname ,"version" :version ,"descriptor" :descriptor ,"doi" : fields . doi , " visible" :fields .is_visible ,"resources" :fields .resources ,"tags" :fields .tags ,"settings" :fields .settings , "source" : fields . source }
316349 appver_url = "admin/appVersions/" + urllib .parse .quote (appname ) + "/" + urllib .parse .quote (version )
317350 msg = ""
318351 if is_overwrite :
@@ -321,7 +354,7 @@ def import_file(file, fields, is_overwrite=False, dry_run=True, verbose=False):
321354 msg += " (dry run)"
322355 # never change app on update: doing so would be arguable if there are
323356 # several appversions per app, and also it avoids having to fetch app
324- # fields on GET (see Appfields ())
357+ # fields on GET (see AppFields ())
325358 can_put_app = not is_overwrite
326359 print ("importing app %s %s%s" % (appname , version , msg ))
327360 if can_put_app :
@@ -391,10 +424,9 @@ def compare_descriptors(d1, d2) -> bool:
391424 return ordered (clean_descriptor (d1 ))== ordered (clean_descriptor (d2 ))
392425
393426# import helpers
394- def import_existing_app (app , file , args = None , is_overwrite = False ,
427+ def import_existing_app (app , file , fields , is_overwrite = False ,
395428 dry_run = True , verbose = False , force_update = False ):
396429 identifier = app ["identifier" ]
397- fields = AppFields (app = app , args = args )
398430 # XXX here we could also compare non-descriptor fields?
399431 if compare_descriptors (app , file ) and not force_update :
400432 if verbose :
@@ -433,22 +465,32 @@ def perform_sync(args, apps, files):
433465 file = None
434466 elif app ["identifier" ] > file ["identifier" ]:
435467 app = None
436- if app != None and file != None :
437- # app identifiers match: compare descriptors and import if changed
438- import_existing_app (app , file , args = args ,
439- is_overwrite = args .overwrite ,
440- dry_run = args .dry_run , verbose = args .verbose ,
441- force_update = args .force_update )
442- i += 1
443- j += 1
468+ if file != None :
469+ # set field values from global args + per-file overrides
470+ fields = AppFields (app = app , args = args )
471+ if "resources" in file :
472+ fields .resources = file ["resources" ]
473+ if "settings" in file :
474+ fields .settings = file ["settings" ]
475+ if "source" in file :
476+ fields .source = file ["source" ]
477+ # do the actual sync
478+ if app != None :
479+ # identifiers match: compare descriptors and import if changed
480+ import_existing_app (app , file , fields ,
481+ is_overwrite = args .overwrite ,
482+ dry_run = args .dry_run , verbose = args .verbose ,
483+ force_update = args .force_update )
484+ i += 1
485+ j += 1
486+ else : # app=None,file!=None: import new app
487+ import_new_app (file , fields ,
488+ dry_run = args .dry_run , verbose = args .verbose )
489+ j += 1
444490 elif app != None :
445491 if args .show_orphans :
446492 print ("%s: orphan app with no descriptor" % app ["identifier" ])
447493 i += 1
448- elif file != None : # import new app
449- import_new_app (file , AppFields (args = args ),
450- dry_run = args .dry_run , verbose = args .verbose )
451- j += 1
452494
453495# helper for list_* commands
454496def print_app (label : str , identifier : str , desc : dict ,
@@ -506,13 +548,14 @@ def cmd_import_file(args):
506548 appname = file ["descriptor" ]["name" ]
507549 version = file ["descriptor" ]["tool-version" ]
508550 app = get_app (file ["identifier" ])
551+ fields = AppFields (app = app , args = args )
509552 if app != None : # app already exists
510- import_existing_app (app , file , args = args ,
553+ import_existing_app (app , file , fields ,
511554 is_overwrite = args .overwrite ,
512555 dry_run = args .dry_run , verbose = args .verbose ,
513556 force_update = args .force_update )
514557 else : # new app
515- import_new_app (file , AppFields ( args = args ) ,
558+ import_new_app (file , fields ,
516559 dry_run = args .dry_run , verbose = args .verbose )
517560
518561# check a single descriptor file
@@ -560,7 +603,7 @@ def add_subcommand(subparsers, name, func, help=None):
560603 cmd .set_defaults (func = func )
561604 return cmd
562605
563- # helper to allow --visible =true/false, with default None
606+ # parse --flag =true/false, with default None
564607def parse_bool (val ):
565608 if isinstance (val , bool ):
566609 return val
@@ -571,6 +614,25 @@ def parse_bool(val):
571614 else :
572615 raise argparse .ArgumentTypeError ("boolean expected" )
573616
617+ # parse --flag=a,b,c and return a list. In practice this can't really fail.
618+ def parse_strlist (val ):
619+ if not isinstance (val , str ):
620+ raise argparse .ArgumentTypeError ("string expected" )
621+ if val == "" :
622+ return []
623+ else :
624+ return val .split ("," )
625+
626+ # parse --flag='{<json>}', return a dict
627+ def parse_map (val ):
628+ try :
629+ r = json .loads (val )
630+ if not isinstance (r , dict ):
631+ raise argparse .ArgumentTypeError ("json map expected" )
632+ return r
633+ except json .decoder .JSONDecodeError as e :
634+ raise argparse .ArgumentTypeError ("invalid json" ,e )
635+
574636def add_list_options (cmd ):
575637 cmd .add_argument ("--show-descriptor" , action = "store_true" , help = "show parsed descriptor content" )
576638 cmd .add_argument ("--show-imagename" , action = "store_true" , help = "show container image name" )
@@ -579,11 +641,15 @@ def add_import_options(cmd):
579641 cmd .add_argument ("--dry-run" , action = "store_true" , help = "perform no changes, just show what would be done" )
580642 cmd .add_argument ("--overwrite" , action = "store_true" , help = "overwrite existing apps" )
581643 cmd .add_argument ("--force-update" , action = "store_true" , help = "force update even if descriptor didn't change" )
582- cmd .add_argument ("--owner" , type = str , help = "set owner for new apps" )
583- cmd .add_argument ("--groups" , type = str , help = "set groups for new apps" )
584- cmd .add_argument ("--public" , type = parse_bool , help = "set is_public for new apps" )
585- cmd .add_argument ("--resources" , type = str , help = "set resources for new or update apps" )
586- cmd .add_argument ("--visible" , type = parse_bool , help = "set visibility for new or update apps" )
644+ # app fields (create only)
645+ cmd .add_argument ("--owner" , type = str , help = "set owner field (create only)" )
646+ cmd .add_argument ("--groups" , type = parse_strlist , help = "set groups field (create only)" )
647+ cmd .add_argument ("--public" , type = parse_bool , help = "set public field (create only)" )
648+ # appversion fields (create+update)
649+ cmd .add_argument ("--resources" , type = parse_strlist , help = "set resources field (create+update)" )
650+ cmd .add_argument ("--visible" , type = parse_bool , help = "set visible field (create+update)" )
651+ cmd .add_argument ("--settings" , type = parse_map , help = "set settings field (create+update)" )
652+ cmd .add_argument ("--source" , type = str , help = "set source field (create+update)" )
587653
588654def add_sync_options (cmd ):
589655 add_import_options (cmd )
0 commit comments