Skip to content

Commit 8282daa

Browse files
committed
auto_processor: implement 'sync_project_metadata_file' method for AutoProcess.
Implements a new method 'sync_project_metadata_file' method in the 'AutoProcess' class, which ensures that the projects listed in 'projects.info' are consistent with those on the filesystem. The code is copies from the 'update' command.
1 parent 484e5eb commit 8282daa

2 files changed

Lines changed: 238 additions & 0 deletions

File tree

auto_process_ngs/auto_processor.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,58 @@ def update_project_metadata_file(self,unaligned_dir=None,
531531
print("Updated project metadata file '%s'" %
532532
self.params.project_metadata)
533533

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

auto_process_ngs/test/test_auto_processor.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,189 @@ def test_update_paths_relocated_analysis_dir_with_qc(self):
11671167
os.path.join(new_path,
11681168
proj.name,
11691169
"fastqs"))
1170+
1171+
1172+
class TestAutoProcessSyncProjectMetadataFile(unittest.TestCase):
1173+
"""
1174+
Tests for the 'sync_project_metadata_file' method
1175+
"""
1176+
def setUp(self):
1177+
# Project metadata items mapping
1178+
self.metadata_map = {
1179+
"User": "user",
1180+
"PI": "PI",
1181+
"Library": "library_type",
1182+
"SC_Platform": "single_cell_platform",
1183+
"Organism": "organism",
1184+
"Comments": "comments"
1185+
}
1186+
# Create a temp working dir
1187+
self.dirn = tempfile.mkdtemp(suffix='TestAutoProcess')
1188+
# Store original location
1189+
self.pwd = os.getcwd()
1190+
# Move to working directory
1191+
os.chdir(self.dirn)
1192+
1193+
def tearDown(self):
1194+
# Return to original dir
1195+
os.chdir(self.pwd)
1196+
# Remove the temporary test directory
1197+
shutil.rmtree(self.dirn)
1198+
1199+
def test_sync_project_metadata_file_project_no_longer_exists(self):
1200+
"""
1201+
AutoProcess.sync_project_metadata_file: remove non-existent project
1202+
"""
1203+
# Metadata for projects
1204+
project_metadata = {
1205+
"AB": {
1206+
"User": "Alan Bailey",
1207+
"PI": "Archie Ballard",
1208+
"Library": "RNA-seq",
1209+
"Organism": "Human"
1210+
}
1211+
}
1212+
# Make an auto-process directory with projects
1213+
mockdir = MockAnalysisDirFactory.bcl2fastq2(
1214+
'231021_A00879_0087_000000000-AGEW9',
1215+
'novaseq',
1216+
project_metadata=project_metadata,
1217+
metadata={ "run_number": 87,
1218+
"source": "local" },
1219+
top_dir=self.dirn)
1220+
mockdir.create()
1221+
# Remove CDE project
1222+
shutil.rmtree(os.path.join(mockdir.dirn, "CDE"))
1223+
# Remove initial entry for CDE in projects.info
1224+
projects_info_contents = []
1225+
with open(os.path.join(mockdir.dirn,"projects.info"),'rt') as fp:
1226+
for line in fp:
1227+
if not line.startswith("CDE"):
1228+
projects_info_contents.append(line)
1229+
with open(os.path.join(mockdir.dirn,"projects.info"),'wt') as fp:
1230+
fp.write("".join(projects_info_contents))
1231+
# Set up AutoProcess instance
1232+
ap = AutoProcess(mockdir.dirn)
1233+
# Check metadata items in projects.info pre-update
1234+
ap_project_metadata = ap.load_project_metadata()
1235+
for pname in project_metadata:
1236+
for item in project_metadata[pname]:
1237+
self.assertEqual(ap_project_metadata.lookup(pname)[item],
1238+
project_metadata[pname][item])
1239+
# Check metadata items in projects pre-update
1240+
for pname in project_metadata:
1241+
p = ap.get_analysis_projects(pname)[0]
1242+
for item in project_metadata[pname]:
1243+
project_item = self.metadata_map[item]
1244+
expected_value = project_metadata[pname][item]
1245+
self.assertEqual(p.info[project_item],
1246+
expected_value)
1247+
# Append info for missing project 'CDE' to projects.info
1248+
with open(os.path.join(mockdir.dirn,"projects.info"),'at') as fp:
1249+
fp.write("""CDE\tCDE3,CDE4\tCharles Edwards\tChIP-seq\t.\tMouse\tChristian Eggars\t1% PhiX spiked in
1250+
""")
1251+
# Do the update
1252+
ap.sync_project_metadata_file()
1253+
# Reload and confirm the updates in projects.info
1254+
ap = AutoProcess(mockdir.dirn)
1255+
ap_project_metadata = ap.load_project_metadata()
1256+
for pname in ["AB", "#CDE"]:
1257+
self.assertTrue(pname in [p["Project"] for p in ap_project_metadata],
1258+
f"'{pname}' not in project metadata")
1259+
1260+
def test_sync_project_metadata_file_add_unlisted_projects(self):
1261+
"""
1262+
AutoProcess.sync_project_metadata_file: add unlisted projects
1263+
"""
1264+
# Metadata for projects
1265+
project_metadata = {
1266+
"AB": {
1267+
"User": "Alan Bailey",
1268+
"PI": "Archie Ballard",
1269+
"Library": "RNA-seq",
1270+
"Organism": "Human"
1271+
},
1272+
"CDE": {
1273+
"User": "Charles Edwards",
1274+
"PI": "Christian Eggars",
1275+
"Library": "ChIP-seq",
1276+
"Organism": "Mouse"
1277+
}
1278+
}
1279+
# Make an auto-process directory with projects
1280+
mockdir = MockAnalysisDirFactory.bcl2fastq2(
1281+
'231021_A00879_0087_000000000-AGEW9',
1282+
'novaseq',
1283+
project_metadata=project_metadata,
1284+
metadata={ "run_number": 87,
1285+
"source": "local" },
1286+
top_dir=self.dirn)
1287+
mockdir.create()
1288+
# Remove initial entry for CDE in projects.info
1289+
projects_info_contents = []
1290+
with open(os.path.join(mockdir.dirn,"projects.info"),'rt') as fp:
1291+
for line in fp:
1292+
if not line.startswith("CDE"):
1293+
projects_info_contents.append(line)
1294+
with open(os.path.join(mockdir.dirn,"projects.info"),'wt') as fp:
1295+
fp.write("".join(projects_info_contents))
1296+
# Set up AutoProcess instance and do the update
1297+
ap = AutoProcess(mockdir.dirn)
1298+
ap.sync_project_metadata_file()
1299+
# Check contents of projects.info
1300+
with open(os.path.join(mockdir.dirn,"projects.info"),'rt') as fp:
1301+
expected_lines = [
1302+
"#Project\tSamples\tUser\tLibrary\tSC_Platform\tOrganism\tPI\tComments",
1303+
"AB\tAB1,AB2\tAlan Bailey\tRNA-seq\t.\tHuman\tArchie Ballard\t.",
1304+
"CDE\tCDE3,CDE4\tCharles Edwards\tChIP-seq\t.\tMouse\tChristian Eggars\t."
1305+
]
1306+
for expected, actual in zip(expected_lines, fp.read().split("\n")):
1307+
self.assertEqual(expected.strip(), actual.strip())
1308+
1309+
def test_sync_project_metadata_file_add_unlisted_projects_ignore_special_names(self):
1310+
"""
1311+
AutoProcess.sync_project_metadata_file: ignore projects with "special" names
1312+
"""
1313+
# Metadata for projects
1314+
project_metadata = {
1315+
"AB": {
1316+
"User": "Alan Bailey",
1317+
"PI": "Archie Ballard",
1318+
"Library": "RNA-seq",
1319+
"Organism": "Human"
1320+
},
1321+
"CDE.bak": {
1322+
"User": "Charles Edwards",
1323+
"PI": "Christian Eggars",
1324+
"Library": "ChIP-seq",
1325+
"Organism": "Mouse"
1326+
}
1327+
}
1328+
# Make an auto-process directory with projects
1329+
mockdir = MockAnalysisDirFactory.bcl2fastq2(
1330+
'231021_A00879_0087_000000000-AGEW9',
1331+
'novaseq',
1332+
project_metadata=project_metadata,
1333+
metadata={ "run_number": 87,
1334+
"source": "local" },
1335+
top_dir=self.dirn)
1336+
mockdir.create()
1337+
# Remove entry for CDE.bak in projects.info
1338+
projects_info_contents = []
1339+
with open(os.path.join(mockdir.dirn,"projects.info"),'rt') as fp:
1340+
for line in fp:
1341+
if not line.startswith("CDE.bak"):
1342+
projects_info_contents.append(line)
1343+
with open(os.path.join(mockdir.dirn,"projects.info"),'wt') as fp:
1344+
fp.write("".join(projects_info_contents))
1345+
# Set up AutoProcess instance and do the update
1346+
ap = AutoProcess(mockdir.dirn)
1347+
ap.sync_project_metadata_file()
1348+
# Check contents of projects.info
1349+
with open(os.path.join(mockdir.dirn,"projects.info"),'rt') as fp:
1350+
expected_lines = [
1351+
"#Project\tSamples\tUser\tLibrary\tSC_Platform\tOrganism\tPI\tComments",
1352+
"AB\tAB1,AB2\tAlan Bailey\tRNA-seq\t.\tHuman\tArchie Ballard\t."
1353+
]
1354+
for expected, actual in zip(expected_lines, fp.read().split("\n")):
1355+
self.assertEqual(expected.strip(), actual.strip())

0 commit comments

Comments
 (0)