rucio 38.2.0__py3-none-any.whl → 38.4.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 (142) hide show
  1. rucio/cli/bin_legacy/rucio.py +26 -23
  2. rucio/cli/command.py +36 -26
  3. rucio/cli/config.py +22 -7
  4. rucio/cli/did.py +2 -2
  5. rucio/cli/download.py +1 -1
  6. rucio/cli/opendata.py +78 -10
  7. rucio/cli/utils.py +13 -1
  8. rucio/client/accountclient.py +20 -19
  9. rucio/client/accountlimitclient.py +5 -4
  10. rucio/client/baseclient.py +25 -25
  11. rucio/client/configclient.py +29 -5
  12. rucio/client/credentialclient.py +2 -1
  13. rucio/client/didclient.py +33 -32
  14. rucio/client/diracclient.py +2 -1
  15. rucio/client/exportclient.py +2 -1
  16. rucio/client/importclient.py +2 -1
  17. rucio/client/lifetimeclient.py +3 -2
  18. rucio/client/lockclient.py +4 -3
  19. rucio/client/metaconventionsclient.py +5 -4
  20. rucio/client/opendataclient.py +8 -7
  21. rucio/client/pingclient.py +2 -1
  22. rucio/client/replicaclient.py +27 -26
  23. rucio/client/requestclient.py +8 -8
  24. rucio/client/richclient.py +6 -0
  25. rucio/client/rseclient.py +31 -28
  26. rucio/client/ruleclient.py +13 -12
  27. rucio/client/scopeclient.py +4 -3
  28. rucio/client/subscriptionclient.py +6 -5
  29. rucio/common/constants.py +23 -0
  30. rucio/common/exception.py +30 -0
  31. rucio/common/plugins.py +33 -15
  32. rucio/common/utils.py +3 -3
  33. rucio/core/config.py +8 -6
  34. rucio/core/credential.py +19 -26
  35. rucio/core/did.py +1 -1
  36. rucio/core/did_meta_plugins/did_column_meta.py +226 -69
  37. rucio/core/opendata.py +150 -8
  38. rucio/core/replica.py +3 -4
  39. rucio/core/request.py +1 -1
  40. rucio/core/rule.py +6 -3
  41. rucio/core/rule_grouping.py +5 -5
  42. rucio/gateway/account.py +8 -7
  43. rucio/gateway/config.py +2 -37
  44. rucio/gateway/opendata.py +2 -2
  45. rucio/gateway/request.py +2 -117
  46. rucio/gateway/rule.py +2 -2
  47. rucio/rse/protocols/webdav.py +5 -2
  48. rucio/rse/translation.py +3 -3
  49. rucio/transfertool/fts3.py +0 -19
  50. rucio/transfertool/fts3_plugins.py +3 -3
  51. rucio/vcsversion.py +3 -3
  52. rucio/web/rest/flaskapi/v1/accountlimits.py +4 -3
  53. rucio/web/rest/flaskapi/v1/accounts.py +26 -25
  54. rucio/web/rest/flaskapi/v1/archives.py +2 -2
  55. rucio/web/rest/flaskapi/v1/auth.py +15 -14
  56. rucio/web/rest/flaskapi/v1/common.py +4 -4
  57. rucio/web/rest/flaskapi/v1/config.py +57 -17
  58. rucio/web/rest/flaskapi/v1/credentials.py +3 -3
  59. rucio/web/rest/flaskapi/v1/dids.py +25 -24
  60. rucio/web/rest/flaskapi/v1/dirac.py +3 -2
  61. rucio/web/rest/flaskapi/v1/export.py +4 -2
  62. rucio/web/rest/flaskapi/v1/heartbeats.py +2 -1
  63. rucio/web/rest/flaskapi/v1/identities.py +5 -4
  64. rucio/web/rest/flaskapi/v1/import.py +3 -2
  65. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +3 -2
  66. rucio/web/rest/flaskapi/v1/locks.py +4 -3
  67. rucio/web/rest/flaskapi/v1/meta_conventions.py +4 -3
  68. rucio/web/rest/flaskapi/v1/metrics.py +2 -1
  69. rucio/web/rest/flaskapi/v1/nongrid_traces.py +2 -1
  70. rucio/web/rest/flaskapi/v1/opendata.py +7 -6
  71. rucio/web/rest/flaskapi/v1/opendata_public.py +6 -5
  72. rucio/web/rest/flaskapi/v1/ping.py +3 -2
  73. rucio/web/rest/flaskapi/v1/redirect.py +4 -3
  74. rucio/web/rest/flaskapi/v1/replicas.py +31 -31
  75. rucio/web/rest/flaskapi/v1/requests.py +7 -7
  76. rucio/web/rest/flaskapi/v1/rses.py +23 -16
  77. rucio/web/rest/flaskapi/v1/rules.py +9 -8
  78. rucio/web/rest/flaskapi/v1/scopes.py +4 -3
  79. rucio/web/rest/flaskapi/v1/subscriptions.py +9 -8
  80. rucio/web/rest/flaskapi/v1/traces.py +2 -1
  81. rucio/web/rest/flaskapi/v1/vos.py +4 -3
  82. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/METADATA +1 -1
  83. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/RECORD +142 -142
  84. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/alembic.ini.template +0 -0
  85. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
  86. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  87. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
  88. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  89. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  90. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  91. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  92. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  93. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  94. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  95. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  96. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
  97. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
  98. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/requirements.server.txt +0 -0
  99. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/tools/bootstrap.py +0 -0
  100. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  101. {rucio-38.2.0.data → rucio-38.4.0.data}/data/rucio/tools/reset_database.py +0 -0
  102. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio +0 -0
  103. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-abacus-account +0 -0
  104. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-abacus-collection-replica +0 -0
  105. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-abacus-rse +0 -0
  106. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-admin +0 -0
  107. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-atropos +0 -0
  108. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-auditor +0 -0
  109. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-automatix +0 -0
  110. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-bb8 +0 -0
  111. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-cache-client +0 -0
  112. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-cache-consumer +0 -0
  113. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-finisher +0 -0
  114. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-poller +0 -0
  115. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-preparer +0 -0
  116. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-receiver +0 -0
  117. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-stager +0 -0
  118. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-submitter +0 -0
  119. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-conveyor-throttler +0 -0
  120. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-dark-reaper +0 -0
  121. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-dumper +0 -0
  122. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-follower +0 -0
  123. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-hermes +0 -0
  124. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-judge-cleaner +0 -0
  125. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-judge-evaluator +0 -0
  126. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-judge-injector +0 -0
  127. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-judge-repairer +0 -0
  128. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-kronos +0 -0
  129. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-minos +0 -0
  130. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
  131. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-necromancer +0 -0
  132. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-oauth-manager +0 -0
  133. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-reaper +0 -0
  134. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-replica-recoverer +0 -0
  135. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-rse-decommissioner +0 -0
  136. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-storage-consistency-actions +0 -0
  137. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-transmogrifier +0 -0
  138. {rucio-38.2.0.data → rucio-38.4.0.data}/scripts/rucio-undertaker +0 -0
  139. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/WHEEL +0 -0
  140. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
  141. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/licenses/LICENSE +0 -0
  142. {rucio-38.2.0.dist-info → rucio-38.4.0.dist-info}/top_level.txt +0 -0
