Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-09-21 09:33:40

0001 #!/usr/bin/env python3
0002 
0003 import argparse
0004 import os
0005 import json
0006 import logging
0007 from typing import Dict, Any
0008 from rucio.client.uploadclient import UploadClient
0009 from rucio.client import Client
0010 from rucio.common.exception import InputValidationError, RSEWriteBlocked, NoFilesUploaded, NotAllFilesUploaded
0011 from jsonschema import validate as json_validate, ValidationError
0012 
0013 
0014 # Define the metadata schema
0015 METADATA_SCHEMA = {
0016     "$schema": "http://json-schema.org/draft-07/schema#",
0017     "title": "ePICRucioMetadataTags",
0018     "description": "Optimized metadata tags for ePIC Rucio datasets using searchable slugs.",
0019     "type": "object",
0020     "properties": {
0021         "software_release": {
0022             "type": "string",
0023             "description": "Container version tag (e.g. 26.03.0-stable, nightly, unstable, or default)",
0024             "pattern": "^([0-9]+\\.[0-9]+\\.[0-9]+-stable|nightly|unstable|default)$"
0025         },
0026         "requester_pwg": {
0027             "type": "string",
0028             "description": "PWG requesting the dataset.",
0029             "enum": [
0030                 "edt",
0031                 "inclusive",
0032                 "jets_hf",
0033                 "semi_inclusive",
0034                 "ew_bsm",
0035                 "other"
0036             ]
0037         },
0038         "q2_min_gev2": {
0039             "type": "number",
0040             "description": "Minimum Q2 value (GeV^2). Optional - not applicable to all datasets."
0041         },
0042         "q2_max_gev2": {
0043             "type": "number",
0044             "description": "Maximum Q2 value (GeV^2). Optional - not applicable to all datasets."
0045         },
0046         "electron_beam_energy_gev": {
0047             "type": "number",
0048             "description": "Electron beam energy (GeV)"
0049         },
0050         "ion_beam_energy_gev": {
0051             "type": "number",
0052             "description": "Ion/nucleus beam energy (GeV)"
0053         },
0054         "is_background_mixed": {
0055             "type": "boolean",
0056             "description": "True if the sample includes background mixing; false if it is a regular/pure signal sample."
0057         },
0058         "ion_species": {
0059             "type": "string",
0060             "description": "Ion species.",
0061             "enum": [
0062                 "p",
0063                 "Au197",
0064                 "Cu63",
0065                 "He3",
0066                 "H2",
0067                 "Ru96",
0068                 "Pb208",
0069                 "Pb207"                
0070             ]
0071         },
0072         "data_level": {
0073             "type": "string",
0074             "description": "Data processing level.",
0075             "enum": [
0076                 "simulation",
0077                 "reconstruction"
0078             ]
0079         },
0080         "gun_particle": {
0081             "type": "string",
0082             "description": "Single particle type. Optional - only applicable to single particle datasets.",
0083             "enum": [
0084                 "e-",
0085                 "e+",
0086                 "proton",
0087                 "neutron",
0088                 "pi+",
0089                 "pi-",
0090                 "pi0",
0091                 "kaon-",
0092                 "kaon+",
0093                 "gamma",
0094                 "mu-"
0095             ]
0096         },
0097         "geometry_config": {
0098             "type": "string",
0099             "description": "Geometry configuration tag (e.g. craterlake_18x275, craterlake_5x41_He3)",
0100             "pattern": "^[a-z][a-z0-9_]*_[0-9]+x[0-9]+(_.+)?$"
0101         },
0102         "gun_momentum_min_gev": {
0103             "type": "number",
0104             "description": "Minimum particle gun momentum (GeV). For fixed-energy runs, equals gun_momentum_max_gev."
0105         },
0106         "gun_momentum_max_gev": {
0107             "type": "number",
0108             "description": "Maximum particle gun momentum (GeV). For fixed-energy runs, equals gun_momentum_min_gev."
0109         },
0110         "gun_theta_min_deg": {
0111             "type": "number",
0112             "description": "Minimum polar angle (degrees) for particle gun angular distribution."
0113         },
0114         "gun_theta_max_deg": {
0115             "type": "number",
0116             "description": "Maximum polar angle (degrees) for particle gun angular distribution."
0117         },
0118         "gun_phi_min_deg": {
0119             "type": "number",
0120             "description": "Minimum azimuthal angle (degrees) for particle gun distribution. Default is 0."
0121         },
0122         "gun_phi_max_deg": {
0123             "type": "number",
0124             "description": "Maximum azimuthal angle (degrees) for particle gun distribution. Default is 360."
0125         },
0126         "gun_distribution": {
0127             "type": "string",
0128             "description": "Angular distribution type for particle gun.",
0129             "enum": ["uniform", "cos(theta)", "eta", "pseudorapidity", "ffbar"]
0130         },
0131         "requester_dsc": {
0132             "type": "string",
0133             "description": "Detector Subsystem Collaboration requesting the dataset. Optional.",
0134             "enum": [
0135                 "tracking",
0136                 "other"
0137             ]
0138         },
0139         "generator": {
0140             "type": "string",
0141             "description": "Generator name",
0142             "enum": [
0143                 "pythia6",
0144                 "pythia8",
0145                 "beagle",
0146                 "djangoh",
0147                 "rapgap",
0148                 "dempgen",
0149                 "sartre",
0150                 "lager",
0151                 "estarlight",
0152                 "epic",
0153                 "getalm",
0154                 "eicmesonsfgen",
0155                 "eic_sr_geant4",
0156                 "eic_esr_xsuite",
0157                 "sherpa",
0158                 "single_particle",
0159                 "other"
0160             ]
0161         },
0162     },
0163     "required": [
0164         "software_release",
0165         "is_background_mixed",
0166         "data_level",
0167         "geometry_config",
0168         "generator"
0169     ]
0170 }
0171 
0172 
0173 def validate_metadata(metadata: Dict[str, Any]) -> bool:
0174     """
0175     Validate metadata against the schema using jsonschema.
0176     
0177     Parameters
0178     ----------
0179     metadata : dict
0180         The metadata dictionary to validate
0181         
0182     Returns
0183     -------
0184     bool
0185         True if valid
0186         
0187     Raises
0188     ------
0189     ValueError
0190         If metadata doesn't match the schema
0191     """
0192     if not isinstance(metadata, dict):
0193         raise ValueError("Metadata must be a JSON object (dictionary)")
0194     
0195     try:
0196         json_validate(instance=metadata, schema=METADATA_SCHEMA)
0197     except ValidationError as e:
0198         raise ValueError(f"Metadata validation failed: {e.message}")
0199     
0200     return True
0201 
0202 
0203 def load_metadata_file(filepath: str) -> Dict[str, Any]:
0204     """
0205     Load and validate metadata from a JSON file.
0206     
0207     Parameters
0208     ----------
0209     filepath : str
0210         Path to the metadata JSON file
0211         
0212     Returns
0213     -------
0214     dict
0215         The validated metadata dictionary
0216         
0217     Raises
0218     ------
0219     FileNotFoundError
0220         If the metadata file doesn't exist
0221     ValueError
0222         If the JSON is invalid or doesn't match the schema
0223     """
0224     if not os.path.exists(filepath):
0225         raise FileNotFoundError(f"Metadata file not found: {filepath}")
0226     
0227     try:
0228         with open(filepath, 'r') as f:
0229             metadata = json.load(f)
0230     except json.JSONDecodeError as e:
0231         raise ValueError(f"Invalid JSON in metadata file: {e}")
0232     
0233     validate_metadata(metadata)
0234     return metadata
0235 
0236 
0237 if __name__ == "__main__":
0238     parser = argparse.ArgumentParser(
0239         prog='Register to RUCIO',
0240         description='Registers files to RUCIO with optional dataset metadata'
0241     )
0242     parser.add_argument(
0243         "-f", dest="file_paths",
0244         action="store", nargs='+', required=True,
0245         help="Enter the local file path(s)"
0246     )
0247     parser.add_argument(
0248         "-d", dest="did_names",
0249         action="store", nargs='+', required=True,
0250         help="Enter the data identifier(s) for rucio catalogue"
0251     )
0252     parser.add_argument(
0253         "-s", dest="scope",
0254         action="store", required=True,
0255         help="Enter the scope"
0256     )
0257     parser.add_argument(
0258         "-r", dest="rse",
0259         action="store", required=True,
0260         help="Enter the rucio storage element (e.g., EIC-XRD for production outputs)"
0261     )
0262     parser.add_argument(
0263         '--noregister', dest="noregister",
0264         action="store_true", default=False,
0265         help="Skip rucio registration (upload only)"
0266     )
0267     parser.add_argument(
0268         '--upload-metadata', dest="metadata_file",
0269         action="store", default=None,
0270         help="Path to JSON file containing dataset metadata"
0271     )
0272     parser.add_argument(
0273         '--metadata-json', dest="metadata_json",
0274         action="store", default=None,
0275         help="JSON string containing dataset metadata"
0276     )
0277 
0278     args = parser.parse_args()
0279 
0280     file_paths = args.file_paths
0281     did_names = args.did_names
0282     scope = args.scope
0283     rse = args.rse
0284     noregister = args.noregister
0285 
0286     # Validation to ensure file_paths and did_names have the same length
0287     if len(file_paths) != len(did_names):
0288         raise ValueError("The number of file paths must match the number of did names.")
0289 
0290     # Validate that all files exist
0291     for file_path in file_paths:
0292         if not os.path.exists(file_path):
0293             raise FileNotFoundError(f"File not found: {file_path}")
0294 
0295     # Load and validate metadata if provided
0296     if args.metadata_file and args.metadata_json:
0297         raise ValueError("Cannot specify both --upload-metadata and --metadata-json")
0298     dataset_meta = None
0299     if args.metadata_file:
0300         dataset_meta = load_metadata_file(args.metadata_file)
0301         print(f"Loaded metadata: {json.dumps(dataset_meta, indent=2)}")
0302     elif args.metadata_json:
0303         try:
0304             dataset_meta = json.loads(args.metadata_json)
0305         except json.JSONDecodeError as e:
0306             raise ValueError(f"Invalid JSON in --metadata-json: {e}")
0307         validate_metadata(dataset_meta)
0308         print(f"Loaded metadata: {json.dumps(dataset_meta, indent=2)}")
0309 
0310     upload_items = []  # List to hold the upload items
0311 
0312     # Loop through the file paths and did names
0313     for file_path, did_name in zip(file_paths, did_names):
0314         parent_directory = os.path.dirname(did_name)  # Get the parent directory from did_name
0315         
0316         # Validate that parent_directory is not empty
0317         if not parent_directory:
0318             raise ValueError(
0319                 f"DID name '{did_name}' does not contain a parent directory. "
0320                 "Expected format: 'parent/filename'"
0321             )
0322         
0323         # Create a new dictionary for each file and did_name
0324         upload_item = {
0325             'path': file_path,
0326             'rse': rse,
0327             'did_scope': scope,
0328             'did_name': did_name,
0329             'dataset_scope': scope,
0330             'dataset_name': parent_directory,
0331             'no_register': noregister
0332         }
0333         
0334         # Add metadata if provided and not in noregister mode
0335         if dataset_meta and not noregister:
0336             upload_item['dataset_meta'] = dataset_meta
0337         
0338         # Append the new item to the upload_items list
0339         upload_items.append(upload_item)
0340 
0341     # Set up logging
0342     logger = logging.getLogger('upload_client')
0343     logger.addHandler(logging.StreamHandler())
0344     logger.setLevel(logging.INFO)
0345     
0346     upload_client = UploadClient(logger=logger)
0347     client = Client()
0348     
0349     try:
0350         upload_client.upload(upload_items)
0351         logger.info("Upload completed successfully!")
0352     except Exception as e:
0353         logger.error(f"Upload failed: {e}")
0354         
0355         dids = [{'scope': scope, 'name': did_name} for did_name in did_names]
0356         
0357         # Get replicas for all DIDs in the rse
0358         replicas = client.list_replicas(
0359             dids,
0360             all_states=True,
0361             rse_expression=rse
0362         )
0363         
0364         # Collect files that need to be cleaned up
0365         files_to_update = []
0366         files_to_tombstone = []
0367         
0368         for replica in replicas:
0369             did_name = replica['name']
0370             state = replica['states'].get(rse)
0371             
0372             if state == 'COPYING':
0373                 logger.warning(
0374                     "Found COPYING replica %s:%s on %s — deleting",
0375                     scope, did_name, rse
0376                 )
0377                 files_to_update.append({'scope': scope, 'name': did_name, 'state': 'U'})
0378                 files_to_tombstone.append({'rse': rse, 'scope': scope, 'name': did_name})
0379         
0380         if files_to_update:
0381             # Update replica states to UNAVAILABLE(U)
0382             client.update_replicas_states(rse=rse, files=files_to_update)
0383             # set tombstone to that did, should trigger deletion
0384             client.set_tombstone(files_to_tombstone)
0385         
0386         raise