ATLAS Offline Software
DQUtils/python/db.py
Go to the documentation of this file.
1 #! /usr/bin/env python
2 
3 # Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
4 
5 from __future__ import with_statement
6 
7 from logging import getLogger; log = getLogger("DQUtils.db")
8 
9 import re
10 import sys
11 from collections import namedtuple
12 from io import StringIO
13 from datetime import datetime
14 from keyword import iskeyword
15 from os.path import dirname
16 import six
17 
18 from CoolConvUtilities.AtlCoolLib import indirectOpen
19 
20 from .channel_mapping import make_channelselection, get_channel_ids_names
21 from .selection import make_browse_objects_selection
22 from .sugar import IOVSet, RunLumi, RunLumiType, TimestampType, make_iov_type
23 
24 DEFAULT_DBNAME = "CONDBR2"
25 
26 def make_safe_fields(fields):
27  return [(field + "_") if iskeyword(field) else field for field in fields]
28 
29 def get_query_range(since, until, runs):
30  """
31  Take `since`, `until` and `runs` and turn them into parameters.
32 
33  If nothing is specified, an infinite range is used.
34  If `runs` is an integer, just that run is used
35  If `runs` is a two-tuple, then it is used as a (from_run, to_run)
36  """
37  if runs and (since is None and until is None):
38  from builtins import int
39  if isinstance(runs, tuple):
40  since, until = (runs[0], 0), (runs[1], 0)
41 
42  elif isinstance(runs, int):
43  since, until = (runs, 0), (runs+1, 0)
44  else:
45  raise RuntimeError("Invalid type for `runs`, should be int or tuple")
46 
47  elif runs:
48  raise RuntimeError("Specify (since and/or until), OR runs, not both")
49 
50  else:
51  if since is None: since = 0
52  if until is None: until = 2**63-1
53 
54  if isinstance(since, tuple): since = RunLumi(*since)
55  if isinstance(until, tuple): until = RunLumi(*until)
56 
57  if isinstance(since, str): since = TimestampType.from_string(since)
58  if isinstance(until, str): until = TimestampType.from_string(until)
59 
60  if isinstance(since, datetime): since = TimestampType.from_date(since)
61  if isinstance(until, datetime): until = TimestampType.from_date(until)
62 
63  assert since <= until, "Bad query range (since > until?)"
64 
65  return since, until
66 
67 def fetch_iovs(folder_name, since=None, until=None, channels=None, tag="",
68  what="all", max_records=-1, with_channel=True, loud=False,
69  database=None, convert_time=False, named_channels=False,
70  selection=None, runs=None, with_time=False, unicode_strings=False):
71  """
72  Helper to fetch objects in a pythonic manner
73  `folder_name` may be an abbreviated name (DQMFONL) or a fully-qualified name
74  (e.g. /GLOBAL/DETSTATUS/DQMFONL)
75  `since`, `until` can be (run, lumi) tuples, or standard iov keys
76  `channels` can be a cool ChannelSelection object or a list of ids/names
77  `tag` COOL folder tag
78  `what` is a list of strings specifying which records should be fetched
79  if it is the string "all" (not a list), then all records are fetched,
80  and naming is turned on.
81  `max_records` specifies the maximum number of records to fetch. -1 means all
82  `with_channel` specifies whether the channel number should be in the result
83  list of each tuple
84  `loud` specifies whether quick_retrieve (C++ function) should print its
85  status every 1000 objects
86  `database` can be used to specify an abbreviated database, or a connection
87  string
88  `convert_time` performs a conversion of `since` and `until` from runlumi
89  to nanoseconds since the epoch.
90  `named_channels` causes the iovs returned to contain strings in the channel
91  identifier
92  `selection` [NOT IMPLEMENTED YET] create a cool selection object
93  `runs` if it is an integer, it is a run number. If it is a tuple, it is a
94  run range.
95  `with_time` retrieve insertiontime for iovs
96  `unicode_strings` return unicode string objects, assuming database content
97  is UTF-8
98  """
99  from .quick_retrieve import quick_retrieve, browse_coracool, get_coracool_payload_spec
100 
101  if channels == []: return IOVSet()
102 
103  since, until = get_query_range(since, until, runs)
104 
105  channel_mapping = None
106  if isinstance(folder_name, str):
107  folder = Databases.get_folder(folder_name, database)
108  else:
109  try:
110  folder = folder_name
111  folder_name = folder.fullPath()
112  except Exception:
113  log.error("Exception when interpreting folder: {0}".format(folder_name))
114  raise
115 
116  log.info("Querying %s", folder_name)
117  log.debug("Query range: [%s, %s]", since, until)
118 
119  short_folder = folder.fullPath().split("/")[-1]
120 
121  time_based_folder = "<timeStamp>time</timeStamp>" in folder.description()
122  coracool_folder = "<coracool>" in folder.description()
123  iov_key_type = TimestampType if time_based_folder else RunLumiType
124 
125  if time_based_folder and (convert_time or runs):
126  # Perform a conversion of the run IoV to a time-based one.
127  # Note: probably inadvisable to do this for long ranges since
128  # it has to retrieve all of the luminosity blocks that took place
129  # in the query range.
130 
131  until = min(until, RunLumi(100000000, 0))
132  runrange = fetch_iovs("LBLB", since, until)
133  if runrange:
134  # If the runrange is empty for some reason, fall back.
135  since, until = runrange.first.StartTime, runrange.last.EndTime
136  return fetch_iovs(folder_name, since, until, channels, tag, what,
137  max_records, with_channel, loud,
138  database, convert_time=False,
139  named_channels=named_channels, selection=selection,
140  with_time=with_time,
141  unicode_strings=unicode_strings)
142  else:
143  return IOVSet()
144 
145  detstatus_names = "DQMFOFL", "DCSOFL", "DQMFONL", "SHIFTOFL", "SHIFTONL", "LBSUMM"
146  if any(short_folder.endswith(x) for x in detstatus_names):
147  channel_mapping = None # get channel mapping from channel_mapping.py
148  else:
149  _, _, channelmap = get_channel_ids_names(folder)
150  cm_reversed = dict((value, key) for key, value in six.iteritems(channelmap))
151  channelmap.update(cm_reversed)
152  channel_mapping = channelmap
153 
154  channels = make_channelselection(channels, channel_mapping)
155 
156  field_name = "%s_VAL" % short_folder
157 
158  if not coracool_folder:
159  if what == "all":
160  what = folder.folderSpecification().payloadSpecification().keys()
161 
162  channelmap = None
163  if named_channels:
164  _, _, channelmap = get_channel_ids_names(folder)
165 
166  folder.setPrefetchAll(False)
167 
168  if selection:
169  sel = make_browse_objects_selection(folder, selection)
170  iterator = folder.browseObjects(since, until, channels, tag, sel)
171  else:
172  iterator = folder.browseObjects(since, until, channels, tag)
173 
174  fields = []
175  if with_time:
176  fields.append("insertion_time")
177  if with_channel:
178  fields.append("channel")
179  fields.extend(what)
180 
181  record = make_iov_type(field_name, make_safe_fields(fields))
182 
183  result = quick_retrieve(iterator, record, what, max_records, with_channel,
184  loud, iov_key_type, channelmap, with_time,
185  unicode_strings)
186 
187  else:
188  args = folder_name, database
189  database, folder_path = Databases.resolve_folder_string(*args)
190 
191  # Coracool
192  assert database, "Coracool folder - you must specify a database"
193 
194  db = Databases.get_instance(database)
195  spec = get_coracool_payload_spec(db, folder_path)
196  if what == "all":
197  what = spec.keys()
198 
199  assert isinstance(what, list), ("Coracool folder - you must specify "
200  "`what` to fetch (it cannot be inferred, as with non-coracool.)")
201 
202  record = make_iov_type(field_name, ["channel", "elements"])
203 
204  element = namedtuple("element", " ".join(make_safe_fields(what)))
205 
206  result = browse_coracool(db, folder_path, since, until, channels, "",
207  what, record, element, iov_key_type)
208 
209  result = IOVSet(result, iov_type=record, origin=short_folder)
210 
211  return result
212 
213 def write_iovs(folder_name, iovs, record, multiversion=True, tag="",
214  create=False, storage_buffer=False):
215  args = folder_name, multiversion, record, create
216  db, folder, payload = Databases.fetch_for_writing(*args)
217 
218  if storage_buffer:
219  folder.setupStorageBuffer()
220 
221  total_iovs = len(iovs)
222  for i, iov in enumerate(iovs):
223  for field_name, field_value in zip(iov._fields[3:], iov[3:]):
224  payload[field_name] = field_value
225 
226  folder.storeObject(iov.since, iov.until, payload, iov.channel, tag)
227  if not i % 1000:
228  log.debug("Wrote %5i / %5i records", i, total_iovs)
229 
230  if storage_buffer:
231  log.debug("Flushing records to database...")
232  folder.flushStorageBuffer()
233  log.debug("... done")
234 
236  """
237  Databases helper class. Used as a singleton. (don't instantiate)
238  Specifies abbreviations for database connection strings and folders
239  """
240 
241  FOLDERS = {
242  "DQMFONL" : "COOLONL_GLOBAL::/GLOBAL/DETSTATUS/DQMFONL",
243  "DQMFONLLB" : "COOLONL_GLOBAL::/GLOBAL/DETSTATUS/DQMFONLLB",
244  "SHIFTONL" : "COOLONL_GLOBAL::/GLOBAL/DETSTATUS/SHIFTONL",
245 
246  "DQMFOFL" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/DQMFOFL",
247  "DCSOFL" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/DCSOFL",
248  "DQCALCOFL" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/DQCALCOFL",
249  "SHIFTOFL" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/SHIFTOFL",
250  "LBSUMM" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/LBSUMM",
251  "VIRTUALFLAGS" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/VIRTUALFLAGS",
252 
253  "DEFECTS" : "COOLOFL_GLOBAL::/GLOBAL/DETSTATUS/DEFECTS",
254 
255  "SOR_Params" : "COOLONL_TDAQ::/TDAQ/RunCtrl/SOR_Params",
256  "EOR_Params" : "COOLONL_TDAQ::/TDAQ/RunCtrl/EOR_Params",
257  "SOR" : "COOLONL_TDAQ::/TDAQ/RunCtrl/SOR",
258  "EOR" : "COOLONL_TDAQ::/TDAQ/RunCtrl/EOR",
259 
260  "LBLB" : "COOLONL_TRIGGER::/TRIGGER/LUMI/LBLB",
261  "LBTIME" : "COOLONL_TRIGGER::/TRIGGER/LUMI/LBTIME",
262  "LBLESTONL" : "COOLONL_TRIGGER::/TRIGGER/LUMI/LBLESTONL",
263  "LVL1COUNTERS" : "COOLONL_TRIGGER::/TRIGGER/LUMI/LVL1COUNTERS",
264  "HLTCOUNTERS" : "COOLOFL_TRIGGER::/TRIGGER/LUMI/HLTCOUNTERS",
265  "HLT/Menu" : "COOLONL_TRIGGER::/TRIGGER/HLT/Menu",
266  "LVL1/Menu" : "COOLONL_TRIGGER::/TRIGGER/LVL1/Menu",
267  "Prescales" : "COOLONL_TRIGGER::/TRIGGER/LVL1/Prescales",
268  }
269 
270  @classmethod
271  def resolve_folder_string(cls, folder_name, db_override=None):
272  """
273  Resolves a simplified folder URI.
274 
275  Examples:
276  folder_name == "SHIFTOFL"
277  will give a connection to COOLOFL_GLOBAL/COMP200
278  folder /GLOBAL/DETSTATUS/SHIFTOFL
279  folder_name == "test.db::SHIFTOFL"
280  will give a connection to an sqlite database test.db
281 
282  Priority:
283  * Database specified in db_override
284  * Database specified in `folder_name`
285  * Database specified in cls.FOLDERS[folder_name]
286  """
287  res_database = db_override
288 
289  # First check - is a database specified in the folder name?
290  if "::" in folder_name:
291  assert folder_name.count("::") == 1, "Bad folder format"
292  database, folder_name = folder_name.split("::")
293 
294  # If res_database hasn't already been set, do so
295  res_database = database if not res_database else res_database
296 
297  if folder_name in cls.FOLDERS:
298  database, res_folder = cls.FOLDERS[folder_name].split("::")
299  res_database = database if not res_database else res_database
300  else:
301  res_folder = folder_name
302 
303  return res_database, res_folder
304 
305  @classmethod
306  def get_folder(cls, folder_name, db_override=None, read_only=True,
307  create_function=None, also_db=False):
308  """
309  Retrieve a folder. The `db_override` parameter allows over-riding the
310  database which comes from the folder string.
311 
312  Parameters:
313  `folder_name` : Folder name or alias to load
314  `db_override` : If specified, causes an alternate database string
315  to be used.
316  `read_only` : Specifies if a read-only database connection should
317  be used.
318  `create_function` : If specified, function to be called in case the
319  folder doesn't exist. It is passed the database
320  connection.
321  `also_db` : Make the return value (db, folder)
322  """
323 
324  if read_only:
325  assert not create_function, "`read_only` specified with `create`"
326 
327  database, folder = cls.resolve_folder_string(folder_name, db_override)
328  assert database, ("Unable to resolve database for (%s, %s)"
329  % (folder_name, db_override))
330 
331  create = bool(create_function)
332  db = cls.get_instance(database, read_only, create)
333 
334  try:
335  cool_folder = db.getFolder(folder)
336  except Exception as error:
337  log.debug('HELP! %s', error.args)
338  args = str(error)
339  log.debug('THIS IS %s', type(args))
340  log.debug('Value of boolean: %s', ("not found" in args))
341  if not ("cannot be established" in args or
342  "not found" in args
343  ):
344  log.exception("Unknown error encountered whilst opening "
345  "database connection to '%s'",
346  database)
347  raise
348 
349  if not create_function:
350  log.exception("Database does not exist, `create_function` not "
351  "specified")
352  raise
353 
354  cool_folder = create_function(db)
355  if also_db:
356  return db, cool_folder
357  return cool_folder
358 
359  @classmethod
360  def resolve_db_string(cls, db_string, read_only=True):
361  """
362  Take a database string - if it looks like a filename ending in ".db"
363  then assume it is a sqlite database with that name.
364 
365  If the `db_string` is prepended with "WRITE|" then a writable connection
366  is requested.
367 
368  If the db_string doesn't contain a "/", then "/" + DEFAULT_DBNAME is
369  appended.
370  """
371  if "://" in db_string:
372  # Assume that the string is already resolved
373  return db_string, read_only
374 
375  if db_string.startswith("WRITE|"):
376  assert db_string.count("|") == 1, "Bad db_string format"
377  db_string = db_string.split("|")[1]
378  read_only = False
379 
380  sqlite_regex = re.compile(r"(?P<filename>.*?\.db)(?:/?(?P<dbname>[^/]+))?$")
381  matched = sqlite_regex.match(db_string)
382  if matched:
383  filename, dbname = matched.groups()
384  dbname = DEFAULT_DBNAME if not dbname else dbname
385  db_string = "sqlite://schema=%s;dbname=%s" % (filename, dbname)
386  else:
387  if "/" not in db_string:
388  return db_string + "/" + DEFAULT_DBNAME, read_only
389 
390  return db_string, read_only
391 
392  @classmethod
393  def get_instance(cls, db_string, read_only=True, create=False):
394  """
395  Open a database connection
396  """
397  res_db_string, read_only = cls.resolve_db_string(db_string, read_only)
398 
399  try:
400  prev_stdout = sys.stdout
401  sys.stdout = StringIO()
402  try:
403  connection = indirectOpen(res_db_string, readOnly=read_only)
404  finally:
405  sys.stdout = prev_stdout
406  except Exception as e:
407  if ((e.args and "The database does not exist" in e.args[0]) or
408  str(e).find ('The database does not exist') >= 0) and not create:
409  log.info("Failed trying to connect to '%s'", res_db_string)
410  raise
411  from PyCool import cool
412  dbService = cool.DatabaseSvcFactory.databaseService()
413  connection = dbService.createDatabase(res_db_string)
414  return connection
415 
416  @classmethod
417  def build_folder(cls, db, folder_name, multiversion, record):
418  """
419  Create `folder_name` on database instance `db`, with recordspecification
420  `record`.
421 
422  Also creates folderset to which folder_name belongs if necessary.
423  """
424  from PyCool import cool
425 
426  folderset_path = dirname(folder_name)
427  try:
428  db.getFolderSet(folderset_path)
429  except Exception as error:
430  caught_error = "Folder set %s not found" % folderset_path
431  if caught_error not in error.args[0]:
432  raise
433 
434  log.debug("Folderset doesn't exist - creating it.")
435  db.createFolderSet(folderset_path, "", True)
436 
437  if not isinstance(record, cool.RecordSpecification):
438  record_spec = cool.RecordSpecification()
439  for field in record:
440  record_spec.extend(*field)
441  else:
442  record_spec = record
443 
444  FV = cool.FolderVersioning
445  versioning = FV.MULTI_VERSION if multiversion else FV.SINGLE_VERSION
446  folder_spec = cool.FolderSpecification(versioning, record_spec)
447  folder = db.createFolder(folder_name, folder_spec)
448  payload = cool.Record(record_spec)
449 
450  return folder, payload
451 
452  @classmethod
453  def fetch_for_writing(cls, orig_folder_name, multiversion=True, record=None,
454  create=False, db_override=None):
455  """
456  Retrieve a folder for writing. Creates it if it doesn't exist.
457 
458  `folder_name` specifies folder to be queried
459  `multiversion` specifies COOL versioning mode
460  `record` is a list of fields to be created in the form:
461  [("<field name>", cool.StorageType.<field type>), ...]
462  or if None, defaults to one Code record,
463  or if isinstance(record, cool.RecordSpecification), uses this.
464  `create` should the database be created if it doesn't
465  `db_override` overrides automatic detection of database string
466  """
467  from PyCool import cool
468  if record is None:
469  record = [("Code", cool.StorageType.Int32)]
470 
471  database, folder_name = cls.resolve_folder_string(orig_folder_name)
472  if db_override:
473  database = db_override
474 
475  try:
476  db = cls.get_instance(database, False)
477 
478  except Exception as error:
479  if not create or "The database does not exist" not in error.args[0]:
480  raise
481 
482  from PyCool import cool
483  dbService = cool.DatabaseSvcFactory.databaseService()
484 
485  resolved_database, _ = cls.resolve_db_string(database)
486 
487  log.info("Database doesn't exist - creating it.")
488  db = dbService.createDatabase(resolved_database)
489 
490  try:
491  folder = db.getFolder(folder_name)
492  payload = cool.Record(folder.payloadSpecification())
493  except Exception as error:
494  if not create or "Folder %s not found" % folder_name not in error.args[0]:
495  raise
496 
497  log.debug("Folder doesn't exist - creating it.")
498  args = db, folder_name, multiversion, record
499  folder, payload = cls.build_folder(*args)
500 
501  return db, folder, payload
python.db.get_query_range
def get_query_range(since, until, runs)
Definition: DQUtils/python/db.py:29
python.db.make_safe_fields
def make_safe_fields(fields)
Definition: DQUtils/python/db.py:26
vtune_athena.format
format
Definition: vtune_athena.py:14
python.db.fetch_iovs
def fetch_iovs(folder_name, since=None, until=None, channels=None, tag="", what="all", max_records=-1, with_channel=True, loud=False, database=None, convert_time=False, named_channels=False, selection=None, runs=None, with_time=False, unicode_strings=False)
Definition: DQUtils/python/db.py:67
python.db.Databases.FOLDERS
dictionary FOLDERS
Definition: DQUtils/python/db.py:241
python.channel_mapping.make_channelselection
def make_channelselection(cs, mapping=None)
Definition: channel_mapping.py:81
min
constexpr double min()
Definition: ap_fixedTest.cxx:26
dirname
std::string dirname(std::string name)
Definition: utils.cxx:200
python.channel_mapping.get_channel_ids_names
def get_channel_ids_names(folder)
Definition: channel_mapping.py:102
python.db.Databases.resolve_folder_string
def resolve_folder_string(cls, folder_name, db_override=None)
Definition: DQUtils/python/db.py:271
python.sugar.runlumi.RunLumi
RunLumi
Definition: runlumi.py:131
python.db.Databases.get_instance
def get_instance(cls, db_string, read_only=True, create=False)
Definition: DQUtils/python/db.py:393
python.db.Databases.get_folder
def get_folder(cls, folder_name, db_override=None, read_only=True, create_function=None, also_db=False)
Definition: DQUtils/python/db.py:306
python.db.Databases.resolve_db_string
def resolve_db_string(cls, db_string, read_only=True)
Definition: DQUtils/python/db.py:360
python.db.write_iovs
def write_iovs(folder_name, iovs, record, multiversion=True, tag="", create=False, storage_buffer=False)
Definition: DQUtils/python/db.py:213
python.quick_retrieve.browse_coracool
browse_coracool
Definition: quick_retrieve.py:17
TCS::join
std::string join(const std::vector< std::string > &v, const char c=',')
Definition: Trigger/TrigT1/L1Topo/L1TopoCommon/Root/StringUtils.cxx:10
python.selection.make_browse_objects_selection
def make_browse_objects_selection(folder, selection)
Definition: selection.py:82
python.db.Databases
Definition: DQUtils/python/db.py:235
python.quick_retrieve.quick_retrieve
quick_retrieve
Definition: quick_retrieve.py:16
python.CaloScaleNoiseConfig.type
type
Definition: CaloScaleNoiseConfig.py:78
python.db.Databases.fetch_for_writing
def fetch_for_writing(cls, orig_folder_name, multiversion=True, record=None, create=False, db_override=None)
Definition: DQUtils/python/db.py:453
python.sugar.iovtype.make_iov_type
def make_iov_type(name, variables, bases=(IOVType,), _memoized={})
Definition: iovtype.py:114
pickleTool.object
object
Definition: pickleTool.py:30
str
Definition: BTagTrackIpAccessor.cxx:11
python.Bindings.keys
keys
Definition: Control/AthenaPython/python/Bindings.py:798
python.quick_retrieve.get_coracool_payload_spec
get_coracool_payload_spec
Definition: quick_retrieve.py:18
python.db.Databases.build_folder
def build_folder(cls, db, folder_name, multiversion, record)
Definition: DQUtils/python/db.py:417
xAOD::bool
setBGCode setTAP setLVL2ErrorBits bool
Definition: TrigDecision_v1.cxx:60
python.AtlCoolLib.indirectOpen
def indirectOpen(coolstr, readOnly=True, debug=False)
Definition: AtlCoolLib.py:130
Trk::split
@ split
Definition: LayerMaterialProperties.h:38
python.CaloCondLogger.getLogger
def getLogger(name="CaloCond")
Definition: CaloCondLogger.py:16