@@ -19,6 +19,7 @@ from urllib.parse import quote_plus
19
19
  from requests.status_codes import codes
20
20
 
21
21
  from rucio.client.baseclient import BaseClient, choice
22
+ from rucio.common.constants import HTTPMethod
22
23
  from rucio.common.utils import build_url
23
24
 
24
25
  if TYPE_CHECKING:
@@ -110,7 +111,7 @@ class RuleClient(BaseClient):
110
111
  'activity': activity, 'notify': notify, 'purge_replicas': purge_replicas,
111
112
  'ignore_availability': ignore_availability, 'comment': comment, 'ask_approval': ask_approval,
112
113
  'asynchronous': asynchronous, 'delay_injection': delay_injection, 'priority': priority, 'meta': meta})
113
- r = self._send_request(url, type_='POST', data=data)
114
+ r = self._send_request(url, method=HTTPMethod.POST, data=data)
114
115
  if r.status_code == codes.created:
115
116
  return loads(r.text)
116
117
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -140,7 +141,7 @@ class RuleClient(BaseClient):
140
141
 
141
142
  data = dumps({'purge_replicas': purge_replicas})
142
143
 
143
- r = self._send_request(url, type_='DEL', data=data)
144
+ r = self._send_request(url, method=HTTPMethod.DELETE, data=data)
144
145
 
145
146
  if r.status_code == codes.ok:
146
147
  return True
