29 # This should be read from the uploaded file, but would require an extra NetCDF module. |
43 # This should be read from the uploaded file, but would require an extra NetCDF module. |
30 regex = "<h3>Measurement (?P<measurement_id>.{12,15}) <small>" # {12, 15} to handle both old- and new-style measurement ids. |
44 regex = "<h3>Measurement (?P<measurement_id>.{12,15}) <small>" # {12, 15} to handle both old- and new-style measurement ids. |
31 |
45 |
32 |
46 |
33 class SCC: |
47 class SCC: |
34 """A simple class that will attempt to upload a file on the SCC server. |
48 """A class that will attempt to interact SCC server. |
35 |
49 |
36 The uploading is done by simulating a normal browser session. In the current |
50 Most interactions are by simulating a normal browser session. In the current |
37 version no check is performed, and no feedback is given if the upload |
51 version few checks are performed before upload a file, and no feedback is given in case the upload |
38 was successful. If everything is setup correctly, it will work. |
52 fails. |
39 """ |
53 """ |
40 |
54 |
41 def __init__(self, auth, output_dir, base_url): |
55 def __init__(self, auth, output_dir, base_url): |
42 |
56 |
43 self.auth = auth |
57 self.auth = auth |
45 self.base_url = base_url |
59 self.base_url = base_url |
46 self.session = requests.Session() |
60 self.session = requests.Session() |
47 self.session.auth = auth |
61 self.session.auth = auth |
48 self.session.verify = False |
62 self.session.verify = False |
49 |
63 |
|
64 # Setup SCC server URLS for later use |
50 self.login_url = urlparse.urljoin(self.base_url, 'accounts/login/') |
65 self.login_url = urlparse.urljoin(self.base_url, 'accounts/login/') |
51 self.logout_url = urlparse.urljoin(self.base_url, 'accounts/logout/') |
66 self.logout_url = urlparse.urljoin(self.base_url, 'accounts/logout/') |
52 self.list_measurements_url = urlparse.urljoin(self.base_url, 'data_processing/measurements/') |
67 self.list_measurements_url = urlparse.urljoin(self.base_url, 'data_processing/measurements/') |
53 |
68 |
54 self.upload_url = urlparse.urljoin(self.base_url, 'data_processing/measurements/quick/') |
69 self.upload_url = urlparse.urljoin(self.base_url, 'data_processing/measurements/quick/') |
55 self.measurement_page_pattern = urlparse.urljoin(self.base_url, 'data_processing/measurements/{0}/') |
70 self.measurement_page_pattern = urlparse.urljoin(self.base_url, 'data_processing/measurements/{0}/') |
56 self.download_hirelpp_pattern = urlparse.urljoin(self.base_url, |
71 self.download_hirelpp_pattern = urlparse.urljoin(self.base_url, |
57 'data_processing/measurements/{0}/download-hirelpp/') |
72 'data_processing/measurements/{0}/download-hirelpp/') |
58 self.download_cloudmask_pattern = urlparse.urljoin(self.base_url, |
73 self.download_cloudmask_pattern = urlparse.urljoin(self.base_url, |
59 'data_processing/measurements/{0}/download-cloudmask/') |
74 'data_processing/measurements/{0}/download-cloudmask/') |
60 |
75 |
61 self.download_elpp_pattern = urlparse.urljoin(self.base_url, |
76 self.download_elpp_pattern = urlparse.urljoin(self.base_url, |
62 'data_processing/measurements/{0}/download-preprocessed/') |
77 'data_processing/measurements/{0}/download-preprocessed/') |
63 self.download_elda_pattern = urlparse.urljoin(self.base_url, |
78 self.download_elda_pattern = urlparse.urljoin(self.base_url, |
64 'data_processing/measurements/{0}/download-optical/') |
79 'data_processing/measurements/{0}/download-optical/') |
65 self.download_plots_pattern = urlparse.urljoin(self.base_url, |
80 self.download_plots_pattern = urlparse.urljoin(self.base_url, |
66 'data_processing/measurements/{0}/download-plots/') |
81 'data_processing/measurements/{0}/download-plots/') |
67 self.download_elic_pattern = urlparse.urljoin(self.base_url, |
82 self.download_elic_pattern = urlparse.urljoin(self.base_url, |
68 'data_processing/measurements/{0}/download-elic/') |
83 'data_processing/measurements/{0}/download-elic/') |
69 self.delete_measurement_pattern = urlparse.urljoin(self.base_url, 'admin/database/measurements/{0}/delete/') |
84 self.delete_measurement_pattern = urlparse.urljoin(self.base_url, 'admin/database/measurements/{0}/delete/') |
70 |
85 |
|
86 # Setup API URLs for later use |
71 self.api_base_url = urlparse.urljoin(self.base_url, 'api/v1/') |
87 self.api_base_url = urlparse.urljoin(self.base_url, 'api/v1/') |
72 self.api_measurement_pattern = urlparse.urljoin(self.api_base_url, 'measurements/{0}/') |
88 self.api_measurement_pattern = urlparse.urljoin(self.api_base_url, 'measurements/{0}/') |
73 self.api_measurements_url = urlparse.urljoin(self.api_base_url, 'measurements') |
89 self.api_measurements_url = urlparse.urljoin(self.api_base_url, 'measurements') |
74 self.api_sounding_search_pattern = urlparse.urljoin(self.api_base_url, 'sounding_files/?filename={0}') |
90 self.api_sounding_search_pattern = urlparse.urljoin(self.api_base_url, 'sounding_files/?filename={0}') |
75 self.api_lidarratio_search_pattern = urlparse.urljoin(self.api_base_url, 'lidarratio_files/?filename={0}') |
91 self.api_lidarratio_search_pattern = urlparse.urljoin(self.api_base_url, 'lidarratio_files/?filename={0}') |
76 self.api_overlap_search_pattern = urlparse.urljoin(self.api_base_url, 'overlap_files/?filename={0}') |
92 self.api_overlap_search_pattern = urlparse.urljoin(self.api_base_url, 'overlap_files/?filename={0}') |
77 |
93 |
78 def login(self, credentials): |
94 def login(self, credentials): |
79 """ Login to SCC. """ |
95 """ Login to the SCC. |
|
96 |
|
97 Parameters |
|
98 ---------- |
|
99 credentials : tuple or list |
|
100 A list or tuple in the form (username, password). |
|
101 """ |
80 logger.debug("Attempting to login to SCC, username %s." % credentials[0]) |
102 logger.debug("Attempting to login to SCC, username %s." % credentials[0]) |
81 login_credentials = {'username': credentials[0], |
103 login_credentials = {'username': credentials[0], |
82 'password': credentials[1]} |
104 'password': credentials[1]} |
83 |
105 |
84 logger.debug("Accessing login page at %s." % self.login_url) |
106 logger.debug("Accessing login page at %s." % self.login_url) |
85 |
107 |
86 # Get upload form |
108 # Get login form |
87 login_page = self.session.get(self.login_url) |
109 login_page = self.session.get(self.login_url) |
88 |
110 |
89 if not login_page.ok: |
111 if not login_page.ok: |
90 raise self.PageNotAccessibleError('Could not access login pages. Status code %s' % login_page.status_code) |
112 raise self.PageNotAccessibleError('Could not access login pages. Status code %s' % login_page.status_code) |
91 |
113 |
96 headers={'X-CSRFToken': login_page.cookies['csrftoken'], |
118 headers={'X-CSRFToken': login_page.cookies['csrftoken'], |
97 'referer': self.login_url}) |
119 'referer': self.login_url}) |
98 return login_submit |
120 return login_submit |
99 |
121 |
100 def logout(self): |
122 def logout(self): |
101 """ Logout from SCC """ |
123 """ Logout from the SCC """ |
102 return self.session.get(self.logout_url, stream=True) |
124 return self.session.get(self.logout_url, stream=True) |
103 |
125 |
104 def upload_file(self, filename, system_id, force_upload, delete_related, delay=0, rs_filename=None, ov_filename=None, lr_filename=None): |
126 def upload_file(self, filename, system_id, force_upload, delete_related, delay=0, rs_filename=None, |
105 """ Upload a filename for processing with a specific system. If the |
127 ov_filename=None, lr_filename=None): |
106 upload is successful, it returns the measurement id. """ |
128 """ Upload a file for processing. |
|
129 |
|
130 If the upload is successful, it returns the measurement id. |
|
131 |
|
132 |
|
133 Parameters |
|
134 ---------- |
|
135 filename : str |
|
136 File path of the file to upload |
|
137 system_id : int |
|
138 System id to be used in the processing |
|
139 force_upload : bool |
|
140 If True, if a measurement with the same ID is found on the server, it will be first deleted and the |
|
141 file current file will be uploaded. If False, the file will not be uploaded if the measurement ID is |
|
142 already present on the SCC server. |
|
143 delete_related : bool |
|
144 Answer to delete related question when deleting existing measurements from the SCC. |
|
145 rs_filename, ov_filename, lr_filename : str |
|
146 Ancillary files pahts to be uploaded. |
|
147 """ |
|
148 # Get the measurement ID from the netcdf file |
107 measurement_id = self.measurement_id_from_file(filename) |
149 measurement_id = self.measurement_id_from_file(filename) |
108 |
150 |
|
151 # Handle possible existing measurements with the same ID on the SCC server. |
109 logger.debug('Checking if a measurement with the same id already exists on the SCC server.') |
152 logger.debug('Checking if a measurement with the same id already exists on the SCC server.') |
110 existing_measurement, _ = self.get_measurement(measurement_id) |
153 existing_measurement, _ = self.get_measurement(measurement_id) |
111 |
154 |
112 if existing_measurement: |
155 if existing_measurement: |
113 if force_upload: |
156 if force_upload: |
130 |
176 |
131 logger.debug("Submitted processing parameters - System: {}, Delay: {}".format(system_id, delay)) |
177 logger.debug("Submitted processing parameters - System: {}, Delay: {}".format(system_id, delay)) |
132 |
178 |
133 files = {'data': open(filename, 'rb')} |
179 files = {'data': open(filename, 'rb')} |
134 |
180 |
|
181 # Add ancillary files to be uploaded |
135 if rs_filename is not None: |
182 if rs_filename is not None: |
136 ancillary_file, _ = self.get_ancillary(rs_filename, 'sounding') |
183 ancillary_file, _ = self.get_ancillary(rs_filename, 'sounding') |
137 |
184 |
138 if ancillary_file.already_on_scc: |
185 if ancillary_file.already_on_scc: |
139 logger.warning("Sounding file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format(ancillary_file)) |
186 logger.warning( |
|
187 "Sounding file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format(ancillary_file)) |
140 else: |
188 else: |
141 logger.debug('Adding sounding file %s' % rs_filename) |
189 logger.debug('Adding sounding file %s' % rs_filename) |
142 files['sounding_file'] = open(rs_filename, 'rb') |
190 files['sounding_file'] = open(rs_filename, 'rb') |
143 |
191 |
144 if ov_filename is not None: |
192 if ov_filename is not None: |
145 ancillary_file, _ = self.get_ancillary(ov_filename, 'overlap') |
193 ancillary_file, _ = self.get_ancillary(ov_filename, 'overlap') |
146 |
194 |
147 if ancillary_file.already_on_scc: |
195 if ancillary_file.already_on_scc: |
148 logger.warning("Overlap file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format(ancillary_file)) |
196 logger.warning( |
|
197 "Overlap file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format(ancillary_file)) |
149 else: |
198 else: |
150 logger.debug('Adding overlap file %s' % ov_filename) |
199 logger.debug('Adding overlap file %s' % ov_filename) |
151 files['overlap_file'] = open(ov_filename, 'rb') |
200 files['overlap_file'] = open(ov_filename, 'rb') |
152 |
201 |
153 if lr_filename is not None: |
202 if lr_filename is not None: |
154 ancillary_file, _ = self.get_ancillary(lr_filename, 'lidarratio') |
203 ancillary_file, _ = self.get_ancillary(lr_filename, 'lidarratio') |
155 |
204 |
156 if ancillary_file.already_on_scc: |
205 if ancillary_file.already_on_scc: |
157 logger.warning( |
206 logger.warning( |
158 "Lidar ratio file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format(ancillary_file)) |
207 "Lidar ratio file {0.filename} already on the SCC with id {0.id}. Ignoring it.".format( |
|
208 ancillary_file)) |
159 else: |
209 else: |
160 logger.debug('Adding lidar ratio file %s' % lr_filename) |
210 logger.debug('Adding lidar ratio file %s' % lr_filename) |
161 files['lidar_ratio_file'] = open(lr_filename, 'rb') |
211 files['lidar_ratio_file'] = open(lr_filename, 'rb') |
162 |
212 |
|
213 # Upload the files |
163 logger.info("Uploading of file %s started." % filename) |
214 logger.info("Uploading of file %s started." % filename) |
164 |
215 |
165 upload_submit = self.session.post(self.upload_url, |
216 upload_submit = self.session.post(self.upload_url, |
166 data=upload_data, |
217 data=upload_data, |
167 files=files, |
218 files=files, |
170 |
221 |
171 if upload_submit.status_code != 200: |
222 if upload_submit.status_code != 200: |
172 logger.warning("Connection error. Status code: %s" % upload_submit.status_code) |
223 logger.warning("Connection error. Status code: %s" % upload_submit.status_code) |
173 return False |
224 return False |
174 |
225 |
175 # Check if there was a redirect to a new page. |
226 # Check if there was a redirect to a new page. If not, something went wrong |
176 if upload_submit.url == self.upload_url: |
227 if upload_submit.url == self.upload_url: |
177 measurement_id = False |
228 measurement_id = False |
178 logger.error("Uploaded file(s) rejected! Try to upload manually to see the error.") |
229 logger.error("Uploaded file(s) rejected! Try to upload manually to see the error.") |
179 else: |
230 else: |
180 measurement_id = re.findall(regex, upload_submit.text)[0] |
231 # TODO: Check if this is needed. This was used when the measurement ID was not read from the input file. |
|
232 measurement_id = re.findall(regex, upload_submit.text)[0] # Get the measurement ID from the output page |
181 logger.info("Successfully uploaded measurement with id %s." % measurement_id) |
233 logger.info("Successfully uploaded measurement with id %s." % measurement_id) |
182 logger.info("You can monitor the processing progress online: {}".format(self.measurement_page_pattern.format(measurement_id))) |
234 logger.info("You can monitor the processing progress online: {}".format( |
|
235 self.measurement_page_pattern.format(measurement_id))) |
183 return measurement_id |
236 return measurement_id |
184 |
237 |
185 @staticmethod |
238 @staticmethod |
186 def measurement_id_from_file(filename): |
239 def measurement_id_from_file(filename): |
187 """ Get the measurement id from the input file. """ |
240 """ Get the measurement id from the input file. |
|
241 |
|
242 Parameters |
|
243 ---------- |
|
244 filename : str |
|
245 File path of the input file. |
|
246 """ |
188 |
247 |
189 if not os.path.isfile(filename): |
248 if not os.path.isfile(filename): |
190 logger.error("File {} does not exist.".format(filename)) |
249 logger.error("File {} does not exist.".format(filename)) |
191 sys.exit(1) |
250 sys.exit(1) |
192 |
251 |
343 logger.info("Rerun-all command submitted successfully for id {}.".format(measurement_id)) |
404 logger.info("Rerun-all command submitted successfully for id {}.".format(measurement_id)) |
344 |
405 |
345 if monitor: |
406 if monitor: |
346 self.monitor_processing(measurement_id) |
407 self.monitor_processing(measurement_id) |
347 |
408 |
348 def process(self, filename, system_id, monitor, force_upload, delete_related, delay=0, rs_filename=None, lr_filename=None, ov_filename=None): |
409 def process(self, filename, system_id, monitor, force_upload, delete_related, delay=0, rs_filename=None, |
|
410 lr_filename=None, ov_filename=None): |
349 """ Upload a file for processing and wait for the processing to finish. |
411 """ Upload a file for processing and wait for the processing to finish. |
350 If the processing is successful, it will download all produced files. |
412 If the processing is successful, it will download all produced files. |
351 """ |
413 """ |
352 logger.info("--- Processing started on %s. ---" % datetime.datetime.now()) |
414 logger.info("--- Processing started on %s. ---" % datetime.datetime.now()) |
353 # Upload file |
415 # Upload file |
422 logger.info("Downloading ELDA plots.") |
484 logger.info("Downloading ELDA plots.") |
423 self.download_plots(measurement_id) |
485 self.download_plots(measurement_id) |
424 if measurement.elic == 127: |
486 if measurement.elic == 127: |
425 logger.info("Downloading ELIC files.") |
487 logger.info("Downloading ELIC files.") |
426 self.download_elic(measurement_id) |
488 self.download_elic(measurement_id) |
427 if measurement.is_calibration and measurement.eldec==0: |
489 if measurement.is_calibration and measurement.eldec == 0: |
428 logger.info("Downloading ELDEC files.") |
490 logger.info("Downloading ELDEC files.") |
429 self.download_eldec(measurement_id) |
491 self.download_eldec(measurement_id) |
430 logger.info("--- Processing finished. ---") |
492 logger.info("--- Processing finished. ---") |
431 |
493 |
432 return measurement |
494 return measurement |
433 |
495 |
434 def get_measurement(self, measurement_id): |
496 def get_measurement(self, measurement_id): |
435 |
497 """ Get a measurement information from the SCC API. |
436 if measurement_id is None: # Is this still required? |
498 |
|
499 Parameters |
|
500 ---------- |
|
501 measurement_id : str |
|
502 The measurement ID to search. |
|
503 |
|
504 Returns |
|
505 ------- |
|
506 : Measurement object or None |
|
507 If the measurement is found, a Measurement object is returned. If not, it returns None |
|
508 """ |
|
509 # TODO: Consider to homogenize with get_ancillary method (i.e. always return a Measurement object). |
|
510 |
|
511 if measurement_id is None: # TODO: Is this still required? |
437 return None |
512 return None |
438 |
513 |
|
514 # Access the API |
439 measurement_url = self.api_measurement_pattern.format(measurement_id) |
515 measurement_url = self.api_measurement_pattern.format(measurement_id) |
440 logger.debug("Measurement API URL: %s" % measurement_url) |
516 logger.debug("Measurement API URL: %s" % measurement_url) |
441 |
517 |
442 response = self.session.get(measurement_url) |
518 response = self.session.get(measurement_url) |
443 |
519 |
753 return "%s: %s, %s" % (self.id, |
835 return "%s: %s, %s" % (self.id, |
754 self.filename, |
836 self.filename, |
755 self.status) |
837 self.status) |
756 |
838 |
757 |
839 |
|
840 # Methods that use the SCC class to perform specific tasks. |
758 def process_file(filename, system_id, settings, force_upload, delete_related, |
841 def process_file(filename, system_id, settings, force_upload, delete_related, |
759 delay=0, monitor=True, rs_filename=None, lr_filename=None, ov_filename=None): |
842 delay=0, monitor=True, rs_filename=None, lr_filename=None, ov_filename=None): |
760 """ Shortcut function to process a file to the SCC. """ |
843 """ Shortcut function to process a file to the SCC. """ |
761 logger.info("Processing file %s, using system %s" % (filename, system_id)) |
844 logger.info("Processing file %s, using system %s" % (filename, system_id)) |
762 |
845 |
931 def setup_download_measurements(parser): |
1015 def setup_download_measurements(parser): |
932 def download_measurements_from_args(parsed): |
1016 def download_measurements_from_args(parsed): |
933 download_measurements(parsed.IDs, parsed.max_retries, parsed.ignore_errors, parsed.config) |
1017 download_measurements(parsed.IDs, parsed.max_retries, parsed.ignore_errors, parsed.config) |
934 |
1018 |
935 parser.add_argument("IDs", help="Measurement IDs that should be downloaded.", nargs="+") |
1019 parser.add_argument("IDs", help="Measurement IDs that should be downloaded.", nargs="+") |
936 parser.add_argument("--max_retries", help="Number of times to retry in cases of missing measurement id.", default=0, type=int) |
1020 parser.add_argument("--max_retries", help="Number of times to retry in cases of missing measurement id.", default=0, |
937 parser.add_argument("--ignore_errors", help="Ignore errors when downloading multiple measurements.", action="store_false") |
1021 type=int) |
|
1022 parser.add_argument("--ignore_errors", help="Ignore errors when downloading multiple measurements.", |
|
1023 action="store_false") |
938 parser.set_defaults(execute=download_measurements_from_args) |
1024 parser.set_defaults(execute=download_measurements_from_args) |
939 |
1025 |
940 |
1026 |
941 def main(): |
1027 def main(): |
942 # Define the command line arguments. |
1028 # Define the command line arguments. |
943 parser = argparse.ArgumentParser() |
1029 parser = argparse.ArgumentParser() |
944 subparsers = parser.add_subparsers() |
1030 subparsers = parser.add_subparsers() |
945 |
1031 |
946 delete_parser = subparsers.add_parser("delete", help="Deletes a measurement.") |
1032 delete_parser = subparsers.add_parser("delete", help="Deletes a measurement.") |
947 rerun_all_parser = subparsers.add_parser("rerun-all", help="Rerun all processing steps for the provided measurement IDs.") |
1033 rerun_all_parser = subparsers.add_parser("rerun-all", |
|
1034 help="Rerun all processing steps for the provided measurement IDs.") |
948 rerun_processing_parser = subparsers.add_parser("rerun-elpp", |
1035 rerun_processing_parser = subparsers.add_parser("rerun-elpp", |
949 help="Rerun low-resolution processing steps for the provided measurement ID.") |
1036 help="Rerun low-resolution processing steps for the provided measurement ID.") |
950 upload_file_parser = subparsers.add_parser("upload-file", help="Submit a file and, optionally, download the output products.") |
1037 upload_file_parser = subparsers.add_parser("upload-file", |
|
1038 help="Submit a file and, optionally, download the output products.") |
951 list_parser = subparsers.add_parser("list", help="List measurements registered on the SCC.") |
1039 list_parser = subparsers.add_parser("list", help="List measurements registered on the SCC.") |
952 download_parser = subparsers.add_parser("download", help="Download selected measurements.") |
1040 download_parser = subparsers.add_parser("download", help="Download selected measurements.") |
953 |
1041 |
954 setup_delete(delete_parser) |
1042 setup_delete(delete_parser) |
955 setup_rerun_all(rerun_all_parser) |
1043 setup_rerun_all(rerun_all_parser) |