Skip to content

Commit d99dcbc

Browse files
authored
Merge pull request #1159 from fls-bioinformatics-core/abstract-updating-functionality
Consolidate path updating functionality in 'archive', 'clone' and 'update' commands
2 parents 8042c64 + 7825a35 commit d99dcbc

5 files changed

Lines changed: 784 additions & 208 deletions

File tree

auto_process_ngs/auto_processor.py

Lines changed: 170 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,9 @@
1010

1111
import sys
1212
import os
13-
import subprocess
1413
import logging
1514
import shutil
16-
import uuid
1715
import time
18-
import ast
19-
import gzip
2016
import atexit
2117
import bcftbx.IlluminaData as IlluminaData
2218
import bcftbx.utils as bcf_utils
@@ -25,6 +21,7 @@
2521
from .analysis import run_reference_id
2622
from .metadata import AnalysisDirParameters
2723
from .metadata import AnalysisDirMetadata
24+
from .metadata import AnalysisProjectInfo
2825
from .metadata import ProjectMetadataFile
2926
from .utils import edit_file
3027
from .utils import get_numbered_subdir
@@ -140,6 +137,64 @@ def create_directory(self,dirn):
140137
print("Making %s" % dir_path)
141138
bcf_utils.mkdir(dir_path)
142139