@@ -162,7 +163,7 @@ class RuleClient(BaseClient):
162
163
  """
163
164
  path = self.RULE_BASEURL + '/' + rule_id
164
165
  url = build_url(choice(self.list_hosts), path=path)
165
- r = self._send_request(url, type_='GET')
166
+ r = self._send_request(url, method=HTTPMethod.GET)
166
167
  if r.status_code == codes.ok:
167
168
  return next(self._load_json_data(r))
168
169
  else:
@@ -185,7 +186,7 @@ class RuleClient(BaseClient):
185
186
  path = self.RULE_BASEURL + '/' + rule_id
186
187
  url = build_url(choice(self.list_hosts), path=path)
187
188
  data = dumps({'options': options})
188
- r = self._send_request(url, type_='PUT', data=data)
189
+ r = self._send_request(url, method=HTTPMethod.PUT, data=data)
189
190
  if r.status_code == codes.ok:
190
191
  return True
191
192
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -216,7 +217,7 @@ class RuleClient(BaseClient):
216
217
  path = self.RULE_BASEURL + '/' + rule_id + '/reduce'
217
218
  url = build_url(choice(self.list_hosts), path=path)
218
219
  data = dumps({'copies': copies, 'exclude_expression': exclude_expression})
219
- r = self._send_request(url, type_='POST', data=data)
220
+ r = self._send_request(url, method=HTTPMethod.POST, data=data)
220
221
  if r.status_code == codes.ok:
221
222
  return loads(r.text)
222
223
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -252,7 +253,7 @@ class RuleClient(BaseClient):
252
253
  'rse_expression': rse_expression,
253
254
  'override': override,
254
255
  })
255
- r = self._send_request(url, type_='POST', data=data)
256
+ r = self._send_request(url, method=HTTPMethod.POST, data=data)
256
257
  if r.status_code == codes.created:
257
258
  return loads(r.text)
258
259
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -273,7 +274,7 @@ class RuleClient(BaseClient):
273
274
  path = self.RULE_BASEURL + '/' + rule_id
274
275
  url = build_url(choice(self.list_hosts), path=path)
275
276
  data = dumps({'options': {'approve': True}})
276
- r = self._send_request(url, type_='PUT', data=data)
277
+ r = self._send_request(url, method=HTTPMethod.PUT, data=data)
277
278
  if r.status_code == codes.ok:
278
279
  return True
279
280
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -300,7 +301,7 @@ class RuleClient(BaseClient):
300
301
  if reason:
301
302
  options['comment'] = reason
302
303
  data = dumps({'options': options})
303
- r = self._send_request(url, type_='PUT', data=data)
304
+ r = self._send_request(url, method=HTTPMethod.PUT, data=data)
304
305
  if r.status_code == codes.ok:
305
306
  return True
306
307
  exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
@@ -321,7 +322,7 @@ class RuleClient(BaseClient):
321
322
  """
322
323
  path = '/'.join([self.RULE_BASEURL, quote_plus(scope), quote_plus(name), 'history'])
323
324
  url = build_url(choice(self.list_hosts), path=path)
324
- r = self._send_request(url, type_='GET')
325
+ r = self._send_request(url, method=HTTPMethod.GET)
325
326
  if r.status_code == codes.ok:
326
327
  return self._load_json_data(r)
327
328
  exc_cls, exc_msg = self._get_exception(r.headers, r.status_code)
@@ -341,7 +342,7 @@ class RuleClient(BaseClient):
341
342
  """
342
343
  path = self.RULE_BASEURL + '/' + rule_id + '/analysis'
343
344
  url = build_url(choice(self.list_hosts), path=path)
344
- r = self._send_request(url, type_='GET')
345
+ r = self._send_request(url, method=HTTPMethod.GET)
345
346
  if r.status_code == codes.ok:
346
347
  return next(self._load_json_data(r))
347
348
  exc_cls, exc_msg = self._get_exception(r.headers, r.status_code)
@@ -361,7 +362,7 @@ class RuleClient(BaseClient):
361
362
  """
362
363
  path = self.RULE_BASEURL + '/' + rule_id + '/locks'
363
364
  url = build_url(choice(self.list_hosts), path=path)
364
- r = self._send_request(url, type_='GET')
365
+ r = self._send_request(url, method=HTTPMethod.GET)
365
366
  if r.status_code == codes.ok:
366
367
  return self._load_json_data(r)
367
368
  exc_cls, exc_msg = self._get_exception(r.headers, r.status_code)
@@ -382,7 +383,7 @@ class RuleClient(BaseClient):
382
383
  filters = filters or {}
383
384
  path = self.RULE_BASEURL + '/'
384
385
  url = build_url(choice(self.list_hosts), path=path)
385
- r = self._send_request(url, type_='GET', params=filters)
386
+ r = self._send_request(url, method=HTTPMethod.GET, params=filters)
386
387
  if r.status_code == codes.ok:
387
388
  return self._load_json_data(r)
388
389
  else:
@@ -18,6 +18,7 @@ from urllib.parse import quote_plus
18
18
  from requests.status_codes import codes
19
19
 
20
20
  from rucio.client.baseclient import BaseClient, choice
21
+ from rucio.common.constants import HTTPMethod
21
22
  from rucio.common.utils import build_url
22
23
 
23
24
 
@@ -56,7 +57,7 @@ class ScopeClient(BaseClient):
56
57
 
57
58
  path = '/'.join([self.SCOPE_BASEURL, account, 'scopes', quote_plus(scope)])
58
59
  url = build_url(choice(self.list_hosts), path=path)
59
- r = self._send_request(url, type_='POST')
60
+ r = self._send_request(url, method=HTTPMethod.POST)
60
61
  if r.status_code == codes.created:
61
62
  return True
62
63
  else:
@@ -74,7 +75,7 @@ class ScopeClient(BaseClient):
74
75
 
75
76
  path = '/'.join(['scopes/'])
76
77
  url = build_url(choice(self.list_hosts), path=path)
