Skip to content

Commit 3e5bbe2

Browse files
authored
Merge pull request #8263 from fstagni/90_scitags
[9.0] feat: add scitag support
2 parents fe07997 + 594b374 commit 3e5bbe2

File tree

3 files changed

+124
-1
lines changed

3 files changed

+124
-1
lines changed

src/DIRAC/DataManagementSystem/Client/FTS3Job.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
import datetime
44
import errno
55
import os
6+
import requests
67
from packaging.version import Version
78

8-
from cachetools import cachedmethod, LRUCache
9+
from cachetools import cachedmethod, LRUCache, TTLCache, cached
10+
from threading import Lock
11+
from typing import Optional
12+
913

1014
# Requires at least version 3.3.3
1115
from fts3 import __version__ as fts3_version
@@ -44,6 +48,50 @@
4448
IDP_CACHE_SIZE = 8
4549

4650

51+
_scitag_cache = TTLCache(maxsize=10, ttl=3600)
52+
_scitag_lock = Lock()
53+
_scitag_json_cache = TTLCache(maxsize=1, ttl=86400)
54+
_scitag_json_lock = Lock()
55+
56+
57+
@cached(_scitag_cache, lock=_scitag_lock)
58+
def get_scitag(vo: str, activity: Optional[str] = None) -> int:
59+
"""
60+
Get the scitag based on the VO and activity.
61+
If the VO is not found in the scitag.json, it defaults to 1.
62+
If no specific activity is provided, it defaults to the "default" activityName.
63+
64+
:param vo: The VO for which to get the scitag
65+
:param activity: The activity for which to get the scitag
66+
:return: The scitag value
67+
"""
68+
69+
@cached(_scitag_json_cache, lock=_scitag_json_lock)
70+
def get_remote_json():
71+
gLogger.verbose("Fetching https://scitags.org/api.json from the network")
72+
response = requests.get("https://scitags.org/api.json")
73+
response.raise_for_status()
74+
return response.json()
75+
76+
vo_id = 1 # Default VO ID
77+
activity_id = 1 # Default activity ID
78+
79+
try:
80+
# Load the JSON data from the cache or network
81+
sj = get_remote_json()
82+
83+
for experiment in sj.get("experiments", []):
84+
if experiment.get("expName") == vo.lower():
85+
vo_id = experiment.get("expId")
86+
for act in experiment.get("activities", []):
87+
if act.get("activityName") == activity:
88+
activity_id = act.get("activityId")
89+
except Exception as e:
90+
gLogger.error(f"Error fetching or parsing scitag.json. Using default scitag values.", repr(e))
91+
# Logic to determine the scitag based on vo and activity (this is what FTS wants)
92+
return vo_id << 6 | activity_id # Example logic, replace with actual implementation
93+
94+
4795
class FTS3Job(JSerializable):
4896
"""Abstract class to represent a job to be executed by FTS. It belongs
4997
to an FTS3Operation
@@ -470,6 +518,9 @@ def _constructTransferJob(self, pinTime, allLFNs, target_spacetoken, protocols=N
470518

471519
ftsFileID = getattr(ftsFile, "fileID")
472520

521+
# scitag 65 is 1 << 6 | 1 (default experiment, default activity)
522+
scitag = get_scitag(vo=self.vo, activity=self.activity)
523+
473524
# Under normal circumstances, we simply submit an fts transfer as such:
474525
# * srcProto://myFile -> destProto://myFile
475526
#
@@ -499,6 +550,7 @@ def _constructTransferJob(self, pinTime, allLFNs, target_spacetoken, protocols=N
499550
filesize=ftsFile.size,
500551
metadata=stageTrans_metadata,
501552
activity=self.activity,
553+
scitag=scitag,
502554
)
503555
transfers.append(stageTrans)
504556

@@ -572,6 +624,7 @@ def _constructTransferJob(self, pinTime, allLFNs, target_spacetoken, protocols=N
572624
activity=self.activity,
573625
source_token=srcToken,
574626
destination_token=dstToken,
627+
scitag=scitag,
575628
)
576629

577630
transfers.append(trans)

src/DIRAC/DataManagementSystem/Client/test/Test_FTS3Objects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ def generateFTS3Job(sourceSE, targetSE, lfns, multiHopSE=None):
204204
newJob.sourceSE = sourceSE
205205
newJob.targetSE = targetSE
206206
newJob.multiHopSE = multiHopSE
207+
newJob.vo = "lhcb"
207208
filesToSubmit = []
208209

209210
for i, lfn in enumerate(lfns, start=1):
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from unittest.mock import Mock, patch
2+
3+
import pytest
4+
5+
from DIRAC.DataManagementSystem.Client.FTS3Job import get_scitag
6+
7+
8+
class TestGetScitag:
9+
def test_valid_vo_and_activity(self):
10+
"""Test get_scitag with valid VO and activity."""
11+
result = get_scitag("atlas", "Analysis Input")
12+
expected = 2 << 6 | 17 # atlas expId=2, analysis activityId=17
13+
assert result == expected
14+
15+
def test_valid_vo_no_activity(self):
16+
"""Test get_scitag with valid VO but no specific activity (should use default)."""
17+
result = get_scitag("cms")
18+
expected = 3 << 6 | 1 # cms expId=200, default activityId=1
19+
assert result == expected
20+
21+
def test_invalid_vo(self):
22+
"""Test get_scitag with invalid VO (should use default vo_id=1)."""
23+
result = get_scitag("nonexistent")
24+
expected = 1 << 6 | 1 # default vo_id=1, default activity_id=1
25+
assert result == expected
26+
27+
def test_valid_vo_invalid_activity(self):
28+
"""Test get_scitag with valid VO but invalid activity."""
29+
result = get_scitag("atlas", "nonexistent_activity")
30+
expected = 2 << 6 | 1 # atlas expId=2, default activity_id=1
31+
assert result == expected
32+
33+
def test_case_insensitive_vo(self):
34+
"""Test that VO matching is case insensitive."""
35+
result = get_scitag("ATLAS", "Data Brokering")
36+
expected = 2 << 6 | 3 # atlas expId=2, production activityId=3
37+
assert result == expected
38+
39+
40+
@pytest.mark.parametrize(
41+
"vo,activity,expected_vo_id,expected_activity_id",
42+
[
43+
("atlas", "Analysis Output", 2, 18),
44+
("atlas", "Debug", 2, 9),
45+
("cms", "Cache", 3, 3),
46+
("cms", "default", 3, 1),
47+
("nonexistent", "any", 1, 1), # defaults
48+
("atlas", "nonexistent", 2, 1), # valid vo, invalid activity
49+
],
50+
)
51+
def test_parametrized_scenarios(vo, activity, expected_vo_id, expected_activity_id):
52+
"""Parametrized test for various VO and activity combinations."""
53+
result = get_scitag(vo, activity)
54+
expected = expected_vo_id << 6 | expected_activity_id
55+
assert result == expected
56+
57+
58+
@pytest.mark.parametrize(
59+
"vo,expected_result",
60+
[
61+
("atlas", 2 << 6 | 1), # Should use default activity
62+
("cms", 3 << 6 | 1), # Should use default activity
63+
("unknown", 1 << 6 | 1), # Should use all defaults
64+
],
65+
)
66+
def test_no_activity_parameter(vo, expected_result):
67+
"""Test behavior when no activity parameter is provided."""
68+
result = get_scitag(vo)
69+
assert result == expected_result

0 commit comments

Comments
 (0)