rucio 37.6.0__py3-none-any.whl → 37.7.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 (139) hide show
  1. rucio/cli/bin_legacy/rucio.py +40 -21
  2. rucio/cli/rule.py +9 -5
  3. rucio/client/baseclient.py +4 -3
  4. rucio/client/downloadclient.py +2 -1
  5. rucio/client/exportclient.py +45 -4
  6. rucio/client/pingclient.py +35 -4
  7. rucio/client/touchclient.py +2 -1
  8. rucio/client/uploadclient.py +3 -2
  9. rucio/common/cache.py +1 -2
  10. rucio/common/client.py +4 -30
  11. rucio/common/config.py +26 -1
  12. rucio/common/constants.py +3 -1
  13. rucio/common/plugins.py +2 -2
  14. rucio/common/policy.py +3 -2
  15. rucio/common/schema/__init__.py +4 -3
  16. rucio/common/types.py +7 -5
  17. rucio/core/account.py +2 -1
  18. rucio/core/account_limit.py +3 -2
  19. rucio/core/did.py +8 -7
  20. rucio/core/dirac.py +2 -1
  21. rucio/core/distance.py +2 -1
  22. rucio/core/exporter.py +3 -2
  23. rucio/core/importer.py +5 -5
  24. rucio/core/permission/__init__.py +2 -1
  25. rucio/core/replica.py +5 -5
  26. rucio/core/request.py +2 -2
  27. rucio/core/rse.py +7 -7
  28. rucio/core/rule.py +8 -8
  29. rucio/core/transfer.py +2 -2
  30. rucio/core/vo.py +2 -1
  31. rucio/daemons/atropos/atropos.py +2 -1
  32. rucio/daemons/automatix/automatix.py +5 -5
  33. rucio/daemons/badreplicas/minos.py +3 -2
  34. rucio/daemons/bb8/bb8.py +2 -1
  35. rucio/daemons/bb8/nuclei_background_rebalance.py +2 -2
  36. rucio/daemons/conveyor/common.py +3 -3
  37. rucio/daemons/conveyor/submitter.py +2 -1
  38. rucio/daemons/hermes/hermes.py +27 -6
  39. rucio/daemons/reaper/dark_reaper.py +5 -4
  40. rucio/daemons/reaper/reaper.py +7 -7
  41. rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +3 -3
  42. rucio/daemons/tracer/kronos.py +3 -2
  43. rucio/daemons/transmogrifier/transmogrifier.py +70 -68
  44. rucio/daemons/undertaker/undertaker.py +2 -1
  45. rucio/db/sqla/models.py +2 -2
  46. rucio/db/sqla/util.py +3 -2
  47. rucio/gateway/account.py +13 -12
  48. rucio/gateway/account_limit.py +90 -116
  49. rucio/gateway/authentication.py +9 -8
  50. rucio/gateway/config.py +11 -10
  51. rucio/gateway/credential.py +2 -1
  52. rucio/gateway/did.py +32 -32
  53. rucio/gateway/dirac.py +2 -1
  54. rucio/gateway/exporter.py +2 -1
  55. rucio/gateway/heartbeat.py +3 -2
  56. rucio/gateway/identity.py +4 -3
  57. rucio/gateway/importer.py +2 -1
  58. rucio/gateway/lifetime_exception.py +4 -3
  59. rucio/gateway/lock.py +6 -5
  60. rucio/gateway/meta_conventions.py +3 -2
  61. rucio/gateway/permission.py +2 -1
  62. rucio/gateway/quarantined_replica.py +2 -1
  63. rucio/gateway/replica.py +18 -18
  64. rucio/gateway/request.py +10 -10
  65. rucio/gateway/rse.py +27 -26
  66. rucio/gateway/rule.py +12 -11
  67. rucio/gateway/scope.py +4 -3
  68. rucio/gateway/subscription.py +7 -6
  69. rucio/gateway/vo.py +5 -4
  70. rucio/rse/__init__.py +7 -6
  71. rucio/rse/rsemanager.py +5 -4
  72. rucio/rse/translation.py +2 -2
  73. rucio/tests/common.py +2 -1
  74. rucio/vcsversion.py +3 -3
  75. rucio/web/rest/flaskapi/v1/accountlimits.py +5 -5
  76. rucio/web/rest/flaskapi/v1/archives.py +2 -1
  77. rucio/web/rest/flaskapi/v1/common.py +4 -3
  78. rucio/web/rest/flaskapi/v1/dids.py +205 -154
  79. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/METADATA +1 -1
  80. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/RECORD +139 -139
  81. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/alembic.ini.template +0 -0
  82. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
  83. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  84. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
  85. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  86. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  87. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  88. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  89. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  90. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  91. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  92. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  93. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
  94. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
  95. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/requirements.server.txt +0 -0
  96. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/bootstrap.py +0 -0
  97. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  98. {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/reset_database.py +0 -0
  99. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio +0 -0
  100. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-account +0 -0
  101. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-collection-replica +0 -0
  102. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-rse +0 -0
  103. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-admin +0 -0
  104. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-atropos +0 -0
  105. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-auditor +0 -0
  106. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-automatix +0 -0
  107. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-bb8 +0 -0
  108. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-cache-client +0 -0
  109. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-cache-consumer +0 -0
  110. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-finisher +0 -0
  111. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-poller +0 -0
  112. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-preparer +0 -0
  113. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-receiver +0 -0
  114. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-stager +0 -0
  115. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-submitter +0 -0
  116. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-throttler +0 -0
  117. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-dark-reaper +0 -0
  118. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-dumper +0 -0
  119. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-follower +0 -0
  120. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-hermes +0 -0
  121. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-cleaner +0 -0
  122. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-evaluator +0 -0
  123. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-injector +0 -0
  124. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-repairer +0 -0
  125. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-kronos +0 -0
  126. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-minos +0 -0
  127. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
  128. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-necromancer +0 -0
  129. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-oauth-manager +0 -0
  130. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-reaper +0 -0
  131. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-replica-recoverer +0 -0
  132. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-rse-decommissioner +0 -0
  133. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-storage-consistency-actions +0 -0
  134. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-transmogrifier +0 -0
  135. {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-undertaker +0 -0
  136. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/WHEEL +0 -0
  137. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
  138. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/licenses/LICENSE +0 -0
  139. {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/top_level.txt +0 -0
rucio/tests/common.py CHANGED
@@ -28,6 +28,7 @@ import pytest
28
28
  import requests
29
29
 
30
30
  from rucio.common.config import config_get, config_get_bool, get_config_dirs
31
+ from rucio.common.constants import DEFAULT_VO
31
32
  from rucio.common.utils import execute
32
33
  from rucio.common.utils import generate_uuid as uuid
33
34
 
@@ -77,7 +78,7 @@ def get_long_vo() -> str:
77
78
  Don't map the name to a short version.
78
79
  :returns: VO name string.
79
80
  """
80
- vo_name = 'def'
81
+ vo_name = DEFAULT_VO
81
82
  if config_get_bool('common', 'multi_vo', raise_exception=False, default=False):
82
83
  vo = config_get('client', 'vo', raise_exception=False, default=None)
83
84
  if vo is not 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.6.0',
7
+ 'version': '37.7.0',
8
8
  'branch_nick': 'release-37',
9
- 'revision_id': '30f77937bd8dd8b480072448e64a5398dbad0c9f',
10
- 'revno': 13789
9
+ 'revision_id': '61ed028cbd532ede88189a81bee971db566fcdec',
10
+ 'revno': 13826
11
11
  }
@@ -72,7 +72,7 @@ class LocalAccountLimit(ErrorHandlingMethodView):
72
72
  parameters = json_parameters()
73
73
  bytes_param = param_get(parameters, 'bytes')
74
74
  try:
75
- set_local_account_limit(account=account, rse=rse, bytes_=bytes_param, issuer=request.environ.get('issuer'), vo=request.environ.get('vo'))
75
+ set_local_account_limit(account=account, rse=rse, bytes_=bytes_param, issuer=request.environ['issuer'], vo=request.environ['vo'])
76
76
  except AccessDenied as error:
77
77
  return generate_http_error_flask(401, error)
78
78
  except (RSENotFound, AccountNotFound) as error:
@@ -108,7 +108,7 @@ class LocalAccountLimit(ErrorHandlingMethodView):
108
108
  description: "No RSE or account found for the given id."
109
109
  """
110
110
  try:
111
- delete_local_account_limit(account=account, rse=rse, issuer=request.environ.get('issuer'), vo=request.environ.get('vo'))
111
+ delete_local_account_limit(account=account, rse=rse, issuer=request.environ['issuer'], vo=request.environ['vo'])
112
112
  except AccessDenied as error:
113
113
  return generate_http_error_flask(401, error)
114
114
  except (AccountNotFound, RSENotFound) as error:
@@ -168,8 +168,8 @@ class GlobalAccountLimit(ErrorHandlingMethodView):
168
168
  account=account,
169
169
  rse_expression=rse_expression,
170
170
  bytes_=bytes_param,
171
- issuer=request.environ.get('issuer'),
172
- vo=request.environ.get('vo'),
171
+ issuer=request.environ['issuer'],
172
+ vo=request.environ['vo'],
173
173
  )
174
174
  except AccessDenied as error:
175
175
  return generate_http_error_flask(401, error)
@@ -206,7 +206,7 @@ class GlobalAccountLimit(ErrorHandlingMethodView):
206
206
  description: "No RSE or account found for the given id."
207
207
  """
208
208
  try:
209
- delete_global_account_limit(account=account, rse_expression=rse_expression, issuer=request.environ.get('issuer'), vo=request.environ.get('vo'))
209
+ delete_global_account_limit(account=account, rse_expression=rse_expression, issuer=request.environ['issuer'], vo=request.environ['vo'])
210
210
  except AccessDenied as error:
211
211
  return generate_http_error_flask(401, error)
212
212
  except (AccountNotFound, RSENotFound) as error:
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING
17
17
 
18
18
  from flask import Flask, Response, request
19
19
 
20
+ from rucio.common.constants import DEFAULT_VO
20
21
  from rucio.gateway.did import list_archive_content
21
22
  from rucio.web.rest.flaskapi.authenticated_bp import AuthenticatedBlueprint
22
23
  from rucio.web.rest.flaskapi.v1.common import ErrorHandlingMethodView, check_accept_header_wrapper_flask, generate_http_error_flask, parse_scope_name, response_headers, try_stream
@@ -80,7 +81,7 @@ class Archive(ErrorHandlingMethodView):
80
81
  for file in list_archive_content(scope=scope, name=name, vo=vo):
81
82
  yield dumps(file) + '\n'
82
83
 
83
- return try_stream(generate(vo=request.environ.get('vo', 'def')))
84
+ return try_stream(generate(vo=request.environ.get('vo', DEFAULT_VO)))
84
85
  except ValueError as error:
85
86
  return generate_http_error_flask(400, error)
86
87
 
@@ -30,6 +30,7 @@ from werkzeug.exceptions import HTTPException
30
30
  from werkzeug.wrappers import Request, Response
31
31
 
32
32
  from rucio.common import config
33
+ from rucio.common.constants import DEFAULT_VO
33
34
  from rucio.common.exception import CannotAuthenticate, DatabaseException, IdentityError, RucioException, UnsupportedRequestedContentType
34
35
  from rucio.common.schema import get_schema_value
35
36
  from rucio.common.utils import generate_uuid, render_json
@@ -161,7 +162,7 @@ def request_auth_env() -> Optional['ResponseReturnValue']:
161
162
  logging.exception('Internal error in validate_auth_token')
162
163
  return 'Internal Error', 500
163
164
 
164
- flask.request.environ['vo'] = auth.get('vo', 'def')
165
+ flask.request.environ['vo'] = auth.get('vo', DEFAULT_VO)
165
166
  flask.request.environ['issuer'] = auth.get('account')
166
167
  flask.request.environ['identity'] = auth.get('identity')
167
168
  flask.request.environ['request_id'] = generate_uuid()
@@ -232,7 +233,7 @@ def parse_scope_name(scope_name: str, vo: Optional[str]) -> tuple[str, ...]:
232
233
  return scope, name
233
234
 
234
235
  if not vo:
235
- vo = 'def'
236
+ vo = DEFAULT_VO
236
237
 
237
238
  # The ':' in DID is replaced by '/', also an '/' is added. Why?
238
239
  pattern = get_schema_value('SCOPE_NAME_REGEXP', vo)
@@ -413,7 +414,7 @@ def extract_vo(headers: Headers) -> str:
413
414
  :returns: a string containing the short VO name.
414
415
  """
415
416
  try:
416
- return map_vo(headers.get('X-Rucio-VO', default='def'))
417
+ return map_vo(headers.get('X-Rucio-VO', default=DEFAULT_VO))
417
418
  except RucioException as err:
418
419
  # VO Name doesn't match allowed spec
419
420
  flask.abort(generate_http_error_flask(status_code=400, exc=err))
@@ -22,7 +22,6 @@ from rucio.common.exception import (
22
22
  DatabaseException,
23
23
  DataIdentifierAlreadyExists,
24
24
  DataIdentifierNotFound,
25
- Duplicate,
26
25
  DuplicateContent,
27
26
  FileAlreadyExists,
28
27
  FileConsistencyMismatch,
@@ -1295,172 +1294,304 @@ class Parents(ErrorHandlingMethodView):
1295
1294
  class Meta(ErrorHandlingMethodView):
1296
1295
 
1297
1296
  @check_accept_header_wrapper_flask(['application/json'])
1298
- def get(self, scope_name):
1297
+ def get(self, scope_name, key=None):
1299
1298
  """
1300
1299
  ---
1301
1300
  summary: Get metadata
1302
- description: "Get the metadata of a DID."
1301
+ description: "Retrieve the metadata of a data identifier (DID)."
1303
1302
  tags:
1304
1303
  - Data Identifiers
1305
1304
  parameters:
1306
1305
  - name: scope_name
1307
1306
  in: path
1308
- description: "The scope and the name of the DID."
1307
+ description: "The scope and the name of the DID (e.g., `scope:name`)."
1308
+ required: true
1309
+ style: simple
1309
1310
  schema:
1310
1311
  type: string
1311
- style: simple
1312
1312
  - name: plugin
1313
1313
  in: query
1314
- description: "The plugin to use."
1314
+ description: "The metadata plugin to use."
1315
+ required: false
1316
+ style: form
1315
1317
  schema:
1316
1318
  type: string
1317
1319
  default: DID_COLUMN
1318
1320
  responses:
1319
1321
  200:
1320
- description: "OK"
1322
+ description: "OK – returns the metadata of the DID."
1321
1323
  content:
1322
1324
  application/json:
1323
1325
  schema:
1324
- description: "A data identifier with all attributes."
1325
1326
  type: object
1327
+ description: "A JSON object containing all attributes of the DID."
1328
+ examples:
1329
+ defaultPlugin:
1330
+ summary: "Response produced by the default 'DID_COLUMN' plug-in"
1331
+ value:
1332
+ scope: "user"
1333
+ name: "dataset_123"
1334
+ did_type: "DATASET"
1335
+ bytes: 123456789
1336
+ length: 42
1337
+ account: "root"
1338
+ is_open: true
1339
+ suppressed: false
1340
+ created_at: "2025-05-20T12:16:58"
1341
+ updated_at: "2025-05-20T12:17:27"
1342
+ # ... rest DID fields
1343
+ jsonPlugin:
1344
+ summary: "Response produced by the 'JSON' plugin"
1345
+ value:
1346
+ custom_key1: "value1"
1347
+ custom_key2: "value2"
1348
+ # ... etc
1326
1349
  400:
1327
- description: "Bad Request - Invalid metadata plugin specified"
1350
+ description: "Bad Request invalid scope_name, or invalid metadata plugin specified."
1328
1351
  401:
1329
- description: "Invalid Auth Token"
1352
+ description: "Unauthorized – invalid Auth Token."
1330
1353
  404:
1331
- description: "DID not found"
1354
+ description: "Not found – the specified DID does not exist."
1355
+ 405:
1356
+ description: "Method Not Allowed – the 'key' parameter is not supported with GET."
1332
1357
  406:
1333
- description: "Not acceptable"
1358
+ description: "Not Acceptable – the requested format is not supported."
1334
1359
  """
1360
+ # Flask injects the `key` keyword argument here because the blueprint registers
1361
+ # the generic `/meta` endpoint with `defaults={'key': None}`. The GET endpoint is
1362
+ # intentionally *not* exposed as `/meta/<key>`—it always returns the complete
1363
+ # metadata record (optionally filtered by the `plugin` query parameter). Hence,
1364
+ # a non‑None `key` should never reach this method today. The following guard
1365
+ # defends against any future routing changes that might introduce
1366
+ # `/meta/<key>` for GET requests by explicitly rejecting such usage.
1367
+ if key is not None:
1368
+ return generate_http_error_flask(405,
1369
+ 'MethodNotAllowed',
1370
+ 'GET not allowing keys')
1371
+
1372
+ vo = request.environ['vo']
1335
1373
  try:
1336
- scope, name = parse_scope_name(scope_name, request.environ['vo'])
1374
+ scope, name = parse_scope_name(scope_name, vo)
1337
1375
  except ValueError as error:
1338
1376
  return generate_http_error_flask(400, error)
1339
1377
 
1378
+ plugin = request.args.get('plugin', default='DID_COLUMN')
1340
1379
  try:
1341
- plugin = request.args.get('plugin', default='DID_COLUMN')
1342
- meta = get_metadata(scope=scope, name=name, plugin=plugin, vo=request.environ['vo'])
1380
+ meta = get_metadata(scope=scope, name=name, plugin=plugin, vo=vo)
1343
1381
  return Response(render_json(**meta), content_type='application/json')
1344
1382
  except DataIdentifierNotFound as error:
1345
1383
  return generate_http_error_flask(404, error)
1346
1384
  except UnsupportedMetadataPlugin as error:
1347
1385
  return generate_http_error_flask(400, error)
1348
1386
 
1349
- def post(self, scope_name):
1387
+ def post(self, scope_name, key=None):
1350
1388
  """
1351
1389
  ---
1352
- summary: Add metadata
1353
- description: "Add metadata to a DID."
1390
+ summary: Set or update metadata
1391
+ description: |
1392
+ Set metadata for a data identifier (DID). If a piece of metadata for a given key
1393
+ already exists, it will be handled according to the underlying metadata plugin
1394
+ in use. Certain plugins may disallow updating specific metadata keys.
1395
+
1396
+ - **Single-key mode** (key provided in the path):
1397
+ The request body must contain a `value` field (e.g., `{"value": "some_value"}`).
1398
+ - **Multi-key mode** (no key in the path):
1399
+ The request body must contain a `meta` field with the dictionary containing
1400
+ multiple key-value pairs (e.g. `{"meta": {"k1": "v1", "k2": "v2"}}`).
1401
+
1402
+ The optional `recursive` flag indicates whether the metadata should be applied
1403
+ recursively to child DIDs. Note that whether recursion is supported depends on
1404
+ the plugin configured for your system.
1354
1405
  tags:
1355
1406
  - Data Identifiers
1356
1407
  parameters:
1357
1408
  - name: scope_name
1358
1409
  in: path
1359
- description: "The scope and the name of the DID."
1410
+ description: "The scope and the name of the DID (e.g., `scope:name`)."
1411
+ required: true
1412
+ style: simple
1360
1413
  schema:
1361
1414
  type: string
1415
+ - name: key
1416
+ in: path
1417
+ description: |
1418
+ The key parameter applies only to the `/meta/<key>` endpoint (**Single-key mode**)
1419
+ and defines which metadata key to set/update. If omitted (by calling just `/meta`
1420
+ without the extra path segment), it defaults to `None` and **Multi-key mode** is used.
1421
+ required: true
1362
1422
  style: simple
1423
+ schema:
1424
+ type: string
1363
1425
  requestBody:
1426
+ required: true
1364
1427
  content:
1365
- 'application/json':
1428
+ application/json:
1366
1429
  schema:
1367
- type: object
1368
- required:
1369
- - meta
1370
- properties:
1371
- meta:
1372
- description: "The metadata to add. A dictionary containing the metadata name as key and the value as value."
1373
- type: object
1374
- recursive:
1375
- description: "Flag if the metadata should be applied recirsively to children."
1376
- type: boolean
1377
- default: false
1430
+ oneOf:
1431
+ - type: object
1432
+ description: "Schema for **Single-key mode** (`key` included in path)."
1433
+ required:
1434
+ - value
1435
+ properties:
1436
+ value:
1437
+ description: "The metadata value to set for this key."
1438
+ type: string
1439
+ recursive:
1440
+ description: "Whether to apply the update recursively to child DIDs."
1441
+ type: boolean
1442
+ default: false
1443
+ - type: object
1444
+ description: "Schema for **Multi-key mode** (`key` not included in path)."
1445
+ required:
1446
+ - meta
1447
+ properties:
1448
+ meta:
1449
+ description: "A dictionary of multiple metadata keys and their values."
1450
+ type: object
1451
+ recursive:
1452
+ description: "Whether to apply the update recursively to child DIDs."
1453
+ type: boolean
1454
+ default: false
1455
+ examples:
1456
+ singleKeyMode:
1457
+ summary: "Setting a single metadata key"
1458
+ value:
1459
+ value: "my_metadata_value"
1460
+ recursive: false
1461
+ multiKeyMode:
1462
+ summary: "Setting multiple metadata keys at once"
1463
+ value:
1464
+ meta:
1465
+ experiment: "ATLAS"
1466
+ physics_group: "Higgs"
1467
+ data_type: "RAW"
1468
+ recursive: true
1378
1469
  responses:
1379
1470
  201:
1380
- description: "Created"
1471
+ description: "Created – metadata was successfully set (or updated)."
1381
1472
  content:
1382
- application/json:
1473
+ text/plain:
1383
1474
  schema:
1384
1475
  type: string
1385
1476
  enum: ["Created"]
1477
+ 400:
1478
+ description: "Bad Request – invalid scope_name, or invalid key/value parameters."
1386
1479
  401:
1387
- description: "Invalid Auth Token"
1480
+ description: "Unauthorized – invalid Auth Token."
1388
1481
  404:
1389
- description: "Not found"
1390
- 406:
1391
- description: "Not acceptable"
1482
+ description: "Not found – the specified DID does not exist."
1392
1483
  """
1484
+ vo = request.environ['vo']
1393
1485
  try:
1394
- scope, name = parse_scope_name(scope_name, request.environ['vo'])
1486
+ scope, name = parse_scope_name(scope_name, vo)
1395
1487
  except ValueError as error:
1396
1488
  return generate_http_error_flask(400, error)
1397
1489
 
1398
1490
  parameters = json_parameters()
1399
- meta = param_get(parameters, 'meta')
1400
1491
 
1401
- try:
1402
- set_metadata_bulk(
1403
- scope=scope,
1404
- name=name,
1405
- meta=meta,
1406
- issuer=request.environ['issuer'],
1407
- recursive=param_get(parameters, 'recursive', default=False),
1408
- vo=request.environ['vo'],
1409
- )
1410
- except DataIdentifierNotFound as error:
1411
- return generate_http_error_flask(404, error)
1412
- except Duplicate as error:
1413
- return generate_http_error_flask(409, error)
1414
- except (KeyNotFound, InvalidMetadata, InvalidValueForKey) as error:
1415
- return generate_http_error_flask(400, error)
1492
+ if key is not None:
1493
+ value = param_get(parameters, 'value')
1494
+ try:
1495
+ set_metadata(
1496
+ scope=scope,
1497
+ name=name,
1498
+ key=key,
1499
+ value=value,
1500
+ issuer=request.environ['issuer'],
1501
+ recursive=param_get(parameters, 'recursive', default=False),
1502
+ vo=vo
1503
+ )
1504
+ except DataIdentifierNotFound as error:
1505
+ return generate_http_error_flask(404, error)
1506
+ except (KeyNotFound, InvalidMetadata, InvalidValueForKey) as error:
1507
+ return generate_http_error_flask(400, error)
1508
+ return 'Created', 201
1416
1509
 
1417
- return "Created", 201
1510
+ else:
1511
+ meta = param_get(parameters, 'meta')
1512
+ try:
1513
+ set_metadata_bulk(
1514
+ scope=scope,
1515
+ name=name,
1516
+ meta=meta,
1517
+ issuer=request.environ['issuer'],
1518
+ recursive=param_get(parameters, 'recursive', default=False),
1519
+ vo=vo,
1520
+ )
1521
+ except DataIdentifierNotFound as error:
1522
+ return generate_http_error_flask(404, error)
1523
+ except (KeyNotFound, InvalidMetadata, InvalidValueForKey) as error:
1524
+ return generate_http_error_flask(400, error)
1525
+ return "Created", 201
1418
1526
 
1419
- def delete(self, scope_name):
1527
+ def delete(self, scope_name, key=None):
1420
1528
  """
1421
1529
  ---
1422
1530
  summary: Delete metadata
1423
- description: "Deletes the specified metadata from the DID."
1531
+ description: |
1532
+ Delete a specific metadata key from a data identifier (DID).
1533
+ This `key` must be provided via the query parameter `?key=...`.
1424
1534
  tags:
1425
1535
  - Data Identifiers
1426
1536
  parameters:
1427
1537
  - name: scope_name
1428
1538
  in: path
1429
- description: "The scope and the name of the DID."
1539
+ description: "The scope and the name of the DID (e.g., `scope:name`)."
1540
+ required: true
1541
+ style: simple
1430
1542
  schema:
1431
1543
  type: string
1432
- style: simple
1433
1544
  - name: key
1434
1545
  in: query
1435
- description: "The key to delete."
1546
+ description: "The metadata key to delete."
1547
+ required: true
1548
+ style: form
1436
1549
  schema:
1437
1550
  type: string
1438
1551
  responses:
1439
1552
  200:
1440
- description: "OK"
1553
+ description: "OK – the metadata key was successfully removed."
1554
+ content:
1555
+ text/plain:
1556
+ schema:
1557
+ type: string
1558
+ enum: [""]
1441
1559
  400:
1442
- description: "scope_name could not be parsed."
1560
+ description: "Bad Request invalid scope_name."
1443
1561
  401:
1444
- description: "Invalid Auth Token"
1562
+ description: "Unauthorized – invalid Auth Token."
1445
1563
  404:
1446
- description: "DID or key not found"
1447
- 406:
1448
- description: "Not acceptable"
1564
+ description: >
1565
+ Not found – the specified DID or `key` does not exist, or no `key` query
1566
+ parameter provided.
1567
+ 405:
1568
+ description: "Method Not Allowed – the 'key' parameter is not supported with DELETE."
1449
1569
  409:
1450
- description: "Feature is not in current database."
1570
+ description: "Conflict action not supported by the utilized metadata plugin."
1451
1571
  """
1572
+ # Flask injects the `key` keyword argument here because the blueprint registers the
1573
+ # generic `/meta` endpoint with `defaults={'key': None}`. For DELETE requests the
1574
+ # API currently expects any metadata key to be supplied via the **query string**
1575
+ # (e.g. `...?key=myfield`), so a non‑None `key` coming from the path is impossible
1576
+ # today. We still keep this guard as a defensive measure in case someone later
1577
+ # extends the routing to allow `/meta/<key>` for DELETE as well.
1578
+ if key is not None:
1579
+ return generate_http_error_flask(405,
1580
+ 'MethodNotAllowed',
1581
+ 'DELETE not allowing keys')
1582
+
1583
+ vo = request.environ['vo']
1452
1584
  try:
1453
- scope, name = parse_scope_name(scope_name, request.environ['vo'])
1585
+ scope, name = parse_scope_name(scope_name, vo)
1454
1586
  except ValueError as error:
1455
1587
  return generate_http_error_flask(400, error)
1456
1588
 
1457
- if 'key' in request.args:
1458
- key = request.args['key']
1459
- else:
1589
+ if 'key' not in request.args:
1460
1590
  return generate_http_error_flask(404, KeyNotFound.__name__, 'No key provided to remove')
1461
1591
 
1592
+ delete_key = request.args['key']
1462
1593
  try:
1463
- delete_metadata(scope=scope, name=name, key=key, vo=request.environ['vo'])
1594
+ delete_metadata(scope=scope, name=name, key=delete_key, vo=vo)
1464
1595
  except (KeyNotFound, DataIdentifierNotFound) as error:
1465
1596
  return generate_http_error_flask(404, error)
1466
1597
  except NotImplementedError as error:
@@ -1469,85 +1600,6 @@ class Meta(ErrorHandlingMethodView):
1469
1600
  return '', 200
1470
1601
 
1471
1602
 
1472
- class SingleMeta(ErrorHandlingMethodView):
1473
- def post(self, scope_name, key):
1474
- """
1475
- ---
1476
- summary: Add metadata
1477
- description: "Add metadata to a DID."
1478
- tags:
1479
- - Data Identifiers
1480
- parameters:
1481
- - name: scope_name
1482
- in: path
1483
- description: "The scope and the name of the DID."
1484
- schema:
1485
- type: string
1486
- style: simple
1487
- - name: key
1488
- in: path
1489
- description: "The key for the metadata."
1490
- schema:
1491
- type: string
1492
- style: simple
1493
- requestBody:
1494
- content:
1495
- 'application/json':
1496
- schema:
1497
- type: object
1498
- required:
1499
- - value
1500
- properties:
1501
- value:
1502
- description: "The value to set."
1503
- type: object
1504
- responses:
1505
- 201:
1506
- description: "Created"
1507
- content:
1508
- application/json:
1509
- schema:
1510
- type: string
1511
- enum: ["Created"]
1512
- 401:
1513
- description: "Invalid Auth Token"
1514
- 404:
1515
- description: "DID not found"
1516
- 406:
1517
- description: "Not acceptable"
1518
- 409:
1519
- description: "Metadata already exists"
1520
- 400:
1521
- description: "Invalid key or value"
1522
- """
1523
- try:
1524
- scope, name = parse_scope_name(scope_name, request.environ['vo'])
1525
- except ValueError as error:
1526
- return generate_http_error_flask(400, error)
1527
-
1528
- parameters = json_parameters()
1529
- value = param_get(parameters, 'value')
1530
-
1531
- try:
1532
- set_metadata(
1533
- scope=scope,
1534
- name=name,
1535
- key=key,
1536
- value=value,
1537
- issuer=request.environ['issuer'],
1538
- recursive=param_get(parameters, 'recursive', default=False),
1539
- vo=request.environ['vo'],
1540
- )
1541
- except DataIdentifierNotFound as error:
1542
- return generate_http_error_flask(404, error)
1543
- except Duplicate as error:
1544
- return generate_http_error_flask(409, error)
1545
- except (KeyNotFound, InvalidMetadata, InvalidValueForKey) as error:
1546
- return generate_http_error_flask(400, error)
1547
-
1548
- return 'Created', 201
1549
-
1550
-
1551
1603
  class BulkDIDsMeta(ErrorHandlingMethodView):
1552
1604
 
1553
1605
  def post(self):
@@ -2298,9 +2350,8 @@ def blueprint():
2298
2350
  attachment_view = Attachment.as_view('attachment')
2299
2351
  bp.add_url_rule('/<path:scope_name>/dids', view_func=attachment_view, methods=['get', 'post', 'delete'])
2300
2352
  meta_view = Meta.as_view('meta')
2301
- bp.add_url_rule('/<path:scope_name>/meta', view_func=meta_view, methods=['get', 'post', 'delete'])
2302
- singlemeta_view = SingleMeta.as_view('singlemeta')
2303
- bp.add_url_rule('/<path:scope_name>/meta/<key>', view_func=singlemeta_view, methods=['post', ])
2353
+ bp.add_url_rule('/<path:scope_name>/meta', defaults={'key': None}, view_func=meta_view, methods=['get', 'post', 'delete'])
2354
+ bp.add_url_rule('/<path:scope_name>/meta/<key>', view_func=meta_view, methods=['post', ])
2304
2355
  bulkdidsmeta_view = BulkDIDsMeta.as_view('bulkdidsmeta')
2305
2356
  bp.add_url_rule('/bulkdidsmeta', view_func=bulkdidsmeta_view, methods=['post', ])
2306
2357
  rules_view = Rules.as_view('rules')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rucio
3
- Version: 37.6.0
3
+ Version: 37.7.0
4
4
  Summary: Rucio Package
5
5
  Home-page: https://rucio.cern.ch/
6
6
  Author: Rucio