77
- r = self._send_request(url)
78
+ r = self._send_request(url, method=HTTPMethod.GET)
78
79
  if r.status_code == codes.ok:
79
80
  scopes = loads(r.text)
80
81
  return scopes
@@ -106,7 +107,7 @@ class ScopeClient(BaseClient):
106
107
  path = '/'.join([self.SCOPE_BASEURL, account, 'scopes/'])
107
108
  url = build_url(choice(self.list_hosts), path=path)
108
109
 
109
- r = self._send_request(url)
110
+ r = self._send_request(url, method=HTTPMethod.GET)
110
111
  if r.status_code == codes.ok:
111
112
  scopes = loads(r.text)
112
113
  return scopes
@@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
18
18
  from requests.status_codes import codes
19
19
 
20
20
  from rucio.client.baseclient import BaseClient, choice
21
+ from rucio.common.constants import HTTPMethod
21
22
  from rucio.common.utils import build_url
22
23
 
23
24
  if TYPE_CHECKING:
@@ -78,7 +79,7 @@ class SubscriptionClient(BaseClient):
78
79
  raise TypeError('replication_rules should be a list')
79
80
  data = dumps({'options': {'filter': filter_, 'replication_rules': replication_rules, 'comments': comments,
80
81
  'lifetime': lifetime, 'retroactive': retroactive, 'dry_run': dry_run, 'priority': priority}})
81
- result = self._send_request(url, type_='POST', data=data)
82
+ result = self._send_request(url, method=HTTPMethod.POST, data=data)
82
83
  if result.status_code == codes.created: # pylint: disable=no-member
83
84
  return result.text
84
85
  else:
@@ -120,7 +121,7 @@ class SubscriptionClient(BaseClient):
120
121
  else:
121
122
  path += '/'
122
123
  url = build_url(choice(self.list_hosts), path=path)
123
- result = self._send_request(url, type_='GET')
124
+ result = self._send_request(url, method=HTTPMethod.GET)
124
125
  if result.status_code == codes.ok: # pylint: disable=no-member
125
126
  return self._load_json_data(result)
126
127
  if result.status_code == codes.not_found:
@@ -173,7 +174,7 @@ class SubscriptionClient(BaseClient):
173
174
  raise TypeError('replication_rules should be a list')
174
175
  data = dumps({'options': {'filter': filter_, 'replication_rules': replication_rules, 'comments': comments,
175
176
  'lifetime': lifetime, 'retroactive': retroactive, 'dry_run': dry_run, 'priority': priority}})
176
- result = self._send_request(url, type_='PUT', data=data)
177
+ result = self._send_request(url, method=HTTPMethod.PUT, data=data)
177
178
  if result.status_code == codes.created: # pylint: disable=no-member
178
179
  return True
179
180
  else:
@@ -203,7 +204,7 @@ class SubscriptionClient(BaseClient):
203
204
  path = self.SUB_BASEURL + '/' + account + '/' + name # type: ignore
204
205
  url = build_url(choice(self.list_hosts), path=path)
205
206
  data = dumps({'options': {'state': 'I'}})
206
- result = self._send_request(url, type_='PUT', data=data)
207
+ result = self._send_request(url, method=HTTPMethod.PUT, data=data)
207
208
  if result.status_code == codes.created: # pylint: disable=no-member
208
209
  return True
209
210
  else:
@@ -228,7 +229,7 @@ class SubscriptionClient(BaseClient):
228
229
 
229
230
  path = '/'.join([self.SUB_BASEURL, account, name, 'rules'])
230
231
  url = build_url(choice(self.list_hosts), path=path)
231
- result = self._send_request(url, type_='GET')
232
+ result = self._send_request(url, method=HTTPMethod.GET)
232
233
  if result.status_code == codes.ok: # pylint: disable=no-member
233
234
  return self._load_json_data(result)
234
235
  else:
rucio/common/constants.py CHANGED
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import enum
16
+ import sys
16
17
  from collections import namedtuple
17
18
  from typing import Literal, get_args
18
19
 
