12Script to download the contents of a globaltag of the central database. 
   14This allows to use the payloads as a local payload directory or use it as a 
   15local database when running basf2. 
   26from urllib.parse 
import urljoin
 
   27from . 
import ConditionsDB, encode_name, file_checksum
 
   28from .cli_utils 
import ItemFilter
 
   29from .iov 
import IntervalOfValidity
 
   30from .local_metadata 
import LocalMetadataProvider
 
   31from basf2 
import B2ERROR, B2WARNING, B2INFO, LogLevel, LogInfo, logging
 
   33from concurrent.futures 
import ThreadPoolExecutor
 
   36def check_payload(destination, payloadinfo, run_range=None):
 
   37    """Return a list of all iovs for a given payload together with the file checksum and filenames. 
   40        destination (str): local folder where to download the payload 
   41        payloadinfo (dict): pyload information as returned by the REST API 
   42        run_range (b2conditions_db.iov.IntervalOfValidity, optional): Interval of validity . Defaults to None. 
   45        tuple: local file name, remote file name, checksum, list of iovs 
   48    payload = payloadinfo[
"payloadId"]
 
   49    module = payload[
"basf2Module"][
"name"]
 
   50    revision = int(payload[
"revision"])
 
   51    checksum = payload[
"checksum"]
 
   53    url = payload[
"payloadUrl"]
 
   54    base = payload[
"baseUrl"]
 
   55    local_file = os.path.join(destination, os.path.basename(url))
 
   56    remote_file = urljoin(base + 
"/", url)
 
   59    for iov 
in payloadinfo[
"payloadIovs"]:
 
   60        if run_range 
is not None:
 
   63                    iov[
"expStart"], iov[
"runStart"], iov[
"expEnd"], iov[
"runEnd"]
 
   64               ).intersect(run_range)
 
   68        iovlist.append([module, revision, iov[
"expStart"], iov[
"runStart"], iov[
"expEnd"], iov[
"runEnd"]])
 
   70    return (local_file, remote_file, checksum, iovlist)
 
   73def download_file(db, local_file, remote_file, checksum, iovlist=None):
 
   74    """Actually download the file""" 
   76    if os.path.exists(local_file):
 
   77        if file_checksum(local_file) == checksum:
 
   81            B2WARNING(f
"Checksum mismatch for {local_file}, downloading again")
 
   84    B2INFO(f
"download {local_file}")
 
   85    with open(local_file, 
"wb") 
as out:
 
   86        file_req = db._session.get(remote_file, stream=
True)
 
   87        if file_req.status_code != requests.codes.ok:
 
   88            B2ERROR(f
"Error downloading {file_req.url}: {file_req.status_code}")
 
   90        shutil.copyfileobj(file_req.raw, out)
 
   93    if file_checksum(local_file) != checksum:
 
   94        B2ERROR(f
"Checksum mismatch after download: {local_file}")
 
  100def download_payload(db, payload, directory):
 
  101    """Download a payload given a PayloadInformation object""" 
  102    remote = urljoin(payload.base_url, payload.payload_url)
 
  103    local = os.path.join(directory, payload.checksum[:2], f
"{payload.name}_r{payload.revision}.root")
 
  105        os.makedirs(os.path.dirname(local), exist_ok=
True)
 
  107        B2ERROR(f
"Cannot download payload: {e}")
 
  109    return download_file(db, local, remote, payload.checksum, iovlist=local)
 
  112def get_tagnames(db, patterns, use_regex=False):
 
  113    """Return a list of tags matching all patterns""" 
  114    all_tags = db.get_globalTags()
 
  118            tagnames = fnmatch.filter(all_tags, tag)
 
  121                tagname_regex = re.compile(tag, re.IGNORECASE)
 
  122            except Exception 
as e:
 
  123                B2ERROR(f
"--tag-regex: '{tag}' is not a valid regular expression: {e}")
 
  125            tagnames = (e 
for e 
in all_tags 
if tagname_regex.search(e))
 
  127        final |= set(tagnames)
 
  131def command_legacydownload(args, db=None):
 
  133    Download a globaltag from the database 
  135    This command allows to download a globaltag from the central database to be 
  136    used locally, either as lookup directory for payloads or as a standalone 
  137    local database if --create-dbfile is specified. 
  139    The command requires the TAGNAME to download and optionally an output 
  140    directory which defaults to centraldb in the local working directory. It 
  141    will check for existing payloads in the output directory and only download 
  142    payloads which are not present or don't have the expected checksum. 
  144    One can filter the payloads to be downloaded by payload name using the 
  145    --filter, --exclude and --regex options. 
  147    .. versionadded:: release-04-00-00 
  149       This has been renamed from ``download`` and is kept for compatibility 
  153       Downloading a globaltag should be done in the new format creating sqlite 
  154       database files. Please use this legacy tool only for downloading "small" 
  155       globaltags or very few payloads. 
  158    payloadfilter = ItemFilter(args)
 
  161        args.add_argument(
"tag", metavar=
"TAGNAME", default=
"production",
 
  162                          help=
"globaltag to download")
 
  163        args.add_argument(
"destination", nargs=
'?', metavar=
"DIR", default=
"centraldb",
 
  164                          help=
"directory to put the payloads into (default: %(default)s)")
 
  165        args.add_argument(
"-c", 
"--create-dbfile", default=
False, action=
"store_true",
 
  166                          help=
"if given save information about all payloads in DIR/database.txt")
 
  167        payloadfilter.add_arguments(
"payloads")
 
  168        args.add_argument(
"-j", type=int, default=1, dest=
"nprocess",
 
  169                          help=
"Number of concurrent connections to use for file " 
  170                          "download (default: %(default)s)")
 
  171        args.add_argument(
"--retries", type=int, default=3,
 
  172                          help=
"Number of retries on connection problems (default: " 
  174        args.add_argument(
"--run-range", nargs=4, default=
None, type=int,
 
  175                          metavar=(
"FIRST_EXP", 
"FIRST_RUN", 
"FINAL_EXP", 
"FINAL_RUN"),
 
  176                          help=
"Can be four numbers to limit the run range to be downloaded" 
  177                          "Only iovs overlapping, even partially, with this range will be downloaded.")
 
  178        group = args.add_mutually_exclusive_group()
 
  179        group.add_argument(
"--tag-pattern", default=
False, action=
"store_true",
 
  180                           help=
"if given, all globaltags which match the shell-style " 
  181                           "pattern TAGNAME will be downloaded: ``*`` stands for anything, " 
  182                           "``?`` stands for a single character. " 
  183                           "If -c is given as well the database files will be ``DIR/TAGNAME.txt``")
 
  184        group.add_argument(
"--tag-regex", default=
False, action=
"store_true",
 
  185                           help=
"if given, all globaltags matching the regular " 
  186                           "expression given by TAGNAME will be downloaded (see " 
  187                           "https://docs.python.org/3/library/re.html). " 
  188                           "If -c is given as well the database files will be ``DIR/TAGNAME.txt``")
 
  192        os.makedirs(args.destination, exist_ok=
True)
 
  194        B2ERROR(
"cannot create destination directory", file=sys.stderr)
 
  197    if not payloadfilter.check_arguments():
 
  200    run_range_str = f
' valid in {tuple(args.run_range)}' if args.run_range 
else '' 
  201    args.run_range = IntervalOfValidity(args.run_range) 
if args.run_range 
else None 
  204    for level 
in LogLevel.values.values():
 
  205        logging.set_info(level, LogInfo.LEVEL | LogInfo.MESSAGE | LogInfo.TIMESTAMP)
 
  207    tagnames = [args.tag]
 
  209    if args.tag_pattern 
or args.tag_regex:
 
  210        tagnames = get_tagnames(db, tagnames, args.tag_regex)
 
  213    for tagname 
in sorted(tagnames):
 
  215            req = db.request(
"GET", f
"/globalTag/{encode_name(tagname)}/globalTagPayloads",
 
  216                             f
"Downloading list of payloads for {tagname} tag{payloadfilter}{run_range_str}")
 
  217        except ConditionsDB.RequestError 
as e:
 
  222        for payload 
in req.json():
 
  223            name = payload[
"payloadId"][
"basf2Module"][
"name"]
 
  224            if payloadfilter.check(name):
 
  225                local_file, remote_file, checksum, iovlist = check_payload(args.destination, payload, args.run_range)
 
  227                    if local_file 
in download_list:
 
  228                        download_list[local_file][-1] += iovlist
 
  230                        download_list[local_file] = [local_file, remote_file, checksum, iovlist]
 
  234        with ThreadPoolExecutor(max_workers=args.nprocess) 
as pool:
 
  235            for iovlist 
in pool.map(
lambda x: download_file(db, *x), download_list.values()):
 
  240                full_iovlist += iovlist
 
  242        if args.create_dbfile:
 
  244            for iov 
in sorted(full_iovlist):
 
  245                dbfile.append(
"dbstore/{} {} {},{},{},{}\n".format(*iov))
 
  246            dbfilename = tagname 
if (args.tag_pattern 
or args.tag_regex) 
else "database" 
  247            with open(os.path.join(args.destination, dbfilename + 
".txt"), 
"w") 
as txtfile:
 
  248                txtfile.writelines(dbfile)
 
  251        B2ERROR(f
"{failed} out of {len(download_list)} payloads could not be downloaded")
 
  255def command_download(args, db=None):
 
  257    Download one or more payloads into a sqlite database for local use 
  259    This command allows to download the information from one or more globaltags 
  260    from the central database to be used locally. 
  262    The command requires at least one tag name to download. It will check for 
  263    existing payloads in the output directory and only download payloads which 
  264    are not present or don't have the expected checksum. 
  266    By default this script will create a local directory called ``conditions/`` 
  267    which contains a ``metadata.sqlite`` with all the payload information of all 
  268    selected globaltags and sub directories containing all the payload files. 
  270    This can be changed by specifying a different name for the metadata file 
  271    using the ``-o`` argument but the payloads will always be saved in sub 
  272    directories in the same directory as the sqlite file. 
  274    .. versionchanged:: release-04-00-00 
  276       Previously this command was primarily intended to download payloads for 
  277       one globaltag and optionally create a text file with payload information 
  278       as well as download all necessary file. This has been changed and will 
  279       now create a sqlite file containing the payload metadata. If you need the 
  280       old behavior please use the command ``b2conditionsdb-legacydownload`` 
  284        args.add_argument(
"tag", nargs=
"*", metavar=
"TAGNAME", help=
"globaltag to download")
 
  285        args.add_argument(
"-o", 
"--dbfile", metavar=
"DATABASEFILE", default=
"conditions/metadata.sqlite",
 
  286                          help=
"Name of the database file to create (default: %(default)s)")
 
  287        args.add_argument(
"-f", 
"--force", action=
"store_true", default=
False,
 
  288                          help=
"Don't ask permission if the output database file exists")
 
  289        args.add_argument(
"--append", action=
"store_true", default=
False,
 
  290                          help=
"Append to the existing database file if possible. " 
  291                          "Otherwise the content in the database file will be overwritten")
 
  292        group = args.add_mutually_exclusive_group()
 
  293        group.add_argument(
"--no-download", action=
"store_true", default=
False,
 
  294                           help=
"Don't download any payloads, just fetch the metadata information")
 
  295        group.add_argument(
"--only-download", action=
"store_true", default=
False,
 
  296                           help=
"Assume the metadata file is already filled, just make sure all payloads are downloaded")
 
  297        args.add_argument(
"--delete-extra-payloads", default=
False, action=
"store_true",
 
  298                          help=
"if given this script will delete all extra files " 
  299                          "that follow the payload naming convention ``AB/{name}_r{revision}.root`` " 
  300                          "if they are not referenced in the database file.")
 
  301        args.add_argument(
"--ignore-missing", action=
"store_true", default=
False,
 
  302                          help=
"Ignore missing globaltags and download all other tags")
 
  303        args.add_argument(
"-j", type=int, default=1, dest=
"nprocess",
 
  304                          help=
"Number of concurrent connections to use for file " 
  305                          "download (default: %(default)s)")
 
  306        args.add_argument(
"--retries", type=int, default=3,
 
  307                          help=
"Number of retries on connection problems (default: " 
  309        group = args.add_mutually_exclusive_group()
 
  310        group.add_argument(
"--tag-pattern", default=
False, action=
"store_true",
 
  311                           help=
"if given, all globaltags which match the shell-style " 
  312                           "pattern TAGNAME will be downloaded: ``*`` stands for anything, " 
  313                           "``?`` stands for a single character. ")
 
  314        group.add_argument(
"--tag-regex", default=
False, action=
"store_true",
 
  315                           help=
"if given, all globaltags matching the regular " 
  316                           "expression given by TAGNAME will be downloaded (see " 
  317                           "https://docs.python.org/3/library/re.html). ")
 
  321    if not args.only_download:
 
  322        if args.tag_regex 
or args.tag_pattern:
 
  323            args.tag = get_tagnames(db, args.tag, args.tag_regex)
 
  326            B2ERROR(
"No tags selected, cannot continue")
 
  329        def get_taginfo(tagname):
 
  330            """return the important information about all our globaltags""" 
  331            tag_info = db.get_globalTagInfo(tagname)
 
  333                B2ERROR(f
"Cannot find globaltag {tagname}")
 
  335            return tag_info[
'globalTagId'], tag_info[
'name'], tag_info[
'globalTagStatus'][
'name']
 
  338        with ThreadPoolExecutor(max_workers=args.nprocess) 
as pool:
 
  339            tags = list(pool.map(get_taginfo, args.tag))
 
  341        if not args.ignore_missing 
and None in tags:
 
  344        tags = sorted((e 
for e 
in tags 
if e 
is not None), key=
lambda tag: tag[1])
 
  345        taglist = [
"Selected globaltags:"]
 
  346        taglist += textwrap.wrap(
", ".join(tag[1] 
for tag 
in tags), width=get_terminal_width(),
 
  347                                 initial_indent=
" "*4, subsequent_indent=
" "*4)
 
  348        B2INFO(
'\n'.join(taglist))
 
  352    destination = os.path.relpath(os.path.dirname(os.path.abspath(args.dbfile)))
 
  354        os.makedirs(destination, exist_ok=
True)
 
  356        B2ERROR(f
"cannot create output directory,  {e}")
 
  359    if not os.path.exists(args.dbfile):
 
  362    elif not args.force 
and not args.only_download:
 
  364        query = input(f
"Database file {args.dbfile} exists, " + (
"overwrite" if not args.append 
else "append") + 
" (y/n) [n]? ")
 
  365        if query.lower().strip() 
not in [
'y', 
'yes']:
 
  366            B2ERROR(
"Output file exists, cannot continue")
 
  371        mode = 
"read" if args.only_download 
else (
"append" if args.append 
else "overwrite")
 
  372        database = LocalMetadataProvider(args.dbfile,  mode)
 
  374        if args.only_download:
 
  375            if database.get_payload_count() == 0:
 
  378    except Exception 
as e:
 
  379        B2ERROR(f
"Cannot open output file {args.dbfile}: {e}")
 
  383    with ThreadPoolExecutor(max_workers=args.nprocess) 
as pool:
 
  384        if not args.only_download:
 
  386            for tag_id, tag_name, tag_state, iovs 
in pool.map(
lambda x: x + (db.get_all_iovs(x[1]),), tags):
 
  387                B2INFO(f
"Adding metadata for {tag_name} to {args.dbfile}")
 
  388                database.add_globaltag(tag_id, tag_name, tag_state, iovs)
 
  395        downloader = functools.partial(download_payload, db, directory=destination)
 
  396        all_payloads = set(pool.map(downloader, database.get_payloads()))
 
  398        if args.delete_extra_payloads:
 
  399            existing_files = set()
 
  400            for dirname, subdirs, filenames 
in os.walk(destination):
 
  402                subdirs[:] = (e 
for e 
in subdirs 
if re.match(
'[0-9a-f]{2}', e))
 
  404                if dirname == destination:
 
  407                for filename 
in filenames:
 
  408                    if not re.match(
r"(.+)_r(\d+).root", filename):
 
  410                    existing_files.add(os.path.join(dirname, filename))
 
  412            extra_files = existing_files - all_payloads
 
  413            B2INFO(f
"Deleting {len(extra_files)} additional payload files")
 
  415            list(pool.map(os.remove, extra_files))
 
  417        return 1 
if None in all_payloads 
else 0