Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoSign in
Socket

apd

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

apd - pypi Package Compare versions

Comparing version
0.5.0
to
0.6.0
+20
-25
PKG-INFO
Metadata-Version: 2.1
Name: apd
Version: 0.5.0
Version: 0.6.0
Summary: Tool to access the Analysis production Data

@@ -19,3 +19,2 @@ License: BSD-3-Clause

Usage

@@ -35,17 +34,11 @@ =====

In [9]: datasets = apd.AnalysisData("SL", "RDs")
In [9]: datasets = apd.get_analysis_data("SL", "RDs")
In [10]: datasets(datatype="2012")
In [10]: datasets(datatype="2012", polarity="magdown")
Out[10]:
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000006_1.bsntuple_mc.root']
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root']

@@ -60,13 +53,9 @@ In [11]:

$ apd-list-pfns SL RDs --datatype=2011 --datatype=2016 --polarity=magdown
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000004_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000005_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000006_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000007_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root'
The *apd-cache* command allows caching the Analysis metadata to a

@@ -87,2 +76,8 @@ specific location.

BEWARE: The endpoint is experimental and can be interrupted at any time.
Further information
===================
See:
https://lhcb-ap.docs.cern.ch/user_guide/accessing_output.html

@@ -6,3 +6,2 @@ Analysis Production Data

Usage

@@ -22,17 +21,11 @@ =====

In [9]: datasets = apd.AnalysisData("SL", "RDs")
In [9]: datasets = apd.get_analysis_data("SL", "RDs")
In [10]: datasets(datatype="2012")
In [10]: datasets(datatype="2012", polarity="magdown")
Out[10]:
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000006_1.bsntuple_mc.root']
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root']

@@ -47,13 +40,9 @@ In [11]:

$ apd-list-pfns SL RDs --datatype=2011 --datatype=2016 --polarity=magdown
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000004_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000005_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000006_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000007_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root'
The *apd-cache* command allows caching the Analysis metadata to a

@@ -74,2 +63,8 @@ specific location.

BEWARE: The endpoint is experimental and can be interrupted at any time.
Further information
===================
See:
https://lhcb-ap.docs.cern.ch/user_guide/accessing_output.html
Metadata-Version: 2.1
Name: apd
Version: 0.5.0
Version: 0.6.0
Summary: Tool to access the Analysis production Data

@@ -19,3 +19,2 @@ License: BSD-3-Clause

Usage

@@ -35,17 +34,11 @@ =====

In [9]: datasets = apd.AnalysisData("SL", "RDs")
In [9]: datasets = apd.get_analysis_data("SL", "RDs")
In [10]: datasets(datatype="2012")
In [10]: datasets(datatype="2012", polarity="magdown")
Out[10]:
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
['root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000002_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000003_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000004_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000005_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110972/0000/00110972_00000006_1.bsntuple_mc.root']
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root',
'root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root']

@@ -60,13 +53,9 @@ In [11]:

$ apd-list-pfns SL RDs --datatype=2011 --datatype=2016 --polarity=magdown
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2011/BSNTUPLE_MC.ROOT/00110968/0000/00110968_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000001_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000002_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000003_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000004_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000005_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000006_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2016/BSNTUPLE_MC.ROOT/00110984/0000/00110984_00000007_1.bsntuple_mc.root
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000002_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000005_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000003_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000001_1.bsntuple_mc.root'
root://eoslhcb.cern.ch//eos/lhcb/grid/prod/lhcb/MC/2012/BSNTUPLE_MC.ROOT/00110970/0000/00110970_00000004_1.bsntuple_mc.root'
The *apd-cache* command allows caching the Analysis metadata to a

@@ -87,2 +76,8 @@ specific location.

BEWARE: The endpoint is experimental and can be interrupted at any time.
Further information
===================
See:
https://lhcb-ap.docs.cern.ch/user_guide/accessing_output.html

@@ -11,2 +11,9 @@ ###############################################################################