140+
def update_paths(self, base_path=None, new_path=None):
141+
"""
142+
Update the paths stored in the analysis directory
143+
144+
Checks the paths stored in the analysis directory
145+
metadata and parameter files, and updates them if
146+
they're inconsistent with the current location.
147+
148+
By default the original 'base' path is taken from
149+
the path stored in the analysis directory parameters,
150+
and the new 'base' path is assumed to be the current
151+
path for the analysis directory (these settings
152+
are sensible if the directory has been relocated or
153+
copied).
154+
155+
Arguments:
156+
base_path (str): current 'base' directory path
157+
(defaults to path stored in analysis directory
158+
parameters)
159+
new_path (str): new 'base' directory path
160+
(defaults to the current path of the analysis
161+
directory)
162+
"""
163+
# Update paths in the top-level parameter file
164+
# (if analysis dir has been moved or copied)
165+
if base_path is None:
166+
# Use stored path as original base path
167+
base_path = self.params.analysis_dir
168+
if new_path is None:
169+
# Use current path as new base path
170+
new_path = self.analysis_dir
171+
if base_path != new_path:
172+
print("Updating analysis directory paths in parameter file")
173+
print(f"-- old base path: {base_path}")
174+
print(f"-- new base path: {new_path}")
175+
for p in ('analysis_dir',
176+
'primary_data_dir',
177+
'sample_sheet'):
178+
if not self.params[p]:
179+
continue
180+
self.params[p] = os.path.normpath(
181+
os.path.join(new_path,
182+
os.path.relpath(self.params[p], base_path)))
183+
print(f"...updated '{p}' (set to '{self.params[p]}'")
184+
# Update paths in QC metadata in projects
185+
for project in self.get_analysis_projects_from_dirs():
186+
# Iterate through all project directories
187+
for qc_dir in project.qc_dirs:
188+
qc_info = project.qc_info(qc_dir)
189+
qc_info['fastq_dir'] = os.path.normpath(
190+
os.path.join(new_path,
191+
os.path.relpath(qc_info.fastq_dir,
192+
base_path)))
193+
print(f"...updated QC info for {project.name}/{qc_dir}")
194+
qc_info.save()
195+
# Save the updated parameter data
196+
self.save_parameters(force=True)
197+
143198
def load_parameters(self,allow_save=True):
144199
"""
145200
Load parameter values from file
@@ -477,6 +532,58 @@ def update_project_metadata_file(self,unaligned_dir=None,
477532
print("Updated project metadata file '%s'" %
478533
self.params.project_metadata)
479534

535+
def sync_project_metadata_file(self):
536+
"""
537+
Synchronise 'projects.info' file with directory contents
538+
"""
539+
# Load information from 'projects.info'
540+
project_metadata = self.load_project_metadata()
541+
# Comment out projects which don't exist on filesystem
542+
save_required = False
543+
for line in project_metadata:
544+
# Iterate through the named projects
545+
name = line['Project']
546+
if name.startswith('#'):
547+
# Commented out, ignore
548+
continue
549+
# Look for a matching project directory
550+
project_dir = os.path.join(self.analysis_dir, name)
551+
if not os.path.exists(project_dir):
552+
print(f"Commenting out missing project '{name}'")
553+
line['Project'] = f"#{name}"
554+
save_required = True
555+
if save_required:
556+
project_metadata.save()
557+
# Add any project directories without entries
558+
save_required = False
559+
projects = [line['Project'] for line in project_metadata]
560+
for project in self.get_analysis_projects_from_dirs():
561+
if project.name.endswith(".bak") \
562+
or project.name.endswith(".orig") \
563+
or project.name.endswith(".tmp"):
564+
# Skip directories with extensions indicating they
565+
# should be ignored
566+
print(f"Not adding entry for unlisted project '{project.name}'")
567+
continue
568+
elif project.name == "undetermined":
569+
# Skip undetermined
570+
continue
571+
elif project.name not in projects and f"#{project.name}" not in projects:
572+
# Add new entry
573+
print(f"Adding entry for unlisted project '{project.name}'")
574+
project_metadata.add_project(project.name,
575+
[s.name for s in project.samples],
576+
user=project.info.user,
577+
PI=project.info.PI,
578+
organism=project.info.organism,
579+
library_type=project.info.library_type,
580+
sc_platform=project.info.single_cell_platform,
581+
comments=project.info.comments)
582+
save_required = True
583+
# Save the updated project metadata if required
584+
if save_required:
585+
project_metadata.save()
586+
480587
def detect_unaligned_dir(self):
481588
# Attempt to detect an existing 'bcl2fastq' or 'Unaligned' directory
482589
# containing data from bcl2fastq
@@ -920,6 +1027,65 @@ def get_analysis_projects_from_dirs(self,pattern=None,strict=False):
9201027
projects.append(test_project)
9211028
return projects
9221029

1030+
def sync_project_metadata(self):
1031+
"""
1032+
Update metadata stored in project dirs with 'projects.info'
1033+
"""
1034+
# Load information from 'projects.info'
1035+
project_metadata = self.load_project_metadata()
1036+
save_required = False
1037+
for line in project_metadata:
1038+
# Iterate through the named projects
1039+
name = line['Project']
1040+
if name.startswith('#'):
1041+
# Commented out, ignore
1042+
continue
1043+
# Look for a matching project directory
1044+
project_dir = os.path.join(self.analysis_dir, name)
1045+
if os.path.exists(project_dir):
1046+
project = AnalysisProject(project_dir)
1047+
print(f"Checking metadata for project '{name}'")
1048+
# Synchronise metadata in projects with projects.info
1049+
metadata_items = dict(
1050+
name=name,
1051+
user=line['User'],
1052+
PI=line['PI'],
1053+
organism=line['Organism'],
1054+
library_type=line['Library'],
1055+
single_cell_platform=line['SC_Platform'],
1056+
comments=line['Comments'],
1057+
samples=project.sample_summary()
1058+
)
1059+
# Only update items where values differ
1060+
project_metadata_updated = False
1061+
for item in metadata_items:
1062+
new_value = (metadata_items[item]
1063+
if metadata_items[item] != '.' else None)
1064+
if project.info[item] != new_value:
1065+
print("...updating '%s' => %r" % (item, new_value))
1066+
project.info[item] = new_value
1067+
project_metadata_updated = True
1068+
# Check paired-end info
1069+
project_info = AnalysisProjectInfo(project.info_file)
1070+
if project_info.paired_end != project.info.paired_end:
1071+
print("...updating paired-end info")
1072+
project_metadata_updated = True
1073+
# Save the updated project metadata if required
1074+
if project_metadata_updated:
1075+
print(f"...saving project metadata for {project.name}")
1076+
project.info.save()
1077+
# Update list of sample names in projects.info
1078+
sample_list = ','.join(sort_sample_names(
1079+
[s.name for s in project.samples]))
1080+
if line['Samples'] != sample_list:
1081+
print("...updating sample list in projects.info")
1082+
line['Samples'] = sample_list
1083+
save_required = True
1084+
# Save master project metadata
1085+
if save_required:
1086+
print("Saving projects.info")
1087+
project_metadata.save()
1088+
9231089
def undetermined(self):
9241090
# Return analysis project directory for undetermined indices
9251091
# or None if not found

auto_process_ngs/commands/archive_cmd.py

Lines changed: 21 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010
#######################################################################
1111

1212
import os
13-
import time
1413
import logging
1514
from ..analysis import AnalysisDir
15+
from ..auto_processor import AutoProcess
1616
from ..metadata import AnalysisDirMetadata
17-
from ..metadata import AnalysisDirParameters
1817
from ..command import Command
1918
from ..commands.report_cmd import report_concise
2019
from .. import apps
@@ -412,37 +411,16 @@ def archive(ap,archive_dir=None,platform=None,year=None,
412411
sched.stop()
413412
# Perform final archiving operations
414413
if final:
415-
# Update the final stored Fastq paths and metadata
416-
# FIXME this is essentially duplicating functionality
417-
# FIXME in the 'update' command
418-
# FIXME (Also probably shouldn't update metadata for
419-
# FIXME 'dry_run' mode?)
420-
print("Updating stored paths and metadata")
414+
# Paths to staging and final directories
421415
staged_analysis_dir = os.path.join(archive_dir,staging)
422416
archived_analysis_dir = os.path.join(archive_dir,final_dest)
423-
parameter_file = os.path.join(staged_analysis_dir,
424-
"auto_process.info")
425-
if os.path.exists(parameter_file):
426-
params = AnalysisDirParameters()
427-
params.load(parameter_file,strict=False)
428-
base_path = params.analysis_dir
429-
print("Stored base path: %s" % base_path)
430-
for p in ('analysis_dir',
431-
'primary_data_dir',
432-
'sample_sheet'):
433-
if not params[p]:
434-
continue
435-
params[p] = os.path.normpath(
436-
os.path.join(archived_analysis_dir,
437-
os.path.relpath(params[p],
438-
base_path)))
439-
print("...updated '%s' (set to '%s')" % (p,params[p]))
440-
params.save()
417+
# Update the final stored Fastq paths and metadata
418+
if not dry_run:
419+
AutoProcess(staged_analysis_dir).update_paths(
420+
base_path=ap.params.analysis_dir,
421+
new_path=archived_analysis_dir)
441422
else:
442-
base_path = ap.analysis_dir
443-
logger.warning("Unable to get old base path from parameters")
444-
logger.warning("Using base path: %s (may be incorrect)" %
445-
base_path)
423+
print("Updating paths skipped for dry run")
446424
# Run ID and reference
447425
metadata_file = os.path.join(staged_analysis_dir,
448426
"metadata.info")
@@ -456,7 +434,10 @@ def archive(ap,archive_dir=None,platform=None,year=None,
456434
metadata['run_reference_id'] = ap.run_reference_id
457435
print("...storing run reference ID ('%s')" %
458436
metadata.run_reference_id)
459-
metadata.save()
437+
if not dry_run:
438+
metadata.save()
439+
else:
440+
print("Run ID/reference ID not updated for dry run")
460441
# Project metadata and QC info
461442
analysis_dir = AnalysisDir(staged_analysis_dir)
462443
# FIXME AnalysisDir.get_projects method might not get all
@@ -489,28 +470,16 @@ def archive(ap,archive_dir=None,platform=None,year=None,
489470
project.name)
490471
# Save the updated information
491472
if project_info_updated:
492-
project.info.save()
493-
# QC metadata
494-
# FIXME should do all QC dirs (not just the primary one)
495-
qc_info = project.qc_info(project.qc_dir)
496-
if qc_info.fastq_dir:
497-
print("Project '%s': updating stored Fastq directory for QC" %
498-
project.name)
499-
# FIXME could we just set it to the current Fastq path?
500-
new_fastq_dir = os.path.normpath(
501-
os.path.join(archived_analysis_dir,
502-
os.path.relpath(qc_info.fastq_dir,
503-
base_path)))
504-
print("...updated Fastq directory: %s" % new_fastq_dir)
505-
qc_info['fastq_dir'] = new_fastq_dir
506473
if not dry_run:
507-
qc_info.save()
508-
# Bail out if there was a problem
509-
if retval != 0:
510-
if not force:
511-
raise Exception("Finalising archive failed")
512-
else:
513-
logger.warning("Finalising archive failed (ignored)")
474+
project.info.save()
475+
else:
476+
print("Project metadata not updated for dry run")
477+
# Bail out if there was a problem
478+
if retval != 0:
479+
if not force:
480+
raise Exception("Finalising archive failed")
481+
else:
482+
logger.warning("Finalising archive failed (ignored)")
514483
# Complete archiving
515484
print("Moving to final location: %s" % final_dest)
516485
if not dry_run:

auto_process_ngs/commands/clone_cmd.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env python
22
#
33
# clone_cmd.py: implement auto process clone command
4-
# Copyright (C) University of Manchester 2019 Peter Briggs
4+
# Copyright (C) University of Manchester 2019-2026 Peter Briggs
55
#
66
#########################################################################
77

@@ -13,7 +13,7 @@
1313
import logging
1414
import shutil
1515
from ..analysis import AnalysisProject
16-
from ..metadata import AnalysisDirParameters
16+
from ..auto_processor import AutoProcess
1717
import bcftbx.utils as bcf_utils
1818

1919
# Module specific logger
@@ -141,17 +141,5 @@ def clone(ap,clone_dir,copy_fastqs=False,exclude_projects=False):
141141
for subdir in ('logs','ScriptCode',):
142142
print("[Subdirectories] making %s" % subdir)
143143
bcf_utils.mkdir(os.path.join(clone_dir,subdir))
144-
# Update the settings
145-
parameter_file = os.path.join(clone_dir,
146-
os.path.basename(ap.parameter_file))
147-
params = AnalysisDirParameters(filen=os.path.join(
148-
clone_dir,
149-
os.path.basename(ap.parameter_file)))
150-
for p in ("sample_sheet","primary_data_dir"):
151-
if not params[p]:
152-
continue
153-
print("[Parameters] updating '%s'" % p)
154-
params[p] = os.path.join(clone_dir,
155-
os.path.relpath(params[p],
156-
ap.analysis_dir))
157-
params.save()
144+
# Update the paths
145+
AutoProcess(clone_dir).update_paths()

0 commit comments

Comments
 (0)