Skip to content

Commit 0b2e606

Browse files
author
Nicolas Georges
committed
Update vipapp.py
- Add settings and source fields in appversion, with related command-line flags on all relevant commands. The source field is optional if vip-portal doesn't support it yet - Preserve doi field - sync_index: Add support for per-row values for resources, settings, and source
1 parent a491a2e commit 0b2e606

1 file changed

Lines changed: 101 additions & 35 deletions

File tree

vipapps/vipapps.py

Lines changed: 101 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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
5558
def 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
202226
def 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
261287
class 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
454496
def 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
564607
def 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+
574636
def 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

588654
def add_sync_options(cmd):
589655
add_import_options(cmd)

0 commit comments

Comments
 (0)