"""idmtools comps cli comands.Copyright 2021, Bill & Melinda Gates Foundation. All rights reserved."""# flake8: D301importglobimportjsonasjson_parserimportosimportsysfromtypingimportOptional,ListimporttabulatefromgetpassimportgetpassfromloggingimportgetLoggerfromCOMPS.CredentialPromptimportCredentialPromptimportjsonasjefromidmtools.assetsimportAssetCollectionfromidmtools_platform_comps.utils.singularity_buildimportSingularityBuildWorkItemlogger=getLogger(__name__)user_logger=getLogger('user')
[docs]defadd_item(assets:AssetCollection,file:str):""" Add Item. Args: assets: Assets file: File or Directory Returns: None Raises: FileNotFoundError if file cannot be found. """ifos.path.isdir(file):assets.add_directory(file)elifos.path.isfile(file):assets.add_asset(file)else:user_logger.error(f"Cannot find file {file}")raiseFileNotFoundError(f"Cannot find file {file}")
try:
[docs]classStaticCredentialPrompt(CredentialPrompt):"""Provides a class to allow login to comps from a username password that is static or provided."""
[docs]def__init__(self,comps_url,username,password):"""Constructor."""if(comps_urlisNone)or(usernameisNone)or(passwordisNone):raiseRuntimeError('Missing comps_url, or username or password')self._times_prompted=0self.comps_url=comps_urlself.username=usernameself.password=password
[docs]defprompt(self):"""Return our stores username and password."""self._times_prompted=self._times_prompted+1ifself._times_prompted>3:raisePermissionError('Failure authenticating')return{'Username':self.username,'Password':self.password}
os.environ['IDMTOOLS_NO_CONFIG_WARNING']='1'fromidmtools.core.platform_factoryimportPlatformimportclickfromidmtools_platform_comps.utils.assetize_output.assetize_outputimportAssetizeOutputfromidmtools_platform_comps.utils.file_filter_workitemimportDEFAULT_EXCLUDESfromidmtools_platform_comps.comps_platformimportCOMPSPlatform@click.group(short_help="COMPS platform related commands.")@click.argument('config-block')@click.pass_contextdefcomps(ctx:click.Context,config_block):""" Commands related to managing the COMPS platform. CONFIG_BLOCK - Name of configuration section or alias to load COMPS connection information from """ctx.obj=dict(config_block=config_block)@comps.command(help="Login to COMPS")@click.option('--username',required=True,help="Username")@click.option('--password',help="Password")@click.pass_contextdeflogin(ctx:click.Context,username,password):# noqa D103fromCOMPSimportClientfromidmtools.core.loggingimportSUCCESSos.environ['IDMTOOLS_LOGGING_USER_OUTPUT']='0'ifpassword:user_logger.warning("Password the password via the command line is considered insecure")else:password=getpass("Password:")# make platform object to load info from alias or config but don't loginplatform=Platform(ctx.obj['config_block'],_skip_login=True)try:Client.login(platform.endpoint,StaticCredentialPrompt(comps_url=platform.endpoint,username=username,password=password))user_logger.log(SUCCESS,"Login succeeded")exceptPermissionError:user_logger.error(f"Could not loging to {platform.endpoint}")sys.exit(-1)@comps.command(help="Allows Downloading outputs from the command line")@click.option('--pattern',default=[],multiple=True,help="File patterns")@click.option('--exclude-pattern',default=DEFAULT_EXCLUDES,multiple=True,help="File patterns")@click.option('--experiment',default=[],multiple=True,help="Experiment ids to filter for files to download")@click.option('--simulation',default=[],multiple=True,help="Simulation ids to filter for files to download")@click.option('--work-item',default=[],multiple=True,help="WorkItems ids to filter for files to download")@click.option('--asset-collection',default=[],multiple=True,help="Asset Collection ids to filter for files to download")@click.option('--dry-run/--no-dry-run',default=False,help="Gather a list of files that would be downloaded instead of actually downloading")@click.option('--wait/--no-wait',default=True,help="Wait on item to finish")@click.option('--include-assets/--no-include-assets',default=False,help="Scan common assets of WorkItems and Experiments when filtering")@click.option('--verbose/--no-verbose',default=True,help="Enable verbose output in worker")@click.option('--json/--no-json',default=False,help="Outputs File list as JSON when used with dry run")@click.option('--simulation-prefix-format-str',default=None,help="Simulation Prefix Format str. Defaults to '{simulation.id}'. For no prefix, pass a empty string")@click.option('--work-item-prefix-format-str',default=None,help="WorkItem Prefix Format str. Defaults to ''")@click.option('--name',default=None,help="Name of Download Workitem. If not provided, one will be generated")@click.option('--output-path',default=os.getcwd(),help="Output path to save zip")@click.option('--delete-after-download/--no-delete-after-download',default=True,help="Delete the workitem used to gather files after download")@click.option('--extract-after-download/--no-extract-after-download',default=True,help="Extract zip after download")@click.option('--zip-name',default="output.zip",help="Name of zipfile")@click.pass_contextdefdownload(# noqa D103ctx:click.Context,pattern,exclude_pattern,experiment,simulation,work_item,asset_collection,dry_run,wait,include_assets,verbose,json,simulation_prefix_format_str,work_item_prefix_format_str,name,output_path,delete_after_download,extract_after_download,zip_name):fromidmtools_platform_comps.utils.download.downloadimportDownloadWorkItemifjsonandnotdry_run:user_logger.error("You cannot return JSON without enabling dry-run mode")sys.exit(-1)ifdry_runanddelete_after_download:user_logger.warning("You are using dry-run with delete after download. This will most result in an empty file list since ""the item will be deleted before the output can be fetched.")# ensure no output is enabled when using --jsonifjson:os.environ['IDMTOOLS_LOGGING_USER_OUTPUT']='0'os.environ['IDMTOOLS_DISABLE_PROGRESS_BAR']='1'p:COMPSPlatform=Platform(ctx.obj['config_block'])dl_wi=DownloadWorkItem(output_path=output_path,delete_after_download=delete_after_download,extract_after_download=extract_after_download,zip_name=zip_name)ifname:dl_wi.name=nameifpattern:dl_wi.file_patterns=list(pattern)ifexclude_pattern:dl_wi.exclude_patterns=exclude_patternifisinstance(exclude_pattern,list)elselist(exclude_pattern)dl_wi.related_experiments=list(experiment)dl_wi.related_simulations=list(simulation)dl_wi.related_work_items=list(work_item)dl_wi.related_asset_collections=list(asset_collection)dl_wi.include_assets=include_assetsdl_wi.dry_run=dry_rundl_wi.verbose=verboseifsimulation_prefix_format_strisnotNone:ifsimulation_prefix_format_str.strip()=="":dl_wi.no_simulation_prefix=Trueelse:dl_wi.simulation_prefix_format_str=simulation_prefix_format_strifwork_item_prefix_format_strisnotNone:dl_wi.work_item_prefix_format_str=work_item_prefix_format_strifdl_wi.total_items_watched()==0:user_logger.error("You must specify at least one item to download")dl_wi.run(wait_until_done=False,platform=p)ifnotjsonandnotdelete_after_download:user_logger.info(f"Item can be viewed at {p.get_workitem_link(dl_wi)}")ifwait:dl_wi.wait(wait_on_done_progress=wait)ifdl_wi.succeeded:ifdl_wi.dry_runandnotdelete_after_download:file=p.get_files(dl_wi,['file_list.json'])file=file['file_list.json'].decode('utf-8')ifjson:user_logger.info(file)else:file=json_parser.loads(file)user_logger.info(tabulate.tabulate([x.values()forxinfile],file[0].keys()))else:passelifdl_wi.failed:user_logger.error("Download failed. Check logs in COMPS")ifdl_wi.failed:dl_wi.fetch_error()sys.exit(-1)@comps.command(help="Allows assetizing outputs from the command line")@click.option('--pattern',default=[],multiple=True,help="File patterns")@click.option('--exclude-pattern',default=DEFAULT_EXCLUDES,multiple=True,help="File patterns")@click.option('--experiment',default=[],multiple=True,help="Experiment ids to assetize")@click.option('--simulation',default=[],multiple=True,help="Simulation ids to assetize")@click.option('--work-item',default=[],multiple=True,help="WorkItems ids to assetize")@click.option('--asset-collection',default=[],multiple=True,help="Asset Collection ids to assetize")@click.option('--dry-run/--no-dry-run',default=False,help="Gather a list of files that would be assetized instead of actually assetizing")@click.option('--wait/--no-wait',default=True,help="Wait on item to finish")@click.option('--include-assets/--no-include-assets',default=False,help="Scan common assets of WorkItems and Experiments when filtering")@click.option('--verbose/--no-verbose',default=True,help="Enable verbose output in worker")@click.option('--json/--no-json',default=False,help="Outputs File list as JSON when used with dry run")@click.option('--simulation-prefix-format-str',default=None,help="Simulation Prefix Format str. Defaults to '{simulation.id}'. For no prefix, pass a empty string")@click.option('--work-item-prefix-format-str',default=None,help="WorkItem Prefix Format str. Defaults to ''")@click.option('--tag',default=[],type=(str,str),multiple=True,help="Tags to add to the created asset collection as pairs.")@click.option('--name',default=None,help="Name of AssetizeWorkitem. If not provided, one will be generated")@click.option('--id-file/--no-id-file',default=False,help="Enable or disable writing out an id file")@click.option('--id-filename',default=None,help="Name of ID file to save build as. Required when id file is enabled")@click.pass_contextdefassetize_outputs(# noqa D103ctx:click.Context,pattern,exclude_pattern,experiment,simulation,work_item,asset_collection,dry_run,wait,include_assets,verbose,json,simulation_prefix_format_str,work_item_prefix_format_str,tag,name,id_file,id_filename):ifid_fileandid_filenameisNone:user_logger.error("--id-filename is required when filename is not provided")sys.exit(-1)ifjson:os.environ['IDMTOOLS_LOGGING_USER_OUTPUT']='0'os.environ['IDMTOOLS_DISABLE_PROGRESS_BAR']='1'p:COMPSPlatform=Platform(ctx.obj['config_block'])ao=AssetizeOutput()ifname:ao.name=nameifpattern:ao.file_patterns=list(pattern)ifexclude_pattern:ao.exclude_patterns=exclude_patternifisinstance(exclude_pattern,list)elselist(exclude_pattern)ao.related_experiments=list(experiment)ao.related_simulations=list(simulation)ao.related_work_items=list(work_item)ao.related_asset_collections=list(asset_collection)ao.include_assets=include_assetsao.dry_run=dry_runao.verbose=verboseifsimulation_prefix_format_strisnotNone:ifsimulation_prefix_format_str.strip()=="":ao.no_simulation_prefix=Trueelse:ao.simulation_prefix_format_str=simulation_prefix_format_strifwork_item_prefix_format_strisnotNone:ao.work_item_prefix_format_str=work_item_prefix_format_striftag:forname,valueintag:ao.asset_tags[name]=valueifao.total_items_watched()==0:user_logger.error("You must specify at least one item to assetize")ao.run(wait_until_done=False,platform=p)ifnotjson:user_logger.info(f"Item can be viewed at {p.get_workitem_link(ao)}")ifwait:ao.wait(wait_on_done_progress=wait)ifao.succeeded:ifao.dry_run:file=p.get_files(ao,['file_list.json'])file=file['file_list.json'].decode('utf-8')ifjson:user_logger.info(file)else:file=json_parser.loads(file)user_logger.info(tabulate.tabulate([x.values()forxinfile],file[0].keys()))else:ifid_file:ao.asset_collection.to_id_file(id_filename)ifjson:result=[i.short_remote_path()foriinao.asset_collection.assets]user_logger.info(je.dumps(result))else:user_logger.info(f"Created {ao.asset_collection.id}")user_logger.info(f"It can be viewed at {p.get_asset_collection_link(ao.asset_collection)}")user_logger.info("Items in Asset Collection")user_logger.info("-------------------------")forassetinao.asset_collection:user_logger.info(asset.short_remote_path())elifao.failed:user_logger.error("Assetized failed. Check logs in COMPS")ifao.failed:ao.fetch_error()sys.exit(-1)@comps.command(help="""\b Create ac from requirement file Args: asset_tag: tag to be added to ac pkg: package name (along with version) wheel: package wheel file """)@click.argument('requirement',type=click.Path(exists=True),required=False)@click.option('--asset_tag',multiple=True,help="Tag to be added to AC. Format: 'key:value'")@click.option('--pkg',multiple=True,help="Package for override. Format: 'key==value'")@click.option('--wheel',multiple=True,help="Local wheel file")@click.pass_contextdefreq2ac(ctx:click.Context,requirement:str=None,asset_tag:Optional[List[str]]=None,# noqa D103pkg:Optional[List[str]]=None,wheel:Optional[List[str]]=None):fromidmtools_platform_comps.utils.python_requirements_ac.requirements_to_asset_collectionimportRequirementsToAssetCollectionpkg_list=list(pkg)wheel_list=[os.path.abspath(w)forwinwheel]tags=dict()fortinasset_tag:parts=t.split(':')tags[parts[0]]=parts[1]p:COMPSPlatform=Platform(ctx.obj['config_block'])pl=RequirementsToAssetCollection(p,requirements_path=requirement,pkg_list=pkg_list,local_wheels=wheel_list,asset_tags=tags)ac_id=pl.run()print(ac_id)@comps.command(help="""\b Check ac existing based on requirement file Args: pkg: package name (along with version) wheel: package wheel file """)@click.argument('requirement',type=click.Path(exists=True),required=False)@click.option('--pkg',multiple=True,help="Package used for override. Format: say, 'key==value'")@click.option('--wheel',multiple=True,help="Local wheel file")@click.pass_contextdefac_exist(ctx:click.Context,requirement:str=None,pkg:Optional[List[str]]=None,wheel:Optional[List[str]]=None):# noqa D103# TODO rename this and move to a subcommand for all the requirements functionsfromidmtools_platform_comps.utils.python_requirements_ac.requirements_to_asset_collectionimportRequirementsToAssetCollectionpkg_list=list(pkg)wheel_list=[os.path.abspath(w)forwinwheel]p:COMPSPlatform=Platform(ctx.obj['config_block'])pl=RequirementsToAssetCollection(p,requirements_path=requirement,pkg_list=pkg_list,local_wheels=wheel_list)# Check if ac with md5 existsac=pl.retrieve_ac_by_tag()ifac:print("AC exist: ",ac.id)else:print("AC doesn't exist")@comps.group(help="Singularity commands")defsingularity():# noqa D103pass@singularity.command(help="Build Singularity Image")@click.option('--common-input',default=[],multiple=True,help="Files")@click.option('--common-input-glob',default=[],multiple=True,help="File patterns")@click.option('--transient-input',default=[],multiple=True,help="Transient Files (Paths)")@click.option('--transient-input-glob',default=[],multiple=True,help="Transient Files Glob Patterns")@click.argument('definition_file')@click.option('--wait/--no-wait',default=True,help="Wait on item to finish")@click.option('--tag',default=[],type=(str,str),multiple=True,help="Extra Tags as Value Pairs for the Resulting AC")@click.option('--workitem-tag',default=[],type=(str,str),multiple=True,help="Extra Tags as Value Pairs for the WorkItem")@click.option('--name',default=None,help="Name of WorkItem. If not provided, one will be generated")@click.option('--force/--no-force',default=False,help="Force build, ignoring build context")@click.option('--image-name',default=None,help="Name of resulting image")@click.option('--id-file/--no-id-file',default=True,help="Enable or disable writing out an ID file that points to the created asset collection")@click.option('--id-filename',default=None,help="Name of ID file to save build as. If not specified, and id-file is enabled, a name is calculated")@click.option('--id-workitem/--no-id-workitem',default=True,help="Enable or disable writing out an id file for the workitem")@click.option('--id-workitem-failed/--no-id-workitem-failed',default=False,help="Write id of the workitem even if it failed. You need to enable --id-workitem for this is be active")@click.option('--id-workitem-filename',default=None,help="Name of ID file to save workitem to. You need to enable --id-workitem for this is be active")@click.pass_contextdefbuild(ctx:click.Context,common_input,common_input_glob,transient_input,transient_input_glob,# noqa D103definition_file,wait,tag,workitem_tag,name,force,image_name:str,id_file:str,id_filename:str,id_workitem:bool,id_workitem_failed:bool,id_workitem_filename:str):p:COMPSPlatform=Platform(ctx.obj['config_block'])sb=SingularityBuildWorkItem(definition_file=definition_file,name=name,force=force,image_name=image_name)iftag:forname,valueintag:sb.image_tags[name]=valueifworkitem_tag:forname,valueintag:sb.tags[name]=value# Add inputs from filesforassets,inputsin[(sb.assets,common_input),(sb.transient_assets,transient_input)]:forfileininputs:add_item(assets,file)# And then from glob patternsforassets,patternsin[(sb.assets,common_input_glob),(sb.transient_assets,transient_input_glob)]:forpatterninpatterns:forfileinglob.glob(pattern):add_item(assets,file)sb.run(wait_until_done=wait,platform=p)ifsb.succeededandid_file:ifid_filenameisNone:id_filename=sb.get_id_filename()user_logger.info(f"Saving the Asset collection ID that contains the image to {id_filename}")sb.asset_collection.to_id_file(id_filename,save_platform=True)ifid_workitem:# TODO when we should use platform id but that need to be updated through the code baseifsb.succeededandsb._uidisNone:user_logger.warning("Cannot save workitem id because an existing container was found with the same inputs. You can force run using --force, but it is recommended to use the container used.")elifid_workitem_failedorsb.succeeded:ifid_workitem_filenameisNone:id_workitem_filename=sb.get_id_filename(prefix="builder.")user_logger.info(f"Saving the Builder Workitem ID that contains the image to {id_workitem_filename}")sb.to_id_file(id_workitem_filename,save_platform=True)sys.exit(0ifsb.succeededelse-1)@singularity.command(help="Pull Singularity Image")@click.argument('image_url')@click.option('--wait/--no-wait',default=True,help="Wait on item to finish")@click.option('--tag',default=[],type=(str,str),multiple=True,help="Extra Tags as Value Pairs for the Resulting AC")@click.option('--workitem-tag',default=[],type=(str,str),multiple=True,help="Extra Tags as Value Pairs for the WorkItem")@click.option('--name',default=None,help="Name of WorkItem. If not provided, one will be generated")@click.option('--force/--no-force',default=False,help="Force build, ignoring build context")@click.option('--image-name',default=None,help="Name of resulting image")@click.option('--id-file/--no-id-file',default=True,help="Enable or disable writing out an id file")@click.option('--id-filename',default=None,help="Name of ID file to save build as. If not specified, and id-file is enabled, a name is calculated")@click.option('--id-workitem/--no-id-workitem',default=True,help="Enable or disable writing out an id file for the workitem")@click.option('--id-workitem-failed/--no-id-workitem-failed',default=False,help="Write id of the workitem even if it failed. You need to enable --id-workitem for this is be active")@click.option('--id-workitem-filename',default=None,help="Name of ID file to save workitem to. You need to enable --id-workitem for this is be active")@click.pass_contextdefpull(ctx:click.Context,image_url,wait,tag,workitem_tag,name,force,image_name:str,id_file:str,# noqa D103id_filename:str,id_workitem:bool,id_workitem_failed:bool,id_workitem_filename:str):p:COMPSPlatform=Platform(ctx.obj['config_block'])sb=SingularityBuildWorkItem(image_url=image_url,force=force,image_name=image_name)sb.name=f"Pulling {image_url}"ifnameisNoneelsenameiftag:forname,valueintag:sb.image_tags[name]=valueifworkitem_tag:forname,valueintag:sb.tags[name]=valuesb.run(wait_until_done=wait,platform=p)ifsb.succeededandid_file:ifid_filenameisNone:id_filename=sb.get_id_filename()user_logger.info(f"Saving ID to {id_filename}")sb.asset_collection.to_id_file(id_filename,save_platform=True)ifid_workitemandsb.succeededandsb._uidisNone:user_logger.warning("Cannot save workitem id because an existing container was found with the same inputs. You can force run using --force, but it is recommended to use the container used.")elifid_workitem_failedorsb.succeeded:ifid_workitem_filenameisNone:id_workitem_filename=sb.get_id_filename(prefix="builder.")user_logger.info(f"Saving the Builder Workitem ID that contains the image to {id_workitem_filename}")sb.to_id_file(id_workitem_filename,save_platform=True)sys.exit(0ifsb.succeededelse-1)exceptImportErrorase:logger.warning(f"COMPS CLI not enabled because a dependency is missing. Most likely it is either click or idmtools cli {e.args}")