33import sys
44import glob
55import csv
6+ from datetime import datetime
7+ from epiweeks import Week
8+ import json
69from scripts .progress_utils import Callback
710import psycopg2
811import psycopg2 .extras
@@ -15,6 +18,8 @@ def parse_args():
1518
1619 parser .add_argument ('--indir' , type = str , default = "/home/wastewater/results/gromstole" ,
1720 help = "Path to the gromstole results directory" )
21+ parser .add_argument ('--upload_dir' , type = str , default = "/home/wastewater/uploads" ,
22+ help = "Path to the uploads directory" )
1823 parser .add_argument ('--dbname' , type = str , default = os .environ .get ("POSTGRES_DB" , "gromstole_db" ),
1924 help = "Postgresql database name" )
2025 parser .add_argument ('--dbhost' , type = str , default = os .environ .get ("POSTGRES_HOST" , "localhost" ),
@@ -43,42 +48,111 @@ def open_connection(connection_parameters):
4348 LAB VARCHAR(255),
4449 RUN VARCHAR(255),
4550 SAMPLE VARCHAR(255),
46- POSITION VARCHAR(255),
51+ COLDATE DATE,
52+ REGION VARCHAR(255),
53+ LATITUDE FLOAT NULL,
54+ LONGITUDE FLOAT NULL,
4755 LABEL VARCHAR(255),
56+ MUTATION VARCHAR(255),
4857 FREQUENCY VARCHAR(255),
4958 COVERAGE VARCHAR(255),
5059 PATH VARCHAR(255))'''
5160 cur .execute (results_table )
5261
5362 cur .execute ('''CREATE INDEX IF NOT EXISTS frequency_index ON RESULTS (frequency)''' )
5463
64+ aggregate_table = '''CREATE TABLE IF NOT EXISTS AGGREGATE_MAPPED (
65+ ID SERIAL PRIMARY KEY,
66+ REGION VARCHAR(255),
67+ YEAR VARCHAR(255),
68+ EPIWEEK VARCHAR(255),
69+ NUC VARCHAR(255),
70+ AMINO VARCHAR(255),
71+ NSAMPLES INTEGER,
72+ COUNT INTEGER,
73+ COVERAGE INTEGER )'''
74+ cur .execute (aggregate_table )
75+
5576 conn .commit ()
5677 return cur , conn
5778
5879
59- def insert_files (cur , files , callback = None ):
80+ def parse_date (dt , formats = ('%y-%m-%d' , '%d-%m-%y' , '%d-%m-%Y' , '%m-%d-%Y' , '%Y-%m-%d' , '%d-%b-%y' , '%m/%d/%Y' , '%d/%m/%y' )):
81+ """ Try multiple date formats """
82+ if dt in ['UNK' , '' , ' ' , 'null' , None ]:
83+ return None
84+ for fmt in formats :
85+ try :
86+ return datetime .strptime (dt , fmt )
87+ except ValueError :
88+ pass
89+ raise ValueError (f"No supported format detected for date string '{ dt } '" )
90+
91+
92+ def retrieve_metadata (runs , upload_dir , callback = None ):
93+ """
94+ Retrieve metadata from the metadata.csv file
95+
96+ :param runs: set, runs to retrieve metadata for
97+ :param upload_dir: str, path to the upload directory
98+ :return: dict, metadata
99+ """
100+ metadata = {}
101+ for lab , run in runs :
102+ if lab not in metadata :
103+ metadata .update ({lab : {}})
104+ if run not in metadata [lab ]:
105+ metadata [lab ].update ({run : {}})
106+
107+ upload_path = os .path .join (upload_dir , lab , run , 'metadata.csv' )
108+ if not os .path .exists (upload_path ):
109+ callback ('Metadata file not found: {}' .format (upload_path ))
110+ continue
111+
112+ with open (upload_path , encoding = 'latin-1' ) as handle :
113+ for row in csv .DictReader (handle ):
114+ sample_key = 'r1 fastq filename'
115+ try :
116+ if len (row ) == 1 or row [sample_key ] == '' :
117+ continue
118+ except :
119+ callback ('Empty metadata file: {}' .format (row ))
120+ raise
121+
122+ sample = row [sample_key ].split ('_' )[0 ]
123+
124+ if lab == 'western' :
125+ sample = sample .replace ('_' , '-' )
126+ try :
127+ metadata [lab ][run ].update ({sample : {
128+ 'coldate' : row ['sample collection date' ],
129+ 'region' : row ['geolocation name (region)' ],
130+ 'latitude' : row ['geolocation latitude' ],
131+ 'longitude' : row ['geolocation longitude' ]
132+ }})
133+ except :
134+ callback ('Column names incorrect in {}' .format (os .path .join (upload_path )))
135+ continue
136+ return metadata
137+
138+
139+ def new_mapped_files (cur , files , callback = None ):
60140 """
61- Inserts the file name, file creation date, path and checksum of the file into the database
141+ Get the set of new mapped files to process
62142
63143 :param curr: object, cursor object
64144 :param filepath: str, path to the file
65- :return: None
145+ :return: set, new files, new runs
66146 """
147+ new_files = set ()
148+ new_runs = set ()
67149 for lab , run , sample , path in files :
68150 cur .execute ('SELECT * FROM RESULTS WHERE LAB = %s AND RUN = %s AND SAMPLE = %s' , (lab , run , sample ))
69151 if cur .fetchone () is not None :
70152 continue
71- callback ("Inserting results from file: {}" .format (path ))
72- with open (path , 'r' ) as f :
73- mapped = csv .DictReader (f )
74- for line in mapped :
75- if float (line ['coverage' ]) * float (line ['frequency' ]) == 1 :
76- continue
77-
78- cur .execute ("INSERT INTO RESULTS (LAB, RUN, SAMPLE, POSITION, LABEL, FREQUENCY, COVERAGE, PATH)"
79- " VALUES(%s, %s, %s, %s, %s, %s, %s, %s)"
80- " ON CONFLICT DO NOTHING" ,
81- (lab , run , sample , line ['position' ], line ['label' ], line ['frequency' ], line ['coverage' ], path ))
153+ new_files .add ((lab , run , sample , path ))
154+ new_runs .add ((lab , run ))
155+ return new_files , new_runs
82156
83157
84158def get_files (files ):
@@ -93,10 +167,92 @@ def get_files(files):
93167 runpath , sample = os .path .split (normfile )
94168 run = os .path .basename (runpath )
95169 lab = found_keywords [0 ]
96- f .add ((lab , run , sample , normfile ))
170+ f .add ((lab , run , sample . split ( '.' )[ 0 ] , normfile ))
97171 return f
98172
99173
174+ def insert_files (cur , files , metadata , callback = None ):
175+ """
176+ Insert files into the database
177+
178+ :param curr: object, cursor object
179+ :param files: set, files to insert
180+ :param metadata: dict, metadata
181+ :return: None
182+ """
183+
184+ # import location to region map as a dict
185+ # TODO: Need a better way to map locations to regions
186+ regions = json .load (open ("data/regions.json" ))
187+
188+ for lab , run , sample , path in files :
189+ if lab not in metadata :
190+ callback ("Lab {} not in metadata" .format (lab ))
191+ continue
192+ if run not in metadata [lab ]:
193+ callback ("Run {} not in metadata" .format (run ))
194+ continue
195+ if sample not in metadata [lab ][run ]:
196+ callback ("Sample {} from {}/{} not in metadata" .format (sample , lab , run ))
197+ continue
198+
199+ md = metadata [lab ][run ][sample ]
200+ callback ("Inserting results from file: {}" .format (path ))
201+ with open (path , 'r' ) as f :
202+ mapped = csv .DictReader (f )
203+ last_fail = None
204+ failed_latlong = set ()
205+ for line in mapped :
206+ coverage = float (line ['coverage' ])
207+ frequency = float (line ['frequency' ])
208+ if coverage < 10 or frequency < 1e-3 :
209+ continue
210+
211+ coldate = parse_date (md ['coldate' ].strip ())
212+ if coldate is None :
213+ continue
214+
215+ try :
216+ latitude = float (md ['latitude' ])
217+ longitude = float (md ['longitude' ])
218+ except ValueError :
219+ if (lab , run , sample ) not in failed_latlong :
220+ failed_latlong .add ((lab , run , sample ))
221+ callback (f"Setting latitude and longitude to None for { lab } /{ run } /{ sample } " )
222+ latitude = None
223+ longitude = None
224+
225+ cur .execute ("INSERT INTO RESULTS (LAB, RUN, SAMPLE, COLDATE, REGION, LATITUDE, LONGITUDE, LABEL, MUTATION, FREQUENCY, COVERAGE, PATH)"
226+ " VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)"
227+ " ON CONFLICT DO NOTHING" ,
228+ (lab , run , sample , coldate , md ['region' ], latitude , longitude ,
229+ line ['position' ], line ['label' ], line ['frequency' ], line ['coverage' ], path ))
230+
231+
232+ # Aggregate Data
233+ location = md ['region' ].strip ()
234+ region = regions .get (location , None )
235+ if region is None and location != last_fail :
236+ callback (f"Failed to map location { location } to region" )
237+ last_fail = location
238+ continue
239+
240+ epiweek = Week .fromdate (coldate .date ())
241+ mutation = (line ['label' ], line ['mutation' ])
242+
243+ # Check to see if the value exists in database
244+ cur .execute ("SELECT * FROM AGGREGATE_MAPPED WHERE REGION = %s AND YEAR = '%s' AND EPIWEEK = '%s' AND NUC = %s AND AMINO = %s" ,
245+ (region , epiweek .year , epiweek .week , mutation [0 ], mutation [1 ]))
246+ if cur .fetchone () is None :
247+ cur .execute ("INSERT INTO AGGREGATE_MAPPED (REGION, YEAR, EPIWEEK, NUC, AMINO, NSAMPLES, COUNT, COVERAGE)"
248+ " VALUES(%s, %s, %s, %s, %s, %s, %s, %s)" ,
249+ (region , epiweek .year , epiweek .week , mutation [0 ], mutation [1 ], 1 , frequency * coverage , coverage ))
250+ else :
251+ cur .execute ("UPDATE AGGREGATE_MAPPED SET NSAMPLES = NSAMPLES + 1, COUNT = COUNT + %s, COVERAGE = COVERAGE + %s"
252+ " WHERE REGION = %s AND YEAR = '%s' AND EPIWEEK = '%s' AND NUC = %s AND AMINO = %s" ,
253+ (frequency * coverage , coverage , region , epiweek .year , epiweek .week , mutation [0 ], mutation [1 ]))
254+
255+
100256if __name__ == "__main__" :
101257 args = parse_args ()
102258 cb = Callback ()
@@ -130,7 +286,9 @@ def get_files(files):
130286 connection_parameters ['dbname' ] = args .dbname
131287 cur , conn = open_connection (connection_parameters )
132288 files = glob .glob ("{}/**/*.mapped.csv" .format (args .indir ), recursive = True )
133- clean_files = get_files (files )
134- insert_files (cur , clean_files , callback = cb .callback )
289+ mapped_files = get_files (files )
290+ unprocessed_mapped_files , unprocessed_runs = new_mapped_files (cur , mapped_files , callback = cb .callback )
291+ metadata = retrieve_metadata (unprocessed_runs , args .upload_dir , callback = cb .callback )
292+ insert_files (cur , unprocessed_mapped_files , metadata , callback = cb .callback )
135293 conn .commit ()
136294 conn .close ()
0 commit comments