@@ -29,6 +30,8 @@ RESERVED_KEYS = ['scope', 'name', 'account', 'did_type', 'is_open', 'monotonic',
29
30
 
30
31
  DEFAULT_VO = 'def'
31
32
 
33
+ DEFAULT_ACTIVITY = 'User Subscriptions'
34
+
32
35
  KEY_TYPES = ['ALL', 'COLLECTION', 'FILE', 'DERIVED']
33
36
  # all(container, dataset, file), collection(dataset or container), file, derived(compute from file for collection)
34
37
 
@@ -219,3 +222,23 @@ SUPPORTED_SIGN_URL_SERVICES = list(get_args(SUPPORTED_SIGN_URL_SERVICES_LITERAL)
219
222
 
220
223
  OPENDATA_DID_STATE_LITERAL = Literal['draft', 'public', 'suspended']
221
224
  OPENDATA_DID_STATE_LITERAL_LIST = list(get_args(OPENDATA_DID_STATE_LITERAL))
225
+
226
+ POLICY_ALGORITHM_TYPES_LITERAL = Literal['non_deterministic_pfn', 'scope', 'lfn2pfn', 'pfn2lfn', 'fts3_tape_metadata_plugins', 'fts3_plugins_init', 'auto_approve']
227
+ POLICY_ALGORITHM_TYPES = list(get_args(POLICY_ALGORITHM_TYPES_LITERAL))
228
+
229
+ # https://github.com/rucio/rucio/issues/7958
230
+ # When Python 3.11 is the minimum supported version, we can use the standard library enum and remove this logic
231
+ if sys.version_info >= (3, 11):
232
+ from http import HTTPMethod
233
+ else:
234
+ @enum.unique
235
+ class HTTPMethod(str, enum.Enum):
236
+ """HTTP verbs used in Rucio requests."""
237
+
238
+ HEAD = "HEAD"
239
+ OPTIONS = "OPTIONS"
240
+ PATCH = "PATCH"
241
+ GET = "GET"
242
+ POST = "POST"
243
+ PUT = "PUT"
244
+ DELETE = "DELETE"
rucio/common/exception.py CHANGED
@@ -1261,3 +1261,33 @@ class OpenDataInvalidStateUpdate(OpenDataError):
1261
1261
  super(OpenDataInvalidStateUpdate, self).__init__(*args)
1262
1262
  self._message = "Invalid state update attempted on open data entry."
1263
1263
  self.error_code = 119
1264
+
1265
+
1266
+ class InvalidPolicyPackageAlgorithmType(RucioException):
1267
+ """
1268
+ Thrown when an unknown algorithm type name is encountered.
1269
+ """
1270
+ def __init__(self, param: str, *args):
1271
+ super(InvalidPolicyPackageAlgorithmType, self).__init__(*args)
1272
+ self._message = f"Invalid policy package algorithm type '{param}'."
1273
+ self.error_code = 120
1274
+
1275
+
1276
+ class InvalidAccountType(RucioException):
1277
+ """
1278
+ Thrown when an account is created with an invalid type
1279
+ """
1280
+ def __init__(self, *args):
1281
+ super(InvalidAccountType, self).__init__(*args)
1282
+ self._message = "Cannot create an account with an invalid type."
1283
+ self.error_code = 121
1284
+
1285
+ class OpenDataDuplicateDOI(OpenDataError):
1286
+ """
1287
+ Throws when a data identifier with the same DOI already exists in the open data catalog.
1288
+ """
1289
+
1290
+ def __init__(self, doi: str, *args):
1291
+ super(OpenDataDuplicateDOI, self).__init__(*args)
1292
+ self._message = f"Data identifier with the same DOI ({doi}) already exists in the open data catalog."
1293
+ self.error_code = 122
rucio/common/plugins.py CHANGED
@@ -22,8 +22,8 @@ from packaging.specifiers import SpecifierSet
22
22
 
23
23
  from rucio.common import config
24
24
  from rucio.common.client import get_client_vo
25
- from rucio.common.constants import DEFAULT_VO
26
- from rucio.common.exception import InvalidAlgorithmName, PolicyPackageIsNotVersioned, PolicyPackageVersionError
25
+ from rucio.common.constants import DEFAULT_VO, POLICY_ALGORITHM_TYPES, POLICY_ALGORITHM_TYPES_LITERAL
26
+ from rucio.common.exception import InvalidAlgorithmName, InvalidPolicyPackageAlgorithmType, PolicyPackageIsNotVersioned, PolicyPackageVersionError
27
27
  from rucio.version import current_version
28
28
 
29
29
  if TYPE_CHECKING:
@@ -75,9 +75,9 @@ class PolicyPackageAlgorithms:
75
75
  - the key is the algorithm type
76
76
  - the value is a dictionary of algorithm names and their callables
77
77
  """
78
- _ALGORITHMS: dict[str, dict[str, 'Callable[..., Any]']] = {}
78
+ _ALGORITHMS: dict[POLICY_ALGORITHM_TYPES_LITERAL, dict[str, 'Callable[..., Any]']] = {}
79
79
  _loaded_policy_modules = False
80
- _default_algorithms: dict[str, 'Callable[..., Any]'] = {}
80
+ _default_algorithms: dict[str, Optional['Callable[..., Any]']] = {}
81
81
 
82
82
  def __init__(self) -> None:
83
83
  if not self._loaded_policy_modules:
@@ -85,12 +85,15 @@ class PolicyPackageAlgorithms:
85
85
  self._loaded_policy_modules = True
86
86
 
87
87
  @classmethod
88
- def _get_default_algorithm(cls: type[PolicyPackageAlgorithmsT], algorithm_type: str, vo: str = "") -> Optional['Callable[..., Any]']:
88
+ def _get_default_algorithm(cls: type[PolicyPackageAlgorithmsT], algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL, vo: str = "") -> Optional['Callable[..., Any]']:
89
89
  """
90
90
  Gets the default algorithm of this type, if present in the policy package.
91
91
  The default algorithm is the function named algorithm_type within the module named algorithm_type.
92
92
  Returns None if no default algorithm present.
93
93
  """
94
+ if algorithm_type not in POLICY_ALGORITHM_TYPES:
95
+ raise InvalidPolicyPackageAlgorithmType(algorithm_type)
96
+
94
97
  # check if default algorithm for this VO is already cached
95
98
  type_for_vo = vo + "_" + algorithm_type
96
99
  if type_for_vo in cls._default_algorithms:
@@ -102,52 +105,71 @@ class PolicyPackageAlgorithms:
102
105
  vo = ''
103
106
  package = cls._get_policy_package_name(vo)
104
107
  except (NoOptionError, NoSectionError):
108
+ cls._default_algorithms[type_for_vo] = default_algorithm
105
109
  return default_algorithm
106
110
 
107
111
  module_name = package + "." + algorithm_type
112
+ LOGGER.info('Attempting to find algorithm %s in default location %s...' % (algorithm_type, module_name))
108
113
  try:
109
114
  module = importlib.import_module(module_name)
110
115
 
111
116
  if hasattr(module, algorithm_type):
112
117
  default_algorithm = getattr(module, algorithm_type)
113
- cls._default_algorithms[type_for_vo] = default_algorithm
118
+ except ModuleNotFoundError:
119
+ LOGGER.info('Algorithm %s not found in default location %s' % (algorithm_type, module_name))
114
120
  except ImportError:
115
- LOGGER.info('Policy algorithm module %s could not be loaded' % module_name)
121
+ LOGGER.info('Algorithm %s found in default location %s, but could not be loaded' % (algorithm_type, module_name))
122
+ # if the default algorithm is not present, this will store None and we will
123
+ # not attempt to load the same algorithm again
124
+ cls._default_algorithms[type_for_vo] = default_algorithm
116
125
  return default_algorithm
117
126
 
118
127
  @classmethod
119
- def _get_one_algorithm(cls: type[PolicyPackageAlgorithmsT], algorithm_type: str, name: str) -> 'Callable[..., Any]':
128
+ def _get_one_algorithm(cls: type[PolicyPackageAlgorithmsT], algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL, name: str) -> 'Callable[..., Any]':
120
129
  """
121
130
  Get the algorithm from the dictionary of algorithms
122
131
  """
132
+ if algorithm_type not in POLICY_ALGORITHM_TYPES:
133
+ raise InvalidPolicyPackageAlgorithmType(algorithm_type)
123
134
  return cls._ALGORITHMS[algorithm_type][name]
124
135
 
125
136
  @classmethod
126
- def _get_algorithms(cls: type[PolicyPackageAlgorithmsT], algorithm_type: str) -> dict[str, 'Callable[..., Any]']:
137
+ def _get_algorithms(cls: type[PolicyPackageAlgorithmsT], algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL) -> dict[str, 'Callable[..., Any]']:
127
138
  """
128
139
  Get the dictionary of algorithms for a given type
129
140
  """
141
+ if algorithm_type not in POLICY_ALGORITHM_TYPES:
142
+ raise InvalidPolicyPackageAlgorithmType(algorithm_type)
130
143
  return cls._ALGORITHMS[algorithm_type]
131
144
 
132
145
  @classmethod
133
146
  def _register(
134
147
  cls: type[PolicyPackageAlgorithmsT],
135
- algorithm_type: str, algorithm_dict: dict[str, 'Callable[..., Any]']) -> None:
148
+ algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL,
149
+ algorithm_dict: dict[str, 'Callable[..., Any]']) -> None:
136
150
  """
137
151
  Provided a dictionary of callable function,
138
152
  and the associated algorithm type,
139
153
  register it as one of the valid algorithms.
140
154
  """
155
+ if algorithm_type not in POLICY_ALGORITHM_TYPES:
156
+ raise InvalidPolicyPackageAlgorithmType(algorithm_type)
157
+
141
158
  if algorithm_type in cls._ALGORITHMS:
142
159
  cls._ALGORITHMS[algorithm_type].update(algorithm_dict)
143
160
  else:
144
161
  cls._ALGORITHMS[algorithm_type] = algorithm_dict
145
162
 
146
163
  @classmethod
147
- def _supports(cls: type[PolicyPackageAlgorithmsT], algorithm_type: str, name: str) -> bool:
164
+ def _supports(
165
+ cls: type[PolicyPackageAlgorithmsT],
166
+ algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL,
167
+ name: str) -> bool:
148
168
  """
149
169
  Check if a algorithm is supported by the plugin
150
170
  """
171
+ if algorithm_type not in POLICY_ALGORITHM_TYPES:
172
+ raise InvalidPolicyPackageAlgorithmType(algorithm_type)
151
173
  return name in cls._ALGORITHMS.get(algorithm_type, {})
152
174
 
153
175
  @classmethod
@@ -196,10 +218,6 @@ class PolicyPackageAlgorithms:
196
218
  if hasattr(module, 'get_algorithms'):
197
219
  all_algorithms = module.get_algorithms()
198
220
 
199
- # for backward compatibility, rename 'surl' to 'non_deterministic_pfn' here
200
- if 'surl' in all_algorithms:
201
- all_algorithms['non_deterministic_pfn'] = all_algorithms['surl']
202
-
203
221
  # check that the names are correctly prefixed for multi-VO
204
222
  if vo:
205
223
  for _, algorithms in all_algorithms.items():
rucio/common/utils.py CHANGED
@@ -46,7 +46,7 @@ import requests
46
46
  from typing_extensions import ParamSpec
47
47
 
48
48
  from rucio.common.config import config_get, config_get_bool
49
- from rucio.common.constants import BASE_SCHEME_MAP, DEFAULT_VO
49
+ from rucio.common.constants import BASE_SCHEME_MAP, DEFAULT_VO, POLICY_ALGORITHM_TYPES_LITERAL
50
50
  from rucio.common.exception import DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, RucioException
51
51
  from rucio.common.extra import import_extras
52
52
  from rucio.common.plugins import PolicyPackageAlgorithms
@@ -392,7 +392,7 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
392
392
  from policy packages
393
393
  """
394
394
 
395
- _algorithm_type = 'non_deterministic_pfn'
395
+ _algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL = 'non_deterministic_pfn'
396
396
 
397
397
  def __init__(self, vo: str = DEFAULT_VO) -> None:
398
398
  """
@@ -560,7 +560,7 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
560
560
  Handle scope extraction algorithms
561
561
  """
562
562
 
563
- _algorithm_type = 'scope'
563
+ _algorithm_type: POLICY_ALGORITHM_TYPES_LITERAL = 'scope'
564
564
 
565
565
  def __init__(self, vo: str = DEFAULT_VO) -> None:
566
566
  """
rucio/core/config.py CHANGED
@@ -330,17 +330,19 @@ def remove_section(section: str, *, session: "Session") -> bool:
330
330
  return False
331
331
  else:
332
332
  stmt = select(
333
+ models.Config.opt,
333
334
  models.Config.value
334
335
  ).where(
335
336
  models.Config.section == section
336
337
  )
337
- for old in session.execute(stmt).all():
338
- old_option = models.ConfigHistory(section=old[0],
339
- opt=old[1],
340
- value=old[2])
338
+ for option, value in session.execute(stmt).all():
339
+ old_option = models.ConfigHistory(
340
+ section=section,
341
+ opt=option,
342
+ value=value)
341
343
  old_option.save(session=session)
342
- delete_from_cache(key=CacheKey.has_option(old[0], old[1]))
343
- delete_from_cache(key=CacheKey.value(old[0], old[1]))
344
+ delete_from_cache(key=CacheKey.has_option(section, option))
345
+ delete_from_cache(key=CacheKey.value(section, option))
344
346
 
345
347
  stmt = delete(
346
348
  models.Config
rucio/core/credential.py CHANGED
@@ -27,7 +27,7 @@ from google.oauth2.service_account import Credentials
27
27
 
28
28
  from rucio.common.cache import MemcacheRegion
29
29
  from rucio.common.config import config_get, get_rse_credentials
30
- from rucio.common.constants import RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS, RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL, SUPPORTED_SIGN_URL_SERVICES, SUPPORTED_SIGN_URL_SERVICES_LITERAL, RseAttr
30
+ from rucio.common.constants import RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS, RSE_BASE_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL, SUPPORTED_SIGN_URL_SERVICES, SUPPORTED_SIGN_URL_SERVICES_LITERAL, HTTPMethod, RseAttr
31
31
  from rucio.common.exception import UnsupportedOperation
32
32
  from rucio.core.monitor import MetricManager
33
33
  from rucio.core.rse import get_rse_attribute
@@ -51,7 +51,7 @@ def get_signed_url(
51
51
  The signed URL will be valid for 1 hour but can be overridden.
52
52
 
53
53
  :param rse_id: The ID of the RSE that the URL points to.
54
- :param service: The service to authorise, either 'gcs', 's3' or 'swift'.
54
+ :param service: The service to authorize, either 'gcs', 's3' or 'swift'.
55
55
  :param operation: The operation to sign, either 'read', 'write', or 'delete'.
56
56
  :param url: The URL to sign.
57
57
  :param lifetime: Lifetime of the signed URL in seconds.
@@ -69,6 +69,8 @@ def get_signed_url(
69
69
  if url is None or url == '':
70
70
  raise UnsupportedOperation('URL must not be empty')
71
71
 
72
+ operations_map = {'read': HTTPMethod.GET.value, 'write': HTTPMethod.PUT.value, 'delete': HTTPMethod.DELETE.value}
73
+
72
74
  if lifetime:
73
75
  if not isinstance(lifetime, int):
74
76
  try:
@@ -88,25 +90,21 @@ def get_signed_url(
88
90
  if lifetime is None:
89
91
  lifetime = 0
90
92
  else:
91
- # GCS is timezone-sensitive, don't use UTC
92
- # has to be converted to Unixtime
93
+ # GCS is timezone-sensitive, don't use UTC. Has to be converted to Unix time
93
94
  lifetime_datetime = datetime.datetime.now() + datetime.timedelta(seconds=lifetime)
94
95
  lifetime = int(time.mktime(lifetime_datetime.timetuple()))
95
96
 
96
97
  # sign the path only
97
98
  path = components.path
98
99
 
99
- # Map operations
100
- operations = {'read': 'GET', 'write': 'PUT', 'delete': 'DELETE'}
101
-
102
- # assemble message to sign
103
- to_sign = "%s\n\n\n%s\n%s" % (operations[operation], lifetime, path)
100
+ # assemble a message to sign
101
+ to_sign = "%s\n\n\n%s\n%s" % (operations_map[operation], lifetime, path)
104
102
 
105
103
  # create URL-capable signature
106
104
  # first character is always a '=', remove it
107
105
  signature = urlencode({'': base64.b64encode(CREDS_GCS.sign_bytes(to_sign))})[1:]
108
106
 
109
- # assemble final signed URL
107
+ # assemble the final signed URL
110
108
  signed_url = (
111
109
  f'https://{host}{path}'
112
110
  f'?GoogleAccessId={CREDS_GCS.service_account_email}'
@@ -127,18 +125,18 @@ def get_signed_url(
127
125
  # split URL to get hostname, bucket and key
128
126
  components = urlparse(url)
129
127
  host = components.netloc
130
- pathcomponents = components.path.split('/')
128
+ path_components = components.path.split('/')
131
129
  if s3_url_style == "path":
132
- if len(pathcomponents) < 3:
130
+ if len(path_components) < 3:
133
131
  raise UnsupportedOperation('Not a valid Path-Style S3 URL')
134
- bucket = pathcomponents[1]
135
- key = '/'.join(pathcomponents[2:])
132
+ bucket = path_components[1]
133
+ key = '/'.join(path_components[2:])
136
134
  elif s3_url_style == "host":
137
- hostcomponents = host.split('.')
138
- bucket = hostcomponents[0]
139
- if len(pathcomponents) < 2:
135
+ host_components = host.split('.')
136
+ bucket = host_components[0]
137
+ if len(path_components) < 2:
140
138
  raise UnsupportedOperation('Not a valid Host-Style S3 URL')
141
- key = '/'.join(pathcomponents[1:])
139
+ key = '/'.join(path_components[1:])
142
140
  else:
143
141
  raise UnsupportedOperation('Not a valid RSE S3 URL style (allowed values: path|host)')
144
142
 
@@ -185,7 +183,7 @@ def get_signed_url(
185
183
  s3op, Params={'Bucket': bucket, 'Key': key}, ExpiresIn=lifetime)
186
184
 
187
185
  else: # service == 'swift'
188
- # split URL to get hostname and path
186
+ # split URL to get the hostname and path
189
187
  components = urlparse(url)
190
188
  host = components.netloc
191
189
 
@@ -194,7 +192,7 @@ def get_signed_url(
194
192
  if colon >= 0:
195
193
  host = host[:colon]
196
194
 
197
- # use RSE ID to look up key
195
+ # use RSE ID to look up the key
198
196
  cred_name = rse_id
199
197
 
200
198
  # look up tempurl signing key
@@ -205,12 +203,7 @@ def get_signed_url(
205
203
  REGION.set('swift-%s' % cred_name, cred)
206
204
  tempurl_key = cred['tempurl_key']
207
205
 
208
- if operation == 'read':
209
- swiftop = 'GET'
210
- elif operation == 'write':
211
- swiftop = 'PUT'
212
- else:
213
- swiftop = 'DELETE'
206
+ swiftop = operations_map[operation]
214
207
 
215
208
  expires = int(time.time() + lifetime) # type: ignore (lifetime could be None)
216
209
 
rucio/core/did.py CHANGED
@@ -724,7 +724,7 @@ def __add_collections_to_container(
724
724
  for row in session.execute(stmt):
725
725
 
726
726
  if row.did_scope is None:
727
- raise exception.DataIdentifierNotFound("Data identifier '%(scope)s:%(name)s' not found" % row)
727
+ raise exception.DataIdentifierNotFound(f"Data identifier '{row.scope}:{row.name}' not found")
728
728
 
729
729
  if row.did_type == DIDType.FILE:
730
730
  raise exception.UnsupportedOperation("Adding a file (%s:%s) to a container (%s:%s) is forbidden" % (row.scope, row.name, parent_did.scope, parent_did.name))