11#!/usr/bin/env python3
22import pycurl
33from io import BytesIO
4- import pycurl
54import ast
65import subprocess
76import pandas as pd
1211import json
1312import sys
1413import itertools
15- import json
1614import re
15+ import logging
16+
17+ # Configure logging
18+ logging .basicConfig (
19+ level = logging .INFO ,
20+ format = '[%(asctime)s - %(levelname)s] : %(message)s' ,
21+ handlers = [
22+ logging .FileHandler ('das-up-to-nevents.log' )
23+ ]
24+ )
25+ logger = logging .getLogger (__name__ )
26+
27+ # Constants to skip short runs and the beginning of each run
28+ MIN_RUN_LENGTH = 40
29+ SKIP_LUMIS = 20
1730
1831## Helpers
1932base_cert_url = "https://cms-service-dqmdc.web.cern.ch/CAF/certification/"
2033base_cert_eos = "/eos/user/c/cmsdqm/www/CAF/certification/"
2134base_cert_cvmfs = "/cvmfs/cms-griddata.cern.ch/cat/metadata/DC/"
2235
2336def get_url_clean (url ):
24-
37+ logger . debug ( f"Fetching URL: { url } " )
2538 buffer = BytesIO ()
2639 c = pycurl .Curl ()
2740 c .setopt (c .URL , url )
2841 c .setopt (c .WRITEDATA , buffer )
2942 c .perform ()
3043 c .close ()
31-
3244 return BeautifulSoup (buffer .getvalue (), "lxml" ).text
3345
3446def get_lumi_ranges (i ):
47+ logger .debug ("Calculating luminosity ranges" )
3548 result = []
3649 for _ , b in itertools .groupby (enumerate (i ), lambda pair : pair [1 ] - pair [0 ]):
3750 b = list (b )
38- result .append ([b [0 ][1 ],b [- 1 ][1 ]])
51+ result .append ([b [0 ][1 ], b [- 1 ][1 ]])
3952 return result
4053
41- def das_do_command (cmd ):
42- out = subprocess .check_output (cmd , shell = True , executable = "/bin/bash" ).decode ('utf8' )
43- return out .split ("\n " )
44-
4554def das_key (dataset ):
55+ logger .debug (f"Creating DAS key for dataset: { dataset } " )
4656 return 'dataset=' + dataset if "#" not in dataset else 'block=' + dataset
4757
48- def das_file_site (dataset , site ):
49- cmd = "dasgoclient --query='file %s site=%s'" % (das_key (dataset ),site )
50- out = das_do_command (cmd )
51- df = pd .DataFrame (out ,columns = ["file" ])
58+ def das_query (query ):
59+ cmd = f"dasgoclient"
60+ # For cms-bot deterministic caching see cms-sw#50101
61+ if "JENKINS_PREFIX" in os .environ :
62+ cmd = f"{ cmd } --limit=100 -unique"
63+ cmd = f"{ cmd } --query='{ query } '"
64+ logger .debug (f"Executing DAS query: { cmd } " )
65+ out = subprocess .check_output (cmd , shell = True , executable = "/bin/bash" ).decode ('utf8' )
66+ result = out .split ("\n " )
67+ logger .debug (f"Query result: { result } " )
68+ return result
5269
70+ def das_file_site (dataset , site ):
71+ out = das_query (f"file { das_key (dataset )} site={ site } " )
72+ df = pd .DataFrame (out , columns = ["file" ])
5373 return df
5474
55- def das_file_data (dataset ,opt = "" ):
56- cmd = "dasgoclient --query='file %s %s| grep file.name, file.nevents'" % (das_key (dataset ),opt )
57- out = das_do_command (cmd )
58- out = [np .array (r .split (" " ))[[0 ,3 ]] for r in out if len (r ) > 0 ]
59-
60- df = pd .DataFrame (out ,columns = ["file" ,"events" ])
75+ def das_file_data (dataset , opt = "" ):
76+ out = das_query (f"file { das_key (dataset )} { opt } | grep file.name, file.nevents" )
77+ out = [r .split (" " ) for r in out if len (r ) > 0 ]
78+ out = [[r [0 ], r [3 ]] for r in out if len (r ) > 3 ]
79+ df = pd .DataFrame (out , columns = ["file" , "events" ])
6180 df .events = df .events .values .astype (int )
62-
6381 return df
6482
65- def das_lumi_data (dataset ,opt = "" ):
66-
67- cmd = "dasgoclient --query='file,lumi,run %s %s'" % (das_key (dataset ),opt )
68-
69- out = das_do_command (cmd )
70- out = [r .split (" " ) for r in out if len (r )> 0 ]
71-
72- df = pd .DataFrame (out ,columns = ["file" ,"run" ,"lumis" ])
73-
83+ def das_lumi_data (dataset , opt = "" ):
84+ out = das_query (f"file,lumi,run { das_key (dataset )} { opt } " )
85+ out = [r .split (" " ) for r in out if len (r ) > 0 ]
86+ df = pd .DataFrame (out , columns = ["file" , "run" , "lumis" ])
7487 return df
7588
76- def das_run_events_data (dataset ,run ,opt = "" ):
77- cmd = "dasgoclient --query='file %s run=%s %s | sum(file.nevents) '" % (das_key (dataset ),run ,opt )
78- out = das_do_command (cmd )[0 ]
79-
80- out = [o for o in out .split (" " ) if "sum" not in o ]
81- out = int ([r .split (" " ) for r in out if len (r )> 0 ][0 ][0 ])
82-
83- return out
84-
85- def das_run_data (dataset ,opt = "" ):
86- cmd = "dasgoclient --query='run %s %s '" % (das_key (dataset ),opt )
87- out = das_do_command (cmd )
89+ def das_run_events_data (dataset , run , opt = "" ):
90+ out = das_query (f"file { das_key (dataset )} run={ run } { opt } | sum(file.nevents)" )
91+ out = out [0 ].split (" " )[- 1 ] if out else "0"
92+ return int (out )
8893
94+ def das_run_data (dataset , opt = "" ):
95+ out = das_query (f"run { das_key (dataset )} { opt } " )
8996 return out
9097
9198def no_intersection ():
99+ logger .error ("No intersection between JSON and dataset. Exiting." )
92100 print ("No intersection between:" )
93101 print (" - json : " , best_json )
94102 print (" - dataset: " , dataset )
@@ -102,22 +110,28 @@ def no_intersection():
102110 parser .add_argument ('--threshold' ,'-t' , help = "Event threshold per file" ,type = int ,default = - 1 )
103111 parser .add_argument ('--events' ,'-e' , help = "Tot number of events targeted" ,type = int ,default = - 1 )
104112 parser .add_argument ('--outfile' ,'-o' , help = 'Dump results to file' , type = str , default = None )
105- parser .add_argument ('--pandas' , '-pd' ,action = 'store_true' ,help = "Store the whole dataset (no event or threshold cut) in a csv" )
113+ parser .add_argument ('--pandas' , '-pd' ,action = 'store_true' ,help = "Store the whole dataset (no event or threshold cut) in a csv" )
106114 parser .add_argument ('--proxy' ,'-p' , help = 'Allow to parse a x509 proxy if needed' , type = str , default = None )
107115 parser .add_argument ('--site' ,'-s' , help = 'Only data at specific site' , type = str , default = None )
108116 parser .add_argument ('--lumis' ,'-l' , help = 'Output file for lumi ranges for the selected files (if black no lumiranges calculated)' , type = str , default = None )
109- parser .add_argument ('--precheck' ,'-pc' , action = 'store_true' , help = 'Check run per run before building the dataframes, to avoid huge caching.' )
110117 parser .add_argument ('--nogolden' ,'-ng' , action = 'store_true' , help = 'Do not crosscheck the dataset run and lumis with a Golden json for data certification' )
111118 parser .add_argument ('--run' ,'-r' , help = "Target a specific run" ,type = int ,default = None ,nargs = "+" )
119+ parser .add_argument ('--debug' , action = 'store_true' , help = 'Enable debug logging' )
112120 args = parser .parse_args ()
113121
122+ in_bot = "JENKINS_PREFIX" in os .environ
123+
124+ logging .getLogger ().setLevel (logging .DEBUG if args .debug or in_bot else logging .INFO )
125+
114126 if args .proxy is not None :
115127 os .environ ["X509_USER_PROXY" ] = args .proxy
128+ logger .debug (f"Set X509_USER_PROXY to { args .proxy } " )
116129 elif "X509_USER_PROXY" not in os .environ :
130+ logger .error ("No X509 proxy set. Exiting." )
117131 print ("No X509 proxy set. Exiting." )
118132 sys .exit (1 )
119-
120- ## Check if we are in the cms-bot "environment"
133+
134+ ## Check if we are in the cms-bot "environment"
121135 dataset = args .dataset
122136 events = args .events
123137 threshold = args .threshold
@@ -126,23 +140,29 @@ def no_intersection():
126140 lumis = args .lumis
127141 runs = args .run
128142 das_opt = ""
129-
143+
144+ logger .info (f"Dataset: { dataset } , Events: { events } , Threshold: { threshold } , Outfile: { outfile } , Site: { site } , Lumis: { lumis } , Runs: { runs } " )
145+
130146 if runs is not None :
131147 das_opt = "run in %s" % (str ([int (r ) for r in runs ]))
148+ logger .debug (f"DAS options for runs: { das_opt } " )
132149
133150 if not args .nogolden :
134-
151+ logger .debug ("Checking for golden JSON files" )
152+
135153 ## get the greatest golden json
136154 year = dataset .split ("Run" )[1 ][2 :4 ] # from 20XX to XX
137155 PD = dataset .split ("/" )[1 ]
138156 cert_type = "Collisions" + str (year )
139157 if "Cosmics" in dataset :
140158 cert_type = "Cosmics" + str (year )
141159 elif "Commisioning" in dataset :
142- cert_type = "Commisioning2020"
160+ cert_type = "Commisioning2020"
143161 elif "HI" in PD :
144162 cert_type = "Collisions" + str (year ) + "HI"
145-
163+
164+ logger .info (f"Certification type: { cert_type } " )
165+
146166 cvmfs_path = base_cert_cvmfs + cert_type + "/"
147167 eos_path = ""
148168 web_path = ""
@@ -151,37 +171,44 @@ def no_intersection():
151171 if os .path .isdir (cvmfs_path ):
152172 cvmfs_path = cvmfs_path + "/latest/"
153173 json_list_full = os .listdir (cvmfs_path )
174+ logger .info (f"Found JSON files in CVMFS: { json_list_full } " )
154175 ## ... if not we try eos ...
155176 if len (json_list_full )== 0 :
156177 eos_path = base_cert_eos + cert_type + "/"
157178 if os .path .isdir (eos_path ):
158179 json_list_full = os .listdir (eos_path )
180+ logger .info (f"Found JSON files in EOS: { json_list_full } " )
159181 ## ... if not we go to the website
160182 if len (json_list_full )== 0 :
161183 web_path = base_cert_url + cert_type + "/"
162184 json_list_full = get_url_clean (web_path ).split ("\n " )
185+ logger .info (f"Found JSON files on web: { json_list_full } " )
186+
163187 pattern = re .compile ("(cert_collisions\d{4}_\d*_\d*_golden.json)(\s|$)" , re .IGNORECASE )
164188 json_list = [match .group (1 ) for entry in json_list_full for match in [re .search (pattern , entry )] if match and match .group (1 )]
165189 if len (json_list )== 0 :
190+ logger .error (f"No matching JSON files found from { source } ({ path } ). The full list was:\n { list_full } " )
166191 raise RuntimeError ("No matching JSON files found from {source} ({path}). The full list was:\n {list_full}" .format (
167192 source = "web" if web_path else "eos" if eos_path else "cvmfs" ,
168193 path = web_path if web_path else eos_path if eos_path else cvmfs_path ,
169194 list_full = '\n ' .join (json_list_full ),
170195 ))
171196
172- # the larger the better, assuming file naming schema
197+ # the larger the better, assuming file naming schema
173198 # Cert_X_RunStart_RunFinish_Type.json
174199 # TODO if args.run keep golden only with right range
175200 run_ranges = [int (c .split ("_" )[- 2 ]) - int (c .split ("_" )[- 3 ]) for c in json_list ]
176201 latest_json = np .array (json_list [np .argmax (run_ranges )]).reshape (1 ,- 1 )[0 ].astype (str )
177202 best_json = str (latest_json [0 ])
203+ logger .info (f"Selected JSON file: { best_json } " )
204+
178205 if not web_path :
179206 with open ((eos_path if eos_path else cvmfs_path ) + "/" + best_json ) as js :
180207 golden = json .load (js )
181208 else :
182209 golden = get_url_clean (web_path + best_json )
183210 golden = ast .literal_eval (golden ) #converts string to dict
184-
211+
185212 # skim for runs in input
186213 if runs is not None :
187214 for k in golden :
@@ -193,7 +220,11 @@ def no_intersection():
193220 for k in golden :
194221 R = []
195222 for r in golden [k ]:
196- R = R + [f for f in range (r [0 ],r [1 ]+ 1 )]
223+ # skipping short runs
224+ if r [1 ]- r [0 ] + 1 < MIN_RUN_LENGTH :
225+ continue
226+
227+ R = R + [f for f in range (r [0 ]+ SKIP_LUMIS ,r [1 ]+ 1 )]
197228 golden_flat [k ] = R
198229
199230 # let's just check there's an intersection between the
@@ -206,26 +237,20 @@ def no_intersection():
206237
207238 # building the dataframe, cleaning for bad lumis
208239 golden_data_runs_tocheck = golden_data_runs
209-
210- if args .precheck :
211- golden_data_runs_tocheck = []
212- # Here we check run per run.
213- # This implies more dasgoclient queries, but smaller outputs
214- # useful when running the IB/PR tests not to have huge
215- # query results that have to be cached.
216- sum_events = 0
217- for r in golden_data_runs :
218- sum_events = sum_events + int (das_run_events_data (dataset ,r ))
219- golden_data_runs_tocheck .append (r )
220- if events > 0 and sum_events > events :
221- break
222- das_opt = "run in %s" % (str ([int (g ) for g in golden_data_runs_tocheck ]))
223-
240+
241+ if in_bot :
242+ best_run = golden_flat
243+ best_run , max_value = max ([(k ,golden_flat [k ]) for k in golden_data_runs_tocheck ], key = lambda x : len (set (x [1 ])))
244+ das_opt = "run in %s" % ([b for b in [best_run ]])
245+
246+ logger .debug (f"DAS options for runs: { das_opt } " )
247+
224248 df = das_lumi_data (dataset ,opt = das_opt ).merge (das_file_data (dataset ,opt = das_opt ),on = "file" ,how = "inner" ) # merge file informations with run and lumis
225249
226250 df ["lumis" ] = [[int (ff ) for ff in f .replace ("[" ,"" ).replace ("]" ,"" ).split ("," )] for f in df .lumis .values ]
227-
251+
228252 if not args .nogolden :
253+ logger .debug ("Filtering data based on golden JSON" )
229254
230255 df_rs = []
231256 for r in golden_data_runs_tocheck :
@@ -239,6 +264,7 @@ def no_intersection():
239264 if df_r ["events" ].sum () < threshold :
240265 continue
241266
267+ # taking only fully certified files, i.e. files for which all lumis are in the golden json
242268 good_lumis = np .array ([len ([ll for ll in l if ll in golden_flat [r ]]) for l in df_r .lumis ])
243269 n_lumis = np .array ([len (l ) for l in df_r .lumis ])
244270 df_rs .append (df_r [good_lumis == n_lumis ])
@@ -250,33 +276,38 @@ def no_intersection():
250276
251277 df .loc [:,"min_lumi" ] = [min (f ) for f in df .lumis ]
252278 df .loc [:,"max_lumi" ] = [max (f ) for f in df .lumis ]
253- df = df .sort_values (["run" ,"min_lumi" ,"max_lumi" ])
254-
279+ df .loc [:,"n_lumis" ] = df .loc [:,"max_lumi" ] - df .loc [:,"min_lumi" ] + 1
280+
281+ df = df .sort_values (["run" ,"n_lumis" ,"min_lumi" ,"max_lumi" ])
282+
255283 if site is not None :
256284 df = df .merge (das_file_site (dataset ,site ),on = "file" ,how = "inner" )
257285
258286 if args .pandas :
259287 df .to_csv (dataset .replace ("/" ,"" )+ ".csv" )
260288
261- if events > 0 :
289+ if events > 0 and not in_bot :
290+ logger .debug (f"Filtering files with more than { events } events" )
262291 df = df [df ["events" ] <= events ] #jump too big files
263292 df .loc [:,"sum_evs" ] = df .loc [:,"events" ].cumsum ()
264293 df = df [df ["sum_evs" ] < events ]
265-
294+
266295 files = df .file
267-
296+
268297 if lumis is not None :
298+ logger .debug (f"Saving luminosity ranges to { lumis } " )
269299 lumi_ranges = { int (r ) : list (get_lumi_ranges (np .sort (np .concatenate (df .loc [df ["run" ]== r ,"lumis" ].values ).ravel ()).tolist ())) for r in np .unique (df .run .values ).tolist ()}
270-
300+
271301 with open (lumis , 'w' ) as fp :
272302 json .dump (lumi_ranges , fp )
273303
274304 if outfile is not None :
305+ logger .debug (f"Saving file list to { outfile } " )
275306 with open (outfile , 'w' ) as f :
276307 for line in files :
277- f .write (f"{ line } \n " )
308+ f .write (f"{ line } \n " )
278309 else :
310+ logger .debug ("Outputting file list to console" )
279311 print ("\n " .join (files ))
280312
281313 sys .exit (0 )
282-
0 commit comments