Small helpers used across the toolbox: sequence file handling, MMseqs2 command
construction, SRA metadata lookups, JSON loading, and Slurm job-script generation.
fromadtoolboximportutils
Most of the MMseqs2 helpers build a shell command and either return it or run it, which is
what lets the same code path work locally, inside a container, or as a submitted Slurm
job.
deffasta_to_dict(fasta:str)->dict:""" This function converts a fasta file to a dictionary Args: fasta: the affress fasta file as a string Returns: A dictionary with the fasta labels as keys and the fasta sequences as values """Dictionary={}withopen(fasta,'r')asf:forlineinf:ifline.startswith('>'):label=line.lstrip(">").strip()Dictionary[label]=""else:Dictionary[label]+=line.strip()returnDictionary
defdict_to_fasta(dictionary:dict,fasta:str)->None:""" This function converts a dictionary to a fasta file Args: dictionary: the dictionary to convert fasta: the address of the fasta file to save Returns: None """withopen(fasta,'w')asf:forlabel,seqindictionary.items():f.write(f">{label}\n{seq}\n")
This function extracts a zipped file to a directory
Args:
container: The container to run the script in. If None, the script is run locally
Returns:
None
defextract_zipped_file(input_file:str,container:str="None",configs=configs.Utils())->str:""" This function extracts a zipped file to a directory Args: container: The container to run the script in. If None, the script is run locally Returns: None """ifcontainer=="None":script=f"gzip -d {input_file}"elifcontainer=="singularity":script=f"singularity exec -B {input_file}:{input_file}{configs.adtoolbox_singularity} gzip -d {input_file}"elifcontainer=="docker":script=f"docker run -v {input_file}:{input_file}{configs.adtoolbox_docker} gzip -d {input_file}"returnscript,
This function takes a csv file directory. The CSV file must include three columns:
1- genome_id: Identifier for the genome, preferrably; NCBI ID
2- NCBI_Name: NCBI taxonomy name for the genome: Does not need to be in a specific format
3- Genome_Dir: Absolute path to the fasta files: NOT .gz
defmake_json_from_genomes(input_dir:str,output_dir:str)->dict:""" This function takes a csv file directory. The CSV file must include three columns: 1- genome_id: Identifier for the genome, preferrably; NCBI ID 2- NCBI_Name: NCBI taxonomy name for the genome: Does not need to be in a specific format 3- Genome_Dir: Absolute path to the fasta files: NOT .gz """genomes_json={}genomes_table=pl.read_csv(input_dir,separator=",",infer_schema_length=0)forrowingenomes_table.to_dicts():gi=row["genome_id"]genomes_json[gi]={}genomes_json[gi]["NCBI_Name"]=row["NCBI_Name"]genomes_json[gi]["Genome_Dir"]=row["Genome_Dir"]withopen(output_dir,"w")asfp:json.dump(genomes_json,fp)return
defseq_generator(self)->None:ifself.type=='Protein':return''.join([random.choice(self.aa_list)foriinrange(self.length)])elifself.type=='DNA':return''.join([random.choice(self.n_list)foriinrange(self.length)])else:print('Type not recognized')
This function converts any database in fasta format for mmseqs2.
Specifically, this function can be used for the protein database and is highly recomended for
shotgun metagenomics samples.
defcreate_mmseqs_database(fasta_db:str,mmseqs_db:str,container:str="None",save:Union[str,None]=None,run:bool=True,config=configs.Utils())->str:"""This function converts any database in fasta format for mmseqs2. Specifically, this function can be used for the protein database and is highly recomended for shotgun metagenomics samples."""fasta_db=os.path.abspath(fasta_db)mmseqs_db=os.path.abspath(mmseqs_db)bashscript=f"mmseqs createdb {fasta_db}{mmseqs_db}"db_name_path=pathlib.Path(mmseqs_db)fasta_name_path=pathlib.Path(fasta_db)ifcontainer=="None":passelifcontainerin{"singularity","apptainer"}:runtime="apptainer"ifcontainer=="apptainer"else"singularity"bashscript=f"{runtime} exec --bind {fasta_db}:{fasta_db}{config.adtoolbox_singularity}{bashscript}"elifcontainer=="docker":bashscript=f"docker run -v {fasta_db}:{fasta_db} -v {db_name_path.parent}:{db_name_path.parent}{config.adtoolbox_docker}{bashscript}"else:raiseValueError("Invalid container type. Please choose between None, singularity, apptainer and docker.")ifsave:withopen(save,'w')asf:f.write(bashscript)ifrun:subprocess.run(bashscript,shell=True)returnbashscript
This function indexes any database in fasta format for mmseqs2.
Specifically, this function can be used for the protein database and is highly recomended for
shotgun metagenomics samples.
defindex_mmseqs_db(mmseqs_db:str,container:str="None",save:Union[str,None]=None,run:bool=True,config=configs.Utils)->str:"""This function indexes any database in fasta format for mmseqs2. Specifically, this function can be used for the protein database and is highly recomended for shotgun metagenomics samples."""mmseqs_db=os.path.abspath(mmseqs_db)bashscript=f"mmseqs createindex {mmseqs_db}{mmseqs_db}"db_name_path=pathlib.Path(mmseqs_db)ifcontainer!="None":passelifcontainerin{"singularity","apptainer"}:runtime="apptainer"ifcontainer=="apptainer"else"singularity"bashscript=f"{runtime} exec --bind {str(pathlib.Path(mmseqs_db).parent)}:{str(pathlib.Path(mmseqs_db).parent)},{db_name_path.parent}:{db_name_path.parent}{config.adtoolbox_singularity}{bashscript}"elifcontainer=="docker":bashscript=f"docker run -v {mmseqs_db}:{mmseqs_db} -v {db_name_path.parent}:{db_name_path.parent}{config.adtoolbox_docker}{bashscript}"else:raiseValueError("Invalid container type. Please choose between None, singularity, apptainer and docker.")ifsave:withopen(save,'w')asf:f.write(bashscript)ifrun:subprocess.run(bashscript,shell=True)returnbashscript
This function searches a query database against a target database using mmseqs2.
Specifically, this function can be used for the protein database and is highly recomended for
shotgun metagenomics samples.
defmmseqs_search(query_db:str,target_db:str,results_db:str,container:str="None",save:Union[str,None]=None,run:bool=True,config=configs.Utils())->str:"""This function searches a query database against a target database using mmseqs2. Specifically, this function can be used for the protein database and is highly recomended for shotgun metagenomics samples."""query_db=os.path.abspath(query_db)target_db=os.path.abspath(target_db)results_db=os.path.abspath(results_db)bashscript=f"mmseqs search {query_db}{target_db}{results_db}{str(pathlib.Path(results_db).parent)}/tmp"ifcontainer=="None":passelifcontainerin{"singularity","apptainer"}:runtime="apptainer"ifcontainer=="apptainer"else"singularity"query_db=str(pathlib.Path(query_db).parent)target_db=str(pathlib.Path(target_db).parent)results_db=str(pathlib.Path(results_db).parent)path_mount=list(set([query_db,target_db,results_db]))path_mount=",".join([f"{i}:{i}"foriinpath_mount])bashscript=f"{runtime} exec --bind {path_mount}{config.adtoolbox_singularity}{bashscript}"elifcontainer=="docker":bashscript=f"docker run -v {query_db}:{query_db} -v {str(pathlib.Path(target_db).parent)}:{str(pathlib.Path(target_db).parent)} -v {results_db}:{results_db}{config.adtoolbox_docker}{bashscript}"else:raiseValueError("Invalid container type. Please choose between None, singularity, apptainer and docker.")ifsave:withopen(save,'w')asf:f.write(bashscript)ifrun:subprocess.run(bashscript,shell=True)returnbashscript
This function converts the results of mmseqs search to a tsv file.
Specifically, this function can be used for the protein database and is highly recomended for
shotgun metagenomics samples.
defmmseqs_result_db_to_tsv(query_db:str,target_db:str,results_db:str,tsv_file:str,container:str="None",save:Union[str,None]=None,run:bool=True,config=configs.Utils())->str:"""This function converts the results of mmseqs search to a tsv file. Specifically, this function can be used for the protein database and is highly recomended for shotgun metagenomics samples."""query_db=os.path.abspath(query_db)target_db=os.path.abspath(target_db)results_db=os.path.abspath(results_db)tsv_file=os.path.abspath(tsv_file)bashscript=f"mmseqs convertalis {query_db}{target_db}{results_db}{tsv_file} --format-mode 4"ifcontainer=="None":passelifcontainerin{"singularity","apptainer"}:runtime="apptainer"ifcontainer=="apptainer"else"singularity"query_db=str(pathlib.Path(query_db).parent)target_db=str(pathlib.Path(target_db).parent)results_db=str(pathlib.Path(results_db).parent)tsv_file=str(pathlib.Path(tsv_file).parent)path_mount=list(set([query_db,target_db,results_db,tsv_file]))path_mount=",".join([f"{i}:{i}"foriinpath_mount])bashscript=f"{runtime} exec --bind {path_mount}{config.adtoolbox_singularity}{bashscript}"elifcontainer=="docker":bashscript=f"docker run -v {query_db}:{query_db} -v {str(pathlib.Path(target_db).parent)}:{str(pathlib.Path(target_db).parent)} -v {results_db}:{results_db}{config.adtoolbox_docker}{bashscript}"else:raiseValueError("Invalid container type. Please choose between None, singularity, apptainer and docker.")ifsave:withopen(save,'w')asf:f.write(bashscript)ifrun:subprocess.run(bashscript,shell=True)returnbashscript
defload_json_entry(json_file:str,entry_key:str,required_keys:Iterable[str]|None=None)->dict:"""Load one top-level entry from a keyed JSON file."""withopen(json_file,"r")asf:entries=json.load(f)ifentry_keynotinentries:available=", ".join(sorted(entries))raiseKeyError(f"{entry_key!r} was not found in {json_file}. Available keys: {available}")entry=entries[entry_key]ifnotisinstance(entry,dict):raiseTypeError(f"{entry_key!r} in {json_file} must be a JSON object")ifrequired_keys:missing=sorted(set(required_keys)-set(entry))ifmissing:raiseKeyError(f"{entry_key!r} in {json_file} is missing required keys: {', '.join(missing)}")returnentry
defload_model_json(json_file:str,model_key:str)->dict:"""Load one ADM model from a JSON file containing all models."""required_keys=["model_parameters","base_parameters","initial_conditions","inlet_conditions","reactions","species",]returnload_json_entry(json_file,model_key,required_keys=required_keys)
This is a function that wraps a bash script in a slurm script.
All resource allocation configuration is obtained from config argument
Args:
command: bash command to run in python string format
run: if True, the slurm script is executed
jobname: the name of the job to be submitted
save: if True, the slurm script is saved to the path specified in config
config: Configs.Utils object that detemines the template form and slurm options
defwrap_for_slurm(command:str,jobname:str,run:bool,save:None|str,config:configs.Utils)->str:""" This is a function that wraps a bash script in a slurm script. All resource allocation configuration is obtained from config argument Args: command: bash command to run in python string format run: if True, the slurm script is executed jobname: the name of the job to be submitted save: if True, the slurm script is saved to the path specified in config config: Configs.Utils object that detemines the template form and slurm options Returns: The slurm script as a string """withopen(config.slurm_template,'r')asf:slurm_template=f.read()command=command.replace("#!/bin/bash","")slurm_script=slurm_template.replace("<command>",command)slurm_script=slurm_script.replace("<sample_name>",jobname)slurm_script=slurm_script.replace("<wall_time>",config.slurm_wall_time)slurm_script=slurm_script.replace("<executer>",config.slurm_executer)slurm_script=slurm_script.replace("<job_name>",jobname)slurm_script=slurm_script.replace("<sample_outlog>",jobname)slurm_script=slurm_script.replace("<memory>",config.slurm_memory)slurm_script=slurm_script.replace("<cpus>",config.slurm_cpus)ifsave:withopen(save,'w')asf:f.write(slurm_script)ifrun:subprocess.run([f"sbatch {save}"],shell=True)returnslurm_script
defgenerate_batch_script(generator_function:callable,number_of_batches:int,input_series:list[list],input_var:list[str],container:str="None",save:Union[str,None]=None,run:bool=False,header:str="#!/bin/bash\n",**kwargs)->tuple:""" This is a general function that generates an iterable of bash scripts for running a function that creates a bash script on an iterable of inputs. """batch_size=len(list(zip(*input_series)))//number_of_batchesbatches=[]foriinrange(len(input_series)):batches.append([input_series[i][j:j+batch_size]forjinrange(0,len(input_series[i]),batch_size)])scripts=[]forinputsinzip(*batches):script=headerfori,iteminenumerate(zip(*inputs)):forind,inpinenumerate(input_var):kwargs.update(dict([(inp,item[ind])]))script_=generator_function(container=container,**kwargs)[0]script=script+script_+"\n"ifsave:ifnotos.path.exists(save):os.makedirs(save)withopen(os.path.join(save,f"{item[0]}.sh"),'w')asf:f.write(script)scripts.append(script)ifrun:forscriptinscripts:subprocess.run([script],shell=True)returntuple(scripts)
This function returns a dictionary that contains the sample metadata from the SRA database.
The function uses the bio command line tool to get the sample metadata. The function also saves the sample metadata if save=True.
if the download somehow fails, the function will return a dictionary with default values like -1 and "Unknown".
Requires
project_accessionConfigs.Metagenomics
Satisfies
sample_metadata
Parameters:
Name
Type
Description
Default
accession
str
The accession number of the SRA project or run
required
save
bool
If True, the sample metadata will be saved in the SRA work directory
defget_sample_metadata_from_accession(accession:str,save:Union[None,str]=None)->dict:"""This function returns a dictionary that contains the sample metadata from the SRA database. The function uses the bio command line tool to get the sample metadata. The function also saves the sample metadata if save=True. if the download somehow fails, the function will return a dictionary with default values like -1 and "Unknown". Requires: <R>project_accession</R> <R>Configs.Metagenomics</R> Satisfies: <S>sample_metadata</S> Args: accession (str): The accession number of the SRA project or run save (bool): If True, the sample metadata will be saved in the SRA work directory Returns: sample_metadata (dict): A dictionary that contains the sample metadata """metadata={}ifos.name=="nt":res=subprocess.run(["bio","search",accession,"--all"],shell=True,capture_output=True)else:res=subprocess.run([f"""bio search {accession} --all"""],shell=True,capture_output=True)try:res=json.loads(res.stdout.decode("utf-8"))[0]metadata["host"]=res.setdefault("host","Unknown")metadata["library_strategy"]=res["library_strategy"].lower()ifres["library_strategy"]else"Unknown"metadata["library_layout"]=res.setdefault("library_layout","Unknown").lower()metadata["base_count"]=float(res.setdefault("base_count",-1))metadata["read_count"]=float(res.setdefault("read_count",-1))avg_read_len=int(metadata["base_count"]/metadata["read_count"])metadata["read_length"]=avg_read_lenifmetadata["library_layout"]=="single"elseavg_read_len/2metadata["sample_accession"]=accessionmetadata["study_accession"]=res["study_accession"]except:metadata["host"]="Unknown"metadata["library_strategy"]="Unknown"metadata["library_layout"]="Unknown"metadata["base_count"]=-1metadata["read_count"]=-1metadata["read_length"]=-1metadata["sample_accession"]=accessionmetadata["study_accession"]="Unknown"ifsave:withopen(save,"w")asf:json.dump(metadata,f)returnmetadata