rucio 37.7.1__py3-none-any.whl → 38.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rucio might be problematic. Click here for more details.

Files changed (130) hide show
  1. rucio/alembicrevision.py +1 -1
  2. rucio/cli/bin_legacy/rucio.py +51 -107
  3. rucio/cli/bin_legacy/rucio_admin.py +26 -26
  4. rucio/cli/command.py +1 -0
  5. rucio/cli/did.py +2 -2
  6. rucio/cli/opendata.py +132 -0
  7. rucio/cli/replica.py +15 -5
  8. rucio/cli/rule.py +7 -2
  9. rucio/cli/scope.py +3 -2
  10. rucio/cli/utils.py +28 -4
  11. rucio/client/baseclient.py +9 -1
  12. rucio/client/client.py +2 -0
  13. rucio/client/diracclient.py +73 -12
  14. rucio/client/opendataclient.py +249 -0
  15. rucio/client/subscriptionclient.py +30 -0
  16. rucio/client/uploadclient.py +10 -13
  17. rucio/common/constants.py +4 -1
  18. rucio/common/exception.py +55 -0
  19. rucio/common/plugins.py +45 -8
  20. rucio/common/schema/generic.py +5 -3
  21. rucio/common/schema/generic_multi_vo.py +4 -2
  22. rucio/common/types.py +8 -7
  23. rucio/common/utils.py +176 -11
  24. rucio/core/dirac.py +5 -5
  25. rucio/core/opendata.py +744 -0
  26. rucio/core/rule.py +63 -8
  27. rucio/core/transfer.py +1 -1
  28. rucio/daemons/common.py +1 -1
  29. rucio/daemons/conveyor/finisher.py +2 -2
  30. rucio/daemons/conveyor/poller.py +2 -2
  31. rucio/daemons/conveyor/preparer.py +1 -1
  32. rucio/daemons/conveyor/submitter.py +2 -2
  33. rucio/daemons/conveyor/throttler.py +1 -1
  34. rucio/db/sqla/constants.py +6 -0
  35. rucio/db/sqla/migrate_repo/versions/a62db546a1f1_opendata_initial_model.py +85 -0
  36. rucio/db/sqla/models.py +69 -0
  37. rucio/db/sqla/session.py +8 -1
  38. rucio/db/sqla/util.py +2 -2
  39. rucio/gateway/dirac.py +1 -1
  40. rucio/gateway/opendata.py +190 -0
  41. rucio/gateway/subscription.py +5 -3
  42. rucio/rse/protocols/protocol.py +9 -5
  43. rucio/rse/translation.py +17 -6
  44. rucio/tests/common.py +64 -12
  45. rucio/transfertool/fts3.py +1 -0
  46. rucio/transfertool/fts3_plugins.py +6 -1
  47. rucio/vcsversion.py +4 -4
  48. rucio/web/rest/flaskapi/v1/auth.py +11 -2
  49. rucio/web/rest/flaskapi/v1/common.py +34 -14
  50. rucio/web/rest/flaskapi/v1/config.py +1 -1
  51. rucio/web/rest/flaskapi/v1/dids.py +447 -160
  52. rucio/web/rest/flaskapi/v1/heartbeats.py +1 -1
  53. rucio/web/rest/flaskapi/v1/identities.py +1 -1
  54. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +1 -1
  55. rucio/web/rest/flaskapi/v1/locks.py +1 -1
  56. rucio/web/rest/flaskapi/v1/main.py +3 -7
  57. rucio/web/rest/flaskapi/v1/meta_conventions.py +1 -16
  58. rucio/web/rest/flaskapi/v1/nongrid_traces.py +1 -1
  59. rucio/web/rest/flaskapi/v1/opendata.py +391 -0
  60. rucio/web/rest/flaskapi/v1/opendata_public.py +146 -0
  61. rucio/web/rest/flaskapi/v1/requests.py +1 -1
  62. rucio/web/rest/flaskapi/v1/rses.py +1 -1
  63. rucio/web/rest/flaskapi/v1/rules.py +1 -1
  64. rucio/web/rest/flaskapi/v1/scopes.py +1 -1
  65. rucio/web/rest/flaskapi/v1/subscriptions.py +6 -9
  66. rucio/web/rest/flaskapi/v1/traces.py +1 -1
  67. rucio/web/rest/flaskapi/v1/vos.py +1 -1
  68. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/alembic.ini.template +1 -1
  69. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/alembic_offline.ini.template +1 -1
  70. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/rucio.cfg.template +2 -2
  71. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +3 -3
  72. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/requirements.server.txt +6 -3
  73. rucio-38.0.0.data/data/rucio/tools/reset_database.py +87 -0
  74. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio +2 -1
  75. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/METADATA +37 -36
  76. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/RECORD +128 -122
  77. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/licenses/AUTHORS.rst +1 -0
  78. rucio/client/fileclient.py +0 -57
  79. rucio-37.7.1.data/data/rucio/tools/reset_database.py +0 -40
  80. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  81. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
  82. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  83. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  84. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  85. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  86. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  87. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  88. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  89. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  90. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/tools/bootstrap.py +0 -0
  91. {rucio-37.7.1.data → rucio-38.0.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  92. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-abacus-account +0 -0
  93. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-abacus-collection-replica +0 -0
  94. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-abacus-rse +0 -0
  95. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-admin +0 -0
  96. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-atropos +0 -0
  97. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-auditor +0 -0
  98. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-automatix +0 -0
  99. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-bb8 +0 -0
  100. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-cache-client +0 -0
  101. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-cache-consumer +0 -0
  102. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-finisher +0 -0
  103. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-poller +0 -0
  104. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-preparer +0 -0
  105. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-receiver +0 -0
  106. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-stager +0 -0
  107. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-submitter +0 -0
  108. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-conveyor-throttler +0 -0
  109. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-dark-reaper +0 -0
  110. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-dumper +0 -0
  111. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-follower +0 -0
  112. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-hermes +0 -0
  113. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-judge-cleaner +0 -0
  114. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-judge-evaluator +0 -0
  115. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-judge-injector +0 -0
  116. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-judge-repairer +0 -0
  117. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-kronos +0 -0
  118. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-minos +0 -0
  119. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
  120. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-necromancer +0 -0
  121. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-oauth-manager +0 -0
  122. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-reaper +0 -0
  123. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-replica-recoverer +0 -0
  124. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-rse-decommissioner +0 -0
  125. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-storage-consistency-actions +0 -0
  126. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-transmogrifier +0 -0
  127. {rucio-37.7.1.data → rucio-38.0.0.data}/scripts/rucio-undertaker +0 -0
  128. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/WHEEL +0 -0
  129. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/licenses/LICENSE +0 -0
  130. {rucio-37.7.1.dist-info → rucio-38.0.0.dist-info}/top_level.txt +0 -0
@@ -21,7 +21,7 @@ from rucio.common.exception import AccessDenied, InvalidObject
21
21
  from rucio.common.schema import validate_schema
22
22
  from rucio.common.types import InternalAccount, InternalScope
23
23
  from rucio.core import subscription
24
- from rucio.db.sqla.constants import DatabaseOperationType
24
+ from rucio.db.sqla.constants import DatabaseOperationType, SubscriptionState
25
25
  from rucio.db.sqla.session import db_session
26
26
  from rucio.gateway.permission import has_permission
27
27
 
@@ -110,7 +110,7 @@ def update_subscription(
110
110
 
111
111
  :param name: Name of the subscription
112
112
  :param account: Account identifier
113
- :param metadata: Dictionary of metadata to update. Supported keys : filter, replication_rules, comments, lifetime, retroactive, dry_run, priority, last_processed
113
+ :param metadata: Dictionary of metadata to update. Supported keys : filter, replication_rules, comments, lifetime, retroactive, dry_run, priority, last_processed, state
114
114
  :param issuer: The account issuing this operation.
115
115
  :param vo: The VO to act on.
116
116
  :raises: SubscriptionNotFound if subscription is not found
@@ -132,6 +132,9 @@ def update_subscription(
132
132
  else:
133
133
  for rule in metadata['replication_rules']:
134
134
  validate_schema(name='activity', obj=rule.get('activity', 'default'), vo=vo)
135
+ if 'state' in metadata and metadata['state'] is not None:
136
+ metadata['state'] = SubscriptionState(metadata['state'])
137
+
135
138
  except ValueError as error:
136
139
  raise TypeError(error)
137
140
 
@@ -148,7 +151,6 @@ def update_subscription(
148
151
  filter_[_key] = [_type(val, vo=vo).internal for val in filter_[_key]]
149
152
  else:
150
153
  filter_[_key] = _type(filter_[_key], vo=vo).internal
151
-
152
154
  return subscription.update_subscription(name=name, account=internal_account, metadata=metadata, session=session)
153
155
 
154
156
 
@@ -69,11 +69,15 @@ class RSEProtocol(ABC):
69
69
  self.rse = rse_settings
70
70
  self.logger = logger
71
71
  if self.rse['deterministic']:
72
- self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes)
73
- if getattr(rsemanager, 'CLIENT_MODE', None) and \
74
- not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
75
- # Remote server has an algorithm we don't understand; always make the server do the lookup.
76
- setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
72
+ if getattr(rsemanager, 'SERVER_MODE', None):
73
+ vo = get_rse_vo(self.rse['id'])
74
+ if getattr(rsemanager, 'CLIENT_MODE', None):
75
+ # assume client has only one VO policy package configured
76
+ vo = ''
77
+ if not RSEDeterministicTranslation.supports(self.rse.get('lfn2pfn_algorithm')):
78
+ # Remote server has an algorithm we don't understand; always make the server do the lookup.
79
+ setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
80
+ self.translator = RSEDeterministicTranslation(self.rse['rse'], rse_settings, self.attributes, vo)
77
81
  else:
78
82
  if getattr(rsemanager, 'CLIENT_MODE', None):
79
83
  setattr(self, 'lfns2pfns', self.__lfns2pfns_client)
rucio/rse/translation.py CHANGED
@@ -50,7 +50,7 @@ class RSEDeterministicScopeTranslation(PolicyPackageAlgorithms):
50
50
  algorithm_name = "def"
51
51
  logger.debug("PFN2LFN: Falling back to %s algorithm.", 'default' if algorithm_name == 'def' else algorithm_name)
52
52
 
53
- self.parser = self.get_parser(algorithm_name)
53
+ self.parser = self.get_parser(algorithm_name, vo)
54
54
 
55
55
  @classmethod
56
56
  def _module_init_(cls) -> None:
@@ -60,8 +60,14 @@ class RSEDeterministicScopeTranslation(PolicyPackageAlgorithms):
60
60
  cls.register(cls._default, "def")
61
61
 
62
62
  @classmethod
63
- def get_parser(cls, algorithm_name: str) -> 'Callable[..., Any]':
64
- return super()._get_one_algorithm(cls._algorithm_type, algorithm_name)
63
+ def get_parser(cls, algorithm_name: str, vo: str) -> 'Callable[..., Any]':
64
+ result = None
65
+ if algorithm_name == vo:
66
+ # default algorithm for VO
67
+ result = super()._get_default_algorithm(RSEDeterministicScopeTranslation._algorithm_type, vo)
68
+ if result is None:
69
+ result = super()._get_one_algorithm(cls._algorithm_type, algorithm_name)
70
+ return result
65
71
 
66
72
  @classmethod
67
73
  def register(
@@ -111,7 +117,8 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
111
117
  self,
112
118
  rse: Optional[str] = None,
113
119
  rse_attributes: Optional["RSESettingsDict"] = None,
114
- protocol_attributes: Optional[dict[str, Any]] = None
120
+ protocol_attributes: Optional[dict[str, Any]] = None,
121
+ vo: str = DEFAULT_VO
115
122
  ):
116
123
  """
117
124
  Initialize a translator object from the RSE, its attributes, and the protocol-specific
@@ -125,6 +132,7 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
125
132
  self.rse = rse
126
133
  self.rse_attributes = rse_attributes if rse_attributes else {}
127
134
  self.protocol_attributes = protocol_attributes if protocol_attributes else {}
135
+ self.vo = vo
128
136
 
129
137
  @classmethod
130
138
  def supports(
@@ -251,9 +259,12 @@ class RSEDeterministicTranslation(PolicyPackageAlgorithms):
251
259
  :returns: RSE specific URI of the physical file
252
260
  """
253
261
  algorithm = self.rse_attributes.get(RseAttr.LFN2PFN_ALGORITHM, 'default')
254
- if algorithm == 'default':
262
+ algorithm_callable = None
263
+ if algorithm == 'default' or algorithm == RSEDeterministicTranslation._DEFAULT_LFN2PFN:
255
264
  algorithm = RSEDeterministicTranslation._DEFAULT_LFN2PFN
256
- algorithm_callable = super()._get_one_algorithm(RSEDeterministicTranslation._algorithm_type, algorithm)
265
+ algorithm_callable = super()._get_default_algorithm(RSEDeterministicTranslation._algorithm_type, self.vo)
266
+ if algorithm_callable is None:
267
+ algorithm_callable = super()._get_one_algorithm(RSEDeterministicTranslation._algorithm_type, algorithm)
257
268
  return algorithm_callable(scope, name, self.rse, self.rse_attributes, self.protocol_attributes)
258
269
 
259
270
 
rucio/tests/common.py CHANGED
@@ -50,23 +50,75 @@ skip_outside_gh_actions = pytest.mark.skipif(os.getenv("GITHUB_ACTIONS") != "tru
50
50
  reason="Skipping tests outside GitHub Actions")
51
51
 
52
52
 
53
- def is_influxdb_available() -> bool:
54
- """Return True if influxdb is available, else return False."""
53
+ def is_influxdb_available(
54
+ url: str = "http://influxdb:8086",
55
+ timeout: float = 2.0
56
+ ) -> bool:
57
+ """
58
+ Return True when InfluxDB is up and ready for queries, otherwise False.
59
+
60
+ Strategy:
61
+ 1. Try /health → 200 + JSON["status"] == "pass"
62
+ 2. Fallback to /ping → 204
63
+ """
64
+ print(f"Checking InfluxDB availability at {url}")
65
+ try:
66
+ r = requests.get(f"{url}/health", timeout=timeout)
67
+ print(f"InfluxDB /health responded with {r.status_code} and body: {r.text}", r.status_code, r.text)
68
+ if r.status_code == 200 and r.json().get("status") == "pass":
69
+ return True
70
+ print(f"InfluxDB is not running healthy at {url}.")
71
+ return False
72
+ except requests.RequestException as e:
73
+ # /health failed or is not available (pre‑1.8)
74
+ print(f"Failed to query InfluxDB /health at {url}: {e}")
75
+
55
76
  try:
56
- response = requests.get('http://localhost:8086/ping')
57
- return response.status_code == 204
58
- except requests.exceptions.ConnectionError:
59
- print('InfluxDB is not running at localhost:8086')
77
+ print(f"Falling back to /ping for InfluxDB at {url}")
78
+ r = requests.get(f"{url}/ping", timeout=timeout)
79
+ print(f"InfluxDB /ping responded with {r.status_code}")
80
+ return r.status_code == 204
81
+ except requests.RequestException as e:
82
+ print(f"InfluxDB is not reachable at {url}: {e}")
60
83
  return False
61
84
 
62
85
 
63
- def is_elasticsearch_available() -> bool:
64
- """Return True if elasticsearch is available, else return False."""
86
+ def is_elasticsearch_available(
87
+ url: str = "http://elasticsearch:9200",
88
+ timeout: float = 2.0,
89
+ min_status: str = 'green',
90
+ ) -> bool:
91
+ """
92
+ Return True when the Elasticsearch node is reachable **and**
93
+ cluster health is at least `min_status` ('red'<'yellow'<'green').
94
+
95
+ 1. GET /_cluster/health → 200 + JSON["status"] meets threshold
96
+ 2. Fallback: HEAD / → 200 (port open but health unknown)
97
+ """
98
+ _status_level = {"red": 1, "yellow": 2, "green": 3}
99
+
100
+ print(f"Checking Elasticsearch availability at {url}")
101
+ try:
102
+ r = requests.get(f"{url}/_cluster/health", timeout=timeout)
103
+ print(f"Elasticsearch /_cluster/health responded with {r.status_code} and body: {r.text}")
104
+ if r.status_code == 200:
105
+ status = r.json().get("status")
106
+ if status and _status_level[status] >= _status_level[min_status]:
107
+ return True
108
+ print(f"Elasticsearch health is {status!r}, below threshold {min_status!r}.")
109
+ return False
110
+ except requests.RequestException as e:
111
+ # Either not reachable or /_cluster/health not yet available
112
+ print(f"Failed to query Elasticsearch /_cluster/health at {url}: {e}")
113
+
114
+ # Very old nodes or boot‑strapping clusters: fall back to a simple HEAD /
65
115
  try:
66
- response = requests.get('http://localhost:9200/')
67
- return response.status_code == 200
68
- except requests.exceptions.ConnectionError:
69
- print('Elasticsearch is not running at localhost:9200')
116
+ print(f"Falling back to HEAD request for Elasticsearch at {url}")
117
+ r = requests.head(url, timeout=timeout)
118
+ print(f"Elasticsearch HEAD / responded with {r.status_code}")
119
+ return r.status_code == 200
120
+ except requests.RequestException as e:
121
+ print(f"Elasticsearch is not reachable at {url}: {e}")
70
122
  return False
71
123
 
72
124
 
@@ -1046,6 +1046,7 @@ class FTS3Transfertool(Transfertool):
1046
1046
  t_file['scitag'] = self.scitags_exp_id << 6 | activity_id
1047
1047
 
1048
1048
  if t_file['metadata']['dst_type'] == 'TAPE':
1049
+ t_file['metadata']['vo'] = rws.scope.vo
1049
1050
  for plugin in self.tape_metadata_plugins:
1050
1051
  t_file = deep_merge_dict(source=plugin.hints(t_file['metadata']), destination=t_file)
1051
1052
 
@@ -17,6 +17,7 @@ import sys
17
17
  from typing import TYPE_CHECKING, Any, Optional, TypeVar
18
18
 
19
19
  from rucio.common.config import config_get_int
20
+ from rucio.common.constants import DEFAULT_VO
20
21
  from rucio.common.exception import InvalidRequest
21
22
  from rucio.common.plugins import PolicyPackageAlgorithms
22
23
 
@@ -85,7 +86,11 @@ class FTS3TapeMetadataPlugin(PolicyPackageAlgorithms):
85
86
  """
86
87
  return {"collocation_hints": collocation_func(**hints)}
87
88
 
88
- def _default(self, *hints: dict) -> dict:
89
+ def _default(self, hint_dict: dict[str, Any]) -> dict:
90
+ vo = hint_dict['vo'] if 'vo' in hint_dict else DEFAULT_VO
91
+ default_algorithm = self._get_default_algorithm(self.ALGORITHM_NAME, vo=vo)
92
+ if default_algorithm is not None:
93
+ return default_algorithm(hint_dict)
89
94
  return {}
90
95
 
91
96
  def _verify_in_format(self, hint_dict: dict[str, Any]) -> None:
rucio/vcsversion.py CHANGED
@@ -4,8 +4,8 @@ This file is automatically generated; Do not edit it. :)
4
4
  '''
5
5
  VERSION_INFO = {
6
6
  'final': True,
7
- 'version': '37.7.1',
8
- 'branch_nick': 'release-37',
9
- 'revision_id': '87b1cc805709f99515d5f513e8a22af3042eca51',
10
- 'revno': 13828
7
+ 'version': '38.0.0',
8
+ 'branch_nick': 'master',
9
+ 'revision_id': 'f355202c14bffc0dff7c4f83c86c90fcce914472',
10
+ 'revno': 13928
11
11
  }
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING
19
19
  from urllib.parse import urlparse
20
20
 
21
21
  from flask import Blueprint, Flask, Response, redirect, render_template, request
22
+ from jinja2.exceptions import TemplateNotFound
22
23
  from werkzeug.datastructures import Headers
23
24
 
24
25
  from rucio.common.config import config_get
@@ -579,9 +580,17 @@ class CodeOIDC(ErrorHandlingMethodView):
579
580
  return render_template('auth_crash.html', crashtype='no_result'), 401, headers
580
581
 
581
582
  if 'fetchcode' in result:
582
- return render_template('auth_granted.html', authcode=result['fetchcode']), 200, headers
583
+ try:
584
+ return render_template('auth_granted.html', authcode=result['fetchcode']), 200, headers
585
+ except TemplateNotFound:
586
+ headers.set('Content-Type', 'text/plain')
587
+ return 'auth_granted.html missing', 500, headers
583
588
  elif 'polling' in result and result['polling'] is True:
584
- return render_template('auth_granted.html', authcode='allok'), 200, headers
589
+ try:
590
+ return render_template('auth_granted.html', authcode='allok'), 200, headers
591
+ except TemplateNotFound:
592
+ headers.set('Content-Type', 'text/plain')
593
+ return 'auth_granted.html missing', 500, headers
585
594
  else:
586
595
  headers.extend(error_headers('InvalidRequest', 'Cannot recognize and process your request'))
587
596
  return render_template('auth_crash.html', crashtype='bad_request'), 400, headers
@@ -20,11 +20,12 @@ import re
20
20
  from configparser import NoOptionError, NoSectionError
21
21
  from functools import wraps
22
22
  from time import time
23
- from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union
23
+ from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, cast
24
24
  from urllib.parse import unquote_plus
25
25
 
26
26
  import flask
27
27
  from flask.views import MethodView
28
+ from typing_extensions import ParamSpec
28
29
  from werkzeug.datastructures import Headers
29
30
  from werkzeug.exceptions import HTTPException
30
31
  from werkzeug.wrappers import Request, Response
@@ -183,25 +184,44 @@ def response_headers(response: ResponseTypeVar) -> ResponseTypeVar:
183
184
  return response
184
185
 
185
186
 
186
- def check_accept_header_wrapper_flask(supported_content_types: 'Iterable[str]'):
187
- """ Decorator to check if an endpoint supports the requested content type. """
187
+ P = ParamSpec('P')
188
+ R = TypeVar('R')
189
+
190
+
191
+ def check_accept_header_wrapper_flask(
192
+ supported_content_types: 'Iterable[str]'
193
+ ) -> 'Callable[[Callable[P, R]], Callable[P, R]]':
194
+ """Decorator that refuses requests with an unsupported *Accept* header."""
195
+
196
+ def wrapper(
197
+ f: 'Callable[P, R]'
198
+ ) -> 'Callable[P, R]':
199
+ """Decorate *f* with an *Accept*-header check and return the new callable."""
188
200
 
189
- def wrapper(f):
190
201
  @wraps(f)
191
- def decorated(*args, **kwargs):
202
+ def decorated(*args: 'P.args', **kwargs: 'P.kwargs') -> 'R':
203
+ """Run the header check, then delegate to *f* (or return 406)."""
204
+
205
+ # 1. no Accept header → accept everything
192
206
  if not flask.request.accept_mimetypes.provided:
193
- # accept anything, if Accept header is not provided
194
207
  return f(*args, **kwargs)
195
208
 
196
- for supported in supported_content_types:
197
- if supported in flask.request.accept_mimetypes:
198
- return f(*args, **kwargs)
209
+ # 2. at least one acceptable media‑type → call the view
210
+ if any(s in flask.request.accept_mimetypes for s in supported_content_types):
211
+ return f(*args, **kwargs)
199
212
 
200
- # none matched..
201
- return generate_http_error_flask(
202
- status_code=406,
203
- exc=UnsupportedRequestedContentType.__name__,
204
- exc_msg=f'The requested content type {flask.request.environ.get("HTTP_ACCEPT")} is not supported. Use {supported_content_types}.'
213
+ # 3. none matched → 406 response
214
+ return cast(
215
+ 'R',
216
+ generate_http_error_flask(
217
+ status_code=406,
218
+ exc=UnsupportedRequestedContentType.__name__,
219
+ exc_msg=(
220
+ f'The requested content type '
221
+ f'{flask.request.environ.get("HTTP_ACCEPT")} is not supported. '
222
+ f'Use {supported_content_types}.'
223
+ ),
224
+ ),
205
225
  )
206
226
 
207
227
  return decorated
@@ -281,7 +281,7 @@ class OptionSet(ErrorHandlingMethodView):
281
281
  return generate_http_error_flask(500, error, f"Could not set value '{value}' for section '{section}' option '{option}'")
282
282
 
283
283
 
284
- def blueprint():
284
+ def blueprint() -> AuthenticatedBlueprint:
285
285
  bp = AuthenticatedBlueprint('config', __name__, url_prefix='/config')
286
286
 
287
287
  option_set_view = OptionSet.as_view('option_set')