###############################################################################
"""Analysis Production Data package.
Programmatic interface to the Analysis Productions database,
that allows retrieving information about the samples produced. It queries a
REST endpoint provided by the Web application, and caches the data locally.
"""
__all__ = [

@@ -13,0 +20,0 @@ "AnalysisData",

###############################################################################
# (c) Copyright 2021-2022 for the benefit of the LHCb Collaboration #
# (c) Copyright 2021-2023 for the benefit of the LHCb Collaboration #
# #

@@ -11,3 +11,10 @@ # This software is distributed under the terms of the GNU General Public #

###############################################################################
"""Interface to the Analysis Production data.
Provides:
* the get_analysis_data method, the principal way to lookup AP info. It returns
and AnalysisData class.
* the AnalysisData class, which allows querying information about Analysis Productions
"""
import copy

@@ -17,7 +24,9 @@ import itertools

import os
from pathlib import Path
from apd.ap_info import (
InvalidCacheError,
SampleCollection,
check_tag_in_list,
fetch_ap_info,
cache_ap_info,
check_tag_value_possible,
iterable,

@@ -33,5 +42,54 @@ load_ap_info,

APD_METADATA_LIFETIME = "APD_METADATA_LIFETIME"
APD_METADATA_LIFETIME_DEFAULT = 600
APD_DATA_CACHE_DIR = "APD_DATA_CACHE_DIR"
def _load_and_setup_cache(
cache_dir, working_group, analysis, ap_date=None, api_url="https://lbap.app.cern.ch"
):
"""Utility function that checks whether the data for the Analysis
is cached already and does it if needed."""
env_cache = os.environ.get(APD_METADATA_CACHE_DIR)
if not cache_dir:
if env_cache:
cache_dir = env_cache
else:
cache_dir = Path.home() / ".cache" / "apd"
logger.debug("Cache directory not set, using %s", cache_dir)
samples = None
try:
lifetime = os.environ.get(APD_METADATA_LIFETIME, APD_METADATA_LIFETIME_DEFAULT)
samples, _ = load_ap_info(
cache_dir,
working_group,
analysis,
ap_date=ap_date,
maxlifetime=lifetime,
)
except FileNotFoundError:
logger.debug(
"Caching information for %s/%s to %s for time %s",
working_group,
analysis,
cache_dir,
ap_date,
)
samples = cache_ap_info(
cache_dir, working_group, analysis, ap_date=ap_date, api_url=api_url
)
except InvalidCacheError:
logger.debug(
"Invalid cache. reloading information for %s/%s to %s for time %s",
working_group,
analysis,
cache_dir,
ap_date,
)
samples = cache_ap_info(
cache_dir, working_group, analysis, ap_date=ap_date, api_url=api_url
)
assert samples is not None
return samples
def _validate_tags(tags, default_tags=None, available_tags=None):

@@ -41,7 +99,20 @@ """Method that checks the dictionary of tag names, values that should be used

- Special cases are handled: tags "name" and "version" as well as "data" and "mc"
(which are converted to a "config" value).
- tag values cannot be None
- tag values cannot be of type bytes
- int tag values are converted to string
Note:
- Special cases are handled: tags "name" and "version" as well as "data" and "mc"
(which are converted to a "config" value).
- Tag values cannot be None.
- Tag values cannot be of type bytes.
- Int tag values are converted to string.
Args:
tags (dict): the dictionary of tags to be validated.
default_tags (dict, optional): provide default tags. Defaults to None.
available_tags (list, optional): provide a list of available tags. Defaults to None.
Raises:
ValueError: see the Note above.
TypeError: see the Note above.
Returns:
dict: the validated tags.
"""

@@ -59,10 +130,2 @@

# name and version are special tags in our case, we check their validity
if "name" in effective_tags:
raise ValueError("name is not a supported tag in AnalysisData objects")
version = effective_tags.get("version", None)
if version and iterable(version):
raise ValueError("version argument doesn't support iterables")
# Special handling for the data and mc tags to avoid having to

@@ -120,3 +183,5 @@ # use the config tag

if available_tags is not None:
check_tag_in_list(t, available_tags)
# NB this raises an exception if the tag is not in the list
# or if the value does not match any samples
check_tag_value_possible(t, v, available_tags)
if isinstance(v, int) and not isinstance(v, bool):

@@ -129,3 +194,3 @@ cleaned[t] = str(v)

def sample_check(samples, tags):
def _sample_check(samples, tags):
"""Filter the SampleCollection and check that we have the

@@ -168,2 +233,3 @@ samples that we expect"""

# Map contains AnalysisData objects already loaded
__analysis_map = {}

@@ -181,3 +247,8 @@

):
"""Cache with the same process"""
"""Main method to get analysis production information.
Gets the AnalysisData information from the same process if possible.
If not loaded already, it loads it from the cache disk and if not present or valid,
fetches from the REST API.
"""
key = (working_group, analysis, ap_date)

@@ -235,39 +306,32 @@ if key in __analysis_map:

"""
self.working_group = working_group
self.analysis = analysis
self._working_group = working_group
self._analysis = analysis
# self.samples is a SampleCollection filled in with the values
metadata_cache = metadata_cache or os.environ.get(APD_METADATA_CACHE_DIR, "")
need_clean_fetch = False
# self._samples is a SampleCollection filled in with the values
# Only for internal use as the default filters are NOT applied
self._samples = None
# Special case when the metadata cache is passed directly as
# a SampleCollection
if metadata_cache:
if isinstance(metadata_cache, SampleCollection):
logger.debug("Using SampleCollection passed to constructor")
self.samples = metadata_cache
else:
logger.debug("Using metadata cache %s", metadata_cache)
try:
self.samples, _ = load_ap_info(
metadata_cache, working_group, analysis, ap_date=ap_date
)
except FileNotFoundError:
logger.debug(
"Could not find cache in %s, loading from remote",
metadata_cache,
)
need_clean_fetch = True
self._samples = metadata_cache
else:
logger.debug(
"No cache set, fetching Analysis Production data from %s", api_url
)
need_clean_fetch = True
# We use the env variable if it is set
envcache = os.environ.get(APD_METADATA_CACHE_DIR, None)
if envcache:
metadata_cache = envcache
if need_clean_fetch:
self.samples = fetch_ap_info(
working_group, analysis, None, api_url, ap_date=ap_date
if self._samples is None:
# In this case the metadata cache was not a SampleCollection or
# not set at all, set setup the cache
self._samples = _load_and_setup_cache(
metadata_cache, working_group, analysis, ap_date, api_url=api_url
)
self.available_tags = self.samples.available_tags()
self._available_tags = self._samples.available_tags()
# Tags is a list of tags that can be used to restrict the samples that will be used
self.default_tags = _validate_tags(kwargs, available_tags=self.available_tags)
# "available_tags" is a list of tags that can be used to restrict the samples that will be used
self._default_tags = _validate_tags(kwargs, available_tags=self._available_tags)

@@ -277,5 +341,5 @@ # Now dealing with data cache

if isinstance(data_cache, str):
self.data_cache = DataCache(data_cache)
self._data_cache = DataCache(data_cache)
else:
self.data_cache = data_cache
self._data_cache = data_cache

@@ -285,4 +349,2 @@ def __call__(

*,
version=None,
name=None,
return_pfns=True,

@@ -299,73 +361,45 @@ check_data=True,

# Cannot mix data from 2 versions in the same dataset
if not version:
version = self.default_tags.get("version", None)
if iterable(version):
raise ValueError("version argument doesn't support iterables")
# Establishing the list of samples to run on
samples = self.samples
samples = self._samples
if name:
if version:
# No need to apply other tags, this specifies explicitly a specific dataset
# We return it straight away
logger.debug("Filtering for version/name %s/%s", name, version)
samples = samples.filter("version", version)
if len(samples) == 0:
raise KeyError(f"No version {version}")
samples = samples.filter("name", name)
if len(samples) == 0:
raise KeyError(f"No name {name}")
else:
# We check whether a version was specified in the default tags
samples = samples.filter("name", name)
if len(samples) != 1:
raise ValueError(
f"{len(samples)} matching {name}, should be exactly 1"
)
else:
# Merge the current tags with the default passed to the constructor
# and check that they are consistent
effective_tags = _validate_tags(
tags, self.default_tags, self.available_tags
)
# Merge the current tags with the default passed to the constructor
# and check that they are consistent
effective_tags = _validate_tags(tags, self._default_tags, self._available_tags)
if version:
effective_tags["version"] = version
for tagname, tagvalue in effective_tags.items():
logger.debug("Filtering for %s = %s", tagname, tagvalue)
for tagname, tagvalue in effective_tags.items():
logger.debug("Filtering for %s = %s", tagname, tagvalue)
# Applying the filters in one go
samples = samples.filter(**effective_tags)
logger.debug("Matched %d samples", len(samples))
# Applying the filters in one go
samples = samples.filter(**effective_tags)
logger.debug("Matched %d samples", len(samples))
# Filter samples and check that we have what we expect
if check_data:
errors = _sample_check(samples, effective_tags)
if len(errors) > 0:
error_txt = f"{len(errors)} problem(s) found\n"
for etags, ecount in errors:
if etags:
error_txt += f"{str(etags)}: "
# Filter samples and check that we have what we expect
if check_data:
errors = sample_check(samples, effective_tags)
if len(errors) > 0:
error_txt = f"{len(errors)} problem(s) found\n"
for etags, ecount in errors:
if etags:
error_txt += f"{str(etags)}: "
if ecount > 0:
error_txt += f"{ecount} samples for the same configuration found, this is ambiguous:"
if ecount > 0:
error_txt += (
f"(only the first {showmax} samples printed)"
if (ecount > showmax)
else ""
error_txt += (
f"(only the first {showmax} samples printed)"
if (ecount > showmax)
else ""
)
match_list = [
str(m)
for m in itertools.islice(
samples.filter(**etags).itertags(), 0, showmax
)
match_list = [
str(m)
for m in itertools.islice(
samples.filter(**etags).itertags(), 0, showmax
)
]
error_txt += "".join(
["\n" + " " * 5 + str(m) for m in match_list]
)
logger.debug("Error loading data: %s", error_txt)
raise ValueError("Error loading data: " + error_txt)
]
error_txt += "".join(
["\n" + " " * 5 + str(m) for m in match_list]
)
else:
error_txt += "No matching sample found"
logger.debug("Error loading data: %s", error_txt)
raise ValueError("Error loading data: " + error_txt)

@@ -381,16 +415,18 @@ if return_pfns:

"""Method to return PFNs, useful as it can be overriden in inheriting classes"""
if not self.data_cache:
if not self._data_cache:
return pfns
return [self.data_cache(pfn) for pfn in pfns]
return [self._data_cache(pfn) for pfn in pfns]
def __str__(self):
txt = f"AnalysisProductions: {self.working_group} / {self.analysis}\n"
txt += str(self.samples)
"""User friendly representation of the AnalysisData instance."""
txt = f"AnalysisProductions: {self._working_group} / {self._analysis}\n"
txt += str(self._samples)
return txt
def __repr__(self):
return f"<AnalysisData: WG={self.analysis}, analysis={self.working_group}, n_samples={len(self.samples)}>"
"""String representation of the AnalysisData instance."""
return f"<AnalysisData: WG={self._analysis}, analysis={self._working_group}, n_samples={len(self._samples)}>"
def summary(self, tags: list = None) -> dict:
"""prepares a summary of the Analysis Production info"""
"""Prepares a summary of the Analysis Production info."""

@@ -401,5 +437,5 @@ # Deal with the tags first

for tag in tags:
if tag in self.available_tags:
if tag in self._available_tags:
try:
values = sorted(self.available_tags[tag])
values = sorted(self._available_tags[tag])
except TypeError as exc:

@@ -409,10 +445,10 @@ raise ValueError(

) from exc
values = list(self.available_tags[tag])
values = list(self._available_tags[tag])
tag_summary[tag] = values
else:
raise ValueError(
f"Requested tag ({tag}) not valid for the given production (wg: {self.working_group}, analysis: {self.analysis})!"
f"Requested tag ({tag}) not valid for the given production (wg: {self._working_group}, analysis: {self._analysis})!"
)
else:
tag_summary = dict(self.available_tags)
tag_summary = dict(self._available_tags)

@@ -424,7 +460,12 @@ summary = {}

if not tags:
summary["analysis"] = self.analysis
summary["working_group"] = self.working_group
summary["Number_of_files"] = self.samples.file_count()
summary["Bytecount"] = self.samples.byte_count()
summary["analysis"] = self._analysis
summary["working_group"] = self._working_group
summary["Number_of_files"] = self._samples.file_count()
summary["Bytecount"] = self._samples.byte_count()
return summary
def all_samples(self):
"""Returns all the samples in this Analysis Production.
i.e. without filtering by the default tags"""
return self._samples

@@ -11,5 +11,9 @@ ###############################################################################

###############################################################################
#
# Tool to load and interpret information from the AnalysisProductions data endpoint
#
"""Internal tools to load and interpret information from the AnalysisProductions data endpoint.
This modules contains the retrieve the data from the AnalysisProductions endpoint
(with the APDataDownloader class). It returns JSON that can be loaded into a
SamplesCollection instance.
"""
import collections.abc

@@ -36,3 +40,3 @@ import difflib

def iterable(arg):
"""Version of Iterable that excludes str"""
"""Version of Iterable that excludes str."""
return isinstance(arg, collections.abc.Iterable) and not isinstance(

@@ -44,3 +48,3 @@ arg, (str, bytes)

def safe_casefold(a):
"""casefold that can be called on any type, does nothing on non str"""
"""Casefold that can be called on any type, does nothing on non str."""
if isinstance(a, str):

@@ -51,4 +55,9 @@ return a.casefold()

def check_tag_in_list(tag, tag_list):
"""Check if the tag exists in the list of whether there is one close enough"""
def check_tag_value_possible(tag, values, available_tags):
"""Check if the `tag` exists in the in the `available_tags` of similar name,
in the sense of `difflib.get_close_matches`. If yes, also check that
the value is one of the existing tags values for that tag
"""
tag_list = [t.lower() for t in available_tags.keys()]
tag = safe_casefold(tag)
if tag not in tag_list:

@@ -61,2 +70,21 @@ msg = f"Tag {tag} unknown."

raise ValueError(msg)
# Now we now the tag exists, checking the value is in the samples
# We can have either a value or a list passed, we always
# transform to a list to simplify processing
if not iterable(values):
values = [safe_casefold(str(values))]
else:
values = [safe_casefold(str(v)) for v in values]
for value in values:
possible_values = available_tags[tag]
if not iterable(possible_values):
possible_values = [possible_values]
possible_values = [safe_casefold(v) for v in possible_values]
if value not in possible_values:
msg = f"No sample for tag {tag}={value}"
closest = difflib.get_close_matches(value, possible_values, n=1)
if closest:
msg += f" Did you mean {closest[0]} ?"
msg += f"\nAvailable values for {tag}: {', '.join(possible_values)}"
raise ValueError(msg)

@@ -69,3 +97,6 @@

class APDataDownloader:
"""Utility class that fetches the Analysis Production information."""
def __init__(self, api_url="https://lbap.app.cern.ch"):
"""Constructor defaulting to the production URL for lbap."""
self.api_url = api_url

@@ -140,3 +171,3 @@ self.token = None

):
"""Utils to compose the name of the cache files"""
"""Utils to compose the name of the cache files."""
cache_dir = Path(cache_dir)

@@ -186,3 +217,3 @@ cache_dir = (cache_dir / "archives" / ap_date) if ap_date else cache_dir

def get_cache_age(cacheinfo):
def _get_cache_age(cacheinfo):
"""Return the number of seconds since the cache was last modified"""

@@ -205,3 +236,3 @@ modif_str = cacheinfo.get(MODIFY, None)

):
"""Fetch the AP info and cache it locally"""
"""Fetch the AP info and cache it locally."""
datafile, tagsfile, cacheinfofile = _analysis_files(

@@ -226,8 +257,11 @@ cache_dir, working_group, analysis, ap_date

cacheinfo = _update_cache_info(cacheinfofile, False, True)
cache_age = get_cache_age(cacheinfo)
cache_age = _get_cache_age(cacheinfo)
logger.debug("cache_age: %s vs maxlife: %s", cache_age, maxlifetime)
# If we have specified a maxlifetime, we return an exception
# if the cache is too old
if maxlifetime:
if cache_age > float(maxlifetime):
if float(maxlifetime) >= 0 and cache_age > float(maxlifetime):
logger.debug(
"cache_too or no caching: %s vs maxlife: %s", cache_age, maxlifetime
)
raise InvalidCacheError(f"Cache too old ({cache_age}s > {maxlifetime}s)")

@@ -260,2 +294,3 @@

def __len__(self):
"""Returns the lenght of the samples list."""
return len(self.info)

@@ -277,2 +312,3 @@

def __repr__(self):
"""Create a string representation of the samples."""
return "\n".join(

@@ -287,2 +323,3 @@ [

def __iter__(self):
"""Iterate on the samples in the info member."""
for s in self.info:

@@ -292,2 +329,3 @@ yield s

def itertags(self):
"""Iterate on all the tags present in all the samples."""
for s in self.info:

@@ -309,4 +347,2 @@ yield self._sampleTags(s)

"""Utility method than handles specific tags, but not iterables"""
sampleTags = self._sampleTags(sample)
check_tag_in_list(ftag, sampleTags.keys())
return safe_casefold(self._sampleTags(sample).get(ftag)) == safe_casefold(

@@ -368,2 +404,3 @@ fvalue

def __or__(self, samples):
"""Logical or between two SampleCollections."""
info = self.info + samples.info

@@ -397,3 +434,4 @@ tags = {**(self.tags), **(samples.tags)}

"""Tool that takes the samples and groups them by the tags specified.
If no list of tags is specified, then the existing ones are used"""
If no list of tags is specified, then the existing ones are used.
"""

@@ -400,0 +438,0 @@ report = self.report()

@@ -19,3 +19,2 @@ ###############################################################################

import tempfile
from pathlib import Path

@@ -26,10 +25,4 @@ import click

from .analysis_data import (
APD_DATA_CACHE_DIR,
APD_METADATA_CACHE_DIR,
APD_METADATA_LIFETIME,
AnalysisData,
get_analysis_data,
)
from .ap_info import InvalidCacheError, cache_ap_info, load_ap_info
from .analysis_data import APD_DATA_CACHE_DIR, APD_METADATA_CACHE_DIR, get_analysis_data
from .ap_info import cache_ap_info
from .authentication import get_auth_headers, logout

@@ -78,38 +71,2 @@ from .data_cache import DataCache

def setup_cache(cache_dir, working_group, analysis, ap_date=None):
"""Utility function that checks whether the data for the Analysis
is cached already and does it if needed."""
if not cache_dir:
cache_dir = Path.home() / ".cache" / "apd"
logger.debug("Cache directory not set, using %s", cache_dir)
try:
lifetime = os.environ.get(APD_METADATA_LIFETIME, None)
load_ap_info(
cache_dir,
working_group,
analysis,
ap_date=ap_date,
maxlifetime=lifetime,
)
except FileNotFoundError:
logger.debug(
"Caching information for %s/%s to %s for time %s",
working_group,
analysis,
cache_dir,
ap_date,
)
cache_ap_info(cache_dir, working_group, analysis, ap_date=ap_date)
except InvalidCacheError:
logger.debug(
"Invalid cache. reloading information for %s/%s to %s for time %s",
working_group,
analysis,
cache_dir,
ap_date,
)
cache_ap_info(cache_dir, working_group, analysis, ap_date=ap_date)
return cache_dir
def _process_common_tags(eventtype, datatype, polarity, config, name, version):

@@ -171,3 +128,3 @@ """Util to simplify the parsing of common tags"""

logger.debug(
"Caching information for %s/%s to %s for time %s",
"Caching %s/%s to %s for time %s",
working_group,

@@ -230,4 +187,2 @@ analysis,

cache_dir = setup_cache(cache_dir, working_group, analysis, date)
# Loading the data and filtering/displaying

@@ -294,5 +249,2 @@ datasets = get_analysis_data(

# Dealing with the cache
cache_dir = setup_cache(cache_dir, working_group, analysis, date)
# Loading the data and filtering/displaying

@@ -332,7 +284,4 @@ datasets = get_analysis_data(

# Dealing with the cache
setup_cache(cache_dir, working_group, analysis)
# Loading the data first
datasets = AnalysisData(working_group, analysis, metadata_cache=cache_dir)
datasets = get_analysis_data(working_group, analysis, metadata_cache=cache_dir)

@@ -346,3 +295,3 @@ # Checking whether we need to group the data...

groups = datasets.samples.groupby(groupby_tags)
groups = datasets.all_samples().groupby(groupby_tags)
if output:

@@ -357,3 +306,3 @@ with open(output, "w") as f:

else:
report = datasets.samples.report()
report = datasets.all_samples().report()
# gets the report as CSV in this case, not JSON

@@ -374,3 +323,3 @@ report_str = "\n".join(([",".join([str(e) for e in line]) for line in report]))

default=os.environ.get(APD_METADATA_CACHE_DIR, None),
help="Specify location of the cached analysis data files",
help="Specify location of the cached analysis metadata",
)

@@ -394,4 +343,2 @@ @click.option(

"""Print a summary of the information available about the specified analysis."""
# Dealing with the cache
cache_dir = setup_cache(cache_dir, working_group, analysis, date)

@@ -422,3 +369,3 @@ # Loading the dataset and displaying its summary

default=os.environ.get(APD_DATA_CACHE_DIR, None),
help="Specify location of the cache for the analysis metadata",
help="Specify location where a copy of the files will be kept",
)

@@ -476,3 +423,2 @@ @click.option(

# pylint: disable-msg=too-many-locals
cache_dir = setup_cache(cache_dir, working_group, analysis, date)

@@ -479,0 +425,0 @@ if not data_cache_dir:

@@ -56,3 +56,3 @@ ###############################################################################

cmd = ["xrdcp", "--silent", str(remote), str(local)]
result = subprocess.run(cmd, check=True, timeout=1000)
result = subprocess.run(cmd, check=True)
return result

@@ -18,3 +18,3 @@ ###############################################################################

from apd.analysis_data import APD_METADATA_CACHE_DIR
from apd.analysis_data import APD_METADATA_CACHE_DIR, APD_METADATA_LIFETIME
from apd.ap_info import cache_ap_info, load_ap_info_from_single_file

@@ -100,2 +100,3 @@

monkeypatch.setenv(APD_METADATA_CACHE_DIR, str(Path(__file__).parent / "cache-dir"))
monkeypatch.setenv(APD_METADATA_LIFETIME, "-1")
yield
###############################################################################
# (c) Copyright 2021 CERN for the benefit of the LHCb Collaboration #
# (c) Copyright 2021-2023 CERN for the benefit of the LHCb Collaboration #
# #

@@ -11,6 +11,12 @@ # This software is distributed under the terms of the GNU General Public #

###############################################################################
import logging
import pytest
from apd import AnalysisData
from apd.analysis_data import APD_METADATA_CACHE_DIR, sample_check
from apd.analysis_data import (
APD_METADATA_CACHE_DIR,
APD_METADATA_LIFETIME,
_sample_check,
)

@@ -26,3 +32,6 @@

def test_fromendpoint(monkeypatch, mocked_responses):
logger = logging.getLogger("apd")
logger.setLevel(logging.DEBUG)
monkeypatch.setenv(APD_METADATA_CACHE_DIR, "")
monkeypatch.setenv(APD_METADATA_LIFETIME, "0")
datasets = AnalysisData("SL", "RDs")

@@ -58,3 +67,3 @@ assert len(datasets(datatype="2012", polarity=["magup", "magdown"])) == 11

):
sample_check(samples, tags)
_sample_check(samples, tags)

@@ -65,3 +74,3 @@

samples = apinfo_multipleversions.filter(**tags)
errors = sample_check(samples, tags)
errors = _sample_check(samples, tags)
assert len(errors) == 0

@@ -73,3 +82,3 @@

samples = apinfo_multipleversions.filter(**tags)
errors = sample_check(samples, tags)
errors = _sample_check(samples, tags)
assert len(errors) == 1

@@ -108,1 +117,8 @@

AnalysisData("SL", "RDs", metadata_cache=apinfo_cache, badtag="novalue")
def test_badtagvalue_constructor(apinfo_cache):
"""In case we specify a tag with a value which does not exist, we should raise
and exception in that case"""
with pytest.raises(ValueError):
AnalysisData("SL", "RDs", metadata_cache=apinfo_cache, datatype="2032")

@@ -29,8 +29,2 @@ ###############################################################################

with pytest.raises(ValueError, match="version argument doesn't support iterables"):
datasets(version=["v0r0p2518507", "v0r0p2970193"], name="2018_15164022_magup")
with pytest.raises(ValueError, match="version argument doesn't support iterables"):
datasets(version={"v0r0p2518507", "v0r0p2970193"}, name="2018_15164022_magup")
pfns = set(

@@ -62,30 +56,2 @@ datasets(

def test_defaults_version(apd_cache):
datasets = AnalysisData("b2oc", "b02dkpi", version="v0r0p2518507")
pfns = datasets(name="2018_15164022_magup")
assert len(pfns) == 6
datasets = AnalysisData("b2oc", "b02dkpi", version="v0r0p2970193")
pfns = datasets(name="2018_15164022_magup")
assert len(pfns) == 1 and "00145075_00000001" in pfns[0]
with pytest.raises(ValueError, match="version argument doesn't support iterables"):
AnalysisData("b2oc", "b02dkpi", version=["v0r0p2518507", "v0r0p2970193"])
with pytest.raises(ValueError, match="version argument doesn't support iterables"):
AnalysisData("b2oc", "b02dkpi", version={"v0r0p2518507", "v0r0p2970193"})
def test_defaults_name(apd_cache):
with pytest.raises(
ValueError, match="name is not a supported tag in AnalysisData objects"
):
AnalysisData(
"b2oc", "b02dkpi", version="v0r0p2518507", name="2018_15164022_magup"
)
with pytest.raises(
ValueError, match="name is not a supported tag in AnalysisData objects"
):
AnalysisData("b2oc", "b02dkpi", name="2018_15164022_magup")
def test_defaults_tag_override(apd_cache):

@@ -176,5 +142,3 @@ datasets = AnalysisData(

with pytest.raises(
ValueError, match=r"Error loading data: 2 problem\(s\) found"
): # TODO: This should be more specific
with pytest.raises(ValueError, match=r"No sample for tag polarity=magoff.*"):
datasets(datatype=["2015", "2016"], polarity=["magoff"])

@@ -212,22 +176,6 @@

def test_missing_by_name(apd_cache):
datasets = AnalysisData("b2oc", "b02dkpi")
with pytest.raises(KeyError):
datasets(version="i-do-no-exist", name="2018_15164022_magup")
with pytest.raises(KeyError):
datasets(version="v0r0p2970193", name="i-do-not-exist")
def test_missing_by_tags(apd_cache):
datasets = AnalysisData("b2oc", "b02dkpi")
with pytest.raises(KeyError):
datasets(version="i-do-no-exist", name="2018_15164022_magup")
with pytest.raises(KeyError):
datasets(version="v0r0p2970193", name="i-do-not-exist")
def test_analysis_case_sensitivity(apd_cache):
datasets = AnalysisData("b2oc", "b02dkpi")
assert len(datasets.samples) == 1694
assert len(AnalysisData("B2OC", "B02DKPI").samples) == 1694
assert len(datasets._samples) == 1694
assert len(AnalysisData("B2OC", "B02DKPI")._samples) == 1694

@@ -245,1 +193,13 @@ pfns = datasets(version="v0r0p2970193", name="2018_15164022_magup")

assert len(pfns) == 1 and "00145075_00000001" in pfns[0]
def test_unknown_tag_value(apd_cache):
datasets = AnalysisData("b2oc", "b02dkpi")
with pytest.raises(ValueError, match="No sample for tag datatype=2032.*"):
datasets(
datatype=["2032"],
polarity=["magup", "magdown"],
eventtype="11164047",
mc=True,
)