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.
- rucio/cli/bin_legacy/rucio.py +40 -21
- rucio/cli/rule.py +9 -5
- rucio/client/baseclient.py +4 -3
- rucio/client/downloadclient.py +2 -1
- rucio/client/exportclient.py +45 -4
- rucio/client/pingclient.py +35 -4
- rucio/client/touchclient.py +2 -1
- rucio/client/uploadclient.py +3 -2
- rucio/common/cache.py +1 -2
- rucio/common/client.py +4 -30
- rucio/common/config.py +26 -1
- rucio/common/constants.py +3 -1
- rucio/common/plugins.py +2 -2
- rucio/common/policy.py +3 -2
- rucio/common/schema/__init__.py +4 -3
- rucio/common/types.py +7 -5
- rucio/core/account.py +2 -1
- rucio/core/account_limit.py +3 -2
- rucio/core/did.py +8 -7
- rucio/core/dirac.py +2 -1
- rucio/core/distance.py +2 -1
- rucio/core/exporter.py +3 -2
- rucio/core/importer.py +5 -5
- rucio/core/permission/__init__.py +2 -1
- rucio/core/replica.py +5 -5
- rucio/core/request.py +2 -2
- rucio/core/rse.py +7 -7
- rucio/core/rule.py +8 -8
- rucio/core/transfer.py +2 -2
- rucio/core/vo.py +2 -1
- rucio/daemons/atropos/atropos.py +2 -1
- rucio/daemons/automatix/automatix.py +5 -5
- rucio/daemons/badreplicas/minos.py +3 -2
- rucio/daemons/bb8/bb8.py +2 -1
- rucio/daemons/bb8/nuclei_background_rebalance.py +2 -2
- rucio/daemons/conveyor/common.py +3 -3
- rucio/daemons/conveyor/submitter.py +2 -1
- rucio/daemons/hermes/hermes.py +27 -6
- rucio/daemons/reaper/dark_reaper.py +5 -4
- rucio/daemons/reaper/reaper.py +7 -7
- rucio/daemons/replicarecoverer/suspicious_replica_recoverer.py +3 -3
- rucio/daemons/tracer/kronos.py +3 -2
- rucio/daemons/transmogrifier/transmogrifier.py +70 -68
- rucio/daemons/undertaker/undertaker.py +2 -1
- rucio/db/sqla/models.py +2 -2
- rucio/db/sqla/util.py +3 -2
- rucio/gateway/account.py +13 -12
- rucio/gateway/account_limit.py +90 -116
- rucio/gateway/authentication.py +9 -8
- rucio/gateway/config.py +11 -10
- rucio/gateway/credential.py +2 -1
- rucio/gateway/did.py +32 -32
- rucio/gateway/dirac.py +2 -1
- rucio/gateway/exporter.py +2 -1
- rucio/gateway/heartbeat.py +3 -2
- rucio/gateway/identity.py +4 -3
- rucio/gateway/importer.py +2 -1
- rucio/gateway/lifetime_exception.py +4 -3
- rucio/gateway/lock.py +6 -5
- rucio/gateway/meta_conventions.py +3 -2
- rucio/gateway/permission.py +2 -1
- rucio/gateway/quarantined_replica.py +2 -1
- rucio/gateway/replica.py +18 -18
- rucio/gateway/request.py +10 -10
- rucio/gateway/rse.py +27 -26
- rucio/gateway/rule.py +12 -11
- rucio/gateway/scope.py +4 -3
- rucio/gateway/subscription.py +7 -6
- rucio/gateway/vo.py +5 -4
- rucio/rse/__init__.py +7 -6
- rucio/rse/rsemanager.py +5 -4
- rucio/rse/translation.py +2 -2
- rucio/tests/common.py +2 -1
- rucio/vcsversion.py +3 -3
- rucio/web/rest/flaskapi/v1/accountlimits.py +5 -5
- rucio/web/rest/flaskapi/v1/archives.py +2 -1
- rucio/web/rest/flaskapi/v1/common.py +4 -3
- rucio/web/rest/flaskapi/v1/dids.py +205 -154
- {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/METADATA +1 -1
- {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/RECORD +139 -139
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/alembic.ini.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio.cfg.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/requirements.server.txt +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/bootstrap.py +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/data/rucio/tools/reset_database.py +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-account +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-collection-replica +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-abacus-rse +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-admin +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-atropos +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-auditor +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-automatix +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-bb8 +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-cache-client +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-cache-consumer +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-finisher +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-poller +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-preparer +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-receiver +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-stager +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-submitter +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-conveyor-throttler +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-dark-reaper +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-dumper +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-follower +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-hermes +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-cleaner +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-evaluator +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-injector +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-judge-repairer +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-kronos +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-minos +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-necromancer +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-oauth-manager +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-reaper +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-replica-recoverer +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-rse-decommissioner +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-storage-consistency-actions +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-transmogrifier +0 -0
- {rucio-37.6.0.data → rucio-37.7.0.data}/scripts/rucio-undertaker +0 -0
- {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/WHEEL +0 -0
- {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {rucio-37.6.0.dist-info → rucio-37.7.0.dist-info}/licenses/LICENSE +0 -0
- {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 =
|
|
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.
|
|
7
|
+
'version': '37.7.0',
|
|
8
8
|
'branch_nick': 'release-37',
|
|
9
|
-
'revision_id': '
|
|
10
|
-
'revno':
|
|
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
|
|
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
|
|
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
|
|
172
|
-
vo=request.environ
|
|
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
|
|
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',
|
|
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',
|
|
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 =
|
|
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=
|
|
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: "
|
|
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
|
|
1350
|
+
description: "Bad Request – invalid scope_name, or invalid metadata plugin specified."
|
|
1328
1351
|
401:
|
|
1329
|
-
description: "
|
|
1352
|
+
description: "Unauthorized – invalid Auth Token."
|
|
1330
1353
|
404:
|
|
1331
|
-
description: "DID not
|
|
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
|
|
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,
|
|
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
|
-
|
|
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:
|
|
1353
|
-
description:
|
|
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
|
-
|
|
1428
|
+
application/json:
|
|
1366
1429
|
schema:
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
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: "
|
|
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,
|
|
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
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
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
|
-
|
|
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:
|
|
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: "
|
|
1560
|
+
description: "Bad Request – invalid scope_name."
|
|
1443
1561
|
401:
|
|
1444
|
-
description: "
|
|
1562
|
+
description: "Unauthorized – invalid Auth Token."
|
|
1445
1563
|
404:
|
|
1446
|
-
description:
|
|
1447
|
-
|
|
1448
|
-
|
|
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: "
|
|
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,
|
|
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=
|
|
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
|
-
|
|
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')
|