rucio 37.7.0__py3-none-any.whl → 38.0.0rc1__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 (122) hide show
  1. rucio/alembicrevision.py +1 -1
  2. rucio/cli/bin_legacy/rucio.py +51 -107
  3. rucio/cli/bin_legacy/rucio_admin.py +26 -26
  4. rucio/cli/command.py +1 -0
  5. rucio/cli/did.py +2 -2
  6. rucio/cli/opendata.py +132 -0
  7. rucio/cli/replica.py +15 -5
  8. rucio/cli/rule.py +7 -2
  9. rucio/cli/scope.py +3 -2
  10. rucio/cli/utils.py +28 -4
  11. rucio/client/baseclient.py +9 -1
  12. rucio/client/client.py +2 -0
  13. rucio/client/diracclient.py +73 -12
  14. rucio/client/opendataclient.py +249 -0
  15. rucio/client/subscriptionclient.py +30 -0
  16. rucio/client/uploadclient.py +10 -13
  17. rucio/common/constants.py +4 -1
  18. rucio/common/exception.py +55 -0
  19. rucio/common/plugins.py +45 -8
  20. rucio/common/schema/generic.py +5 -3
  21. rucio/common/schema/generic_multi_vo.py +4 -2
  22. rucio/common/types.py +8 -7
  23. rucio/common/utils.py +176 -11
  24. rucio/core/dirac.py +5 -5
  25. rucio/core/opendata.py +744 -0
  26. rucio/core/rule.py +63 -8
  27. rucio/core/transfer.py +1 -1
  28. rucio/daemons/hermes/hermes.py +26 -17
  29. rucio/db/sqla/constants.py +6 -0
  30. rucio/db/sqla/migrate_repo/versions/a62db546a1f1_opendata_initial_model.py +85 -0
  31. rucio/db/sqla/models.py +67 -0
  32. rucio/db/sqla/util.py +2 -2
  33. rucio/gateway/dirac.py +1 -1
  34. rucio/gateway/opendata.py +190 -0
  35. rucio/gateway/subscription.py +5 -3
  36. rucio/rse/protocols/protocol.py +9 -5
  37. rucio/rse/translation.py +17 -6
  38. rucio/transfertool/fts3.py +1 -0
  39. rucio/transfertool/fts3_plugins.py +6 -1
  40. rucio/vcsversion.py +4 -4
  41. rucio/web/rest/flaskapi/v1/common.py +34 -14
  42. rucio/web/rest/flaskapi/v1/config.py +1 -1
  43. rucio/web/rest/flaskapi/v1/dids.py +447 -160
  44. rucio/web/rest/flaskapi/v1/heartbeats.py +1 -1
  45. rucio/web/rest/flaskapi/v1/identities.py +1 -1
  46. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +1 -1
  47. rucio/web/rest/flaskapi/v1/locks.py +1 -1
  48. rucio/web/rest/flaskapi/v1/main.py +3 -8
  49. rucio/web/rest/flaskapi/v1/meta_conventions.py +1 -16
  50. rucio/web/rest/flaskapi/v1/nongrid_traces.py +1 -1
  51. rucio/web/rest/flaskapi/v1/opendata.py +391 -0
  52. rucio/web/rest/flaskapi/v1/opendata_public.py +146 -0
  53. rucio/web/rest/flaskapi/v1/requests.py +1 -1
  54. rucio/web/rest/flaskapi/v1/rses.py +1 -1
  55. rucio/web/rest/flaskapi/v1/rules.py +1 -1
  56. rucio/web/rest/flaskapi/v1/scopes.py +1 -1
  57. rucio/web/rest/flaskapi/v1/subscriptions.py +6 -9
  58. rucio/web/rest/flaskapi/v1/traces.py +1 -1
  59. rucio/web/rest/flaskapi/v1/vos.py +1 -1
  60. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/alembic.ini.template +1 -1
  61. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/alembic_offline.ini.template +1 -1
  62. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio.cfg.template +2 -2
  63. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio_multi_vo.cfg.template +3 -3
  64. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/requirements.server.txt +6 -3
  65. rucio-38.0.0rc1.data/data/rucio/tools/reset_database.py +87 -0
  66. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio +2 -1
  67. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/METADATA +36 -36
  68. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/RECORD +120 -114
  69. rucio/client/fileclient.py +0 -57
  70. rucio-37.7.0.data/data/rucio/tools/reset_database.py +0 -40
  71. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/globus-config.yml.template +0 -0
  72. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/ldap.cfg.template +0 -0
  73. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  74. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  75. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  76. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  77. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  78. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  79. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  80. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  81. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/tools/bootstrap.py +0 -0
  82. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  83. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-account +0 -0
  84. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-collection-replica +0 -0
  85. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-abacus-rse +0 -0
  86. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-admin +0 -0
  87. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-atropos +0 -0
  88. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-auditor +0 -0
  89. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-automatix +0 -0
  90. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-bb8 +0 -0
  91. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-cache-client +0 -0
  92. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-cache-consumer +0 -0
  93. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-finisher +0 -0
  94. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-poller +0 -0
  95. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-preparer +0 -0
  96. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-receiver +0 -0
  97. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-stager +0 -0
  98. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-submitter +0 -0
  99. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-conveyor-throttler +0 -0
  100. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-dark-reaper +0 -0
  101. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-dumper +0 -0
  102. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-follower +0 -0
  103. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-hermes +0 -0
  104. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-cleaner +0 -0
  105. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-evaluator +0 -0
  106. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-injector +0 -0
  107. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-judge-repairer +0 -0
  108. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-kronos +0 -0
  109. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-minos +0 -0
  110. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-minos-temporary-expiration +0 -0
  111. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-necromancer +0 -0
  112. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-oauth-manager +0 -0
  113. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-reaper +0 -0
  114. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-replica-recoverer +0 -0
  115. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-rse-decommissioner +0 -0
  116. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-storage-consistency-actions +0 -0
  117. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-transmogrifier +0 -0
  118. {rucio-37.7.0.data → rucio-38.0.0rc1.data}/scripts/rucio-undertaker +0 -0
  119. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/WHEEL +0 -0
  120. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/licenses/AUTHORS.rst +0 -0
  121. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/licenses/LICENSE +0 -0
  122. {rucio-37.7.0.dist-info → rucio-38.0.0rc1.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@
14
14
 
15
15
  import ast
16
16
  from json import dumps
17
+ from typing import TYPE_CHECKING, Any, Optional, cast
17
18
 
18
19
  from flask import Flask, Response, request
19
20
 
@@ -37,7 +38,7 @@ from rucio.common.exception import (
37
38
  UnsupportedOperation,
38
39
  UnsupportedStatus,
39
40
  )
40
- from rucio.common.utils import APIEncoder, parse_response, render_json
41
+ from rucio.common.utils import APIEncoder, clone_function, parse_response, render_json
41
42
  from rucio.db.sqla.constants import DIDType
42
43
  from rucio.gateway.did import (
43
44
  add_did,
@@ -72,6 +73,10 @@ from rucio.gateway.rule import list_associated_replication_rules_for_file, list_
72
73
  from rucio.web.rest.flaskapi.authenticated_bp import AuthenticatedBlueprint
73
74
  from rucio.web.rest.flaskapi.v1.common import ErrorHandlingMethodView, check_accept_header_wrapper_flask, generate_http_error_flask, json_list, json_parameters, json_parse, param_get, parse_scope_name, response_headers, try_stream
74
75
 
76
+ if TYPE_CHECKING:
77
+
78
+ from flask.typing import ResponseReturnValue, RouteCallable
79
+
75
80
 
76
81
  class Scope(ErrorHandlingMethodView):
77
82
 
@@ -193,7 +198,7 @@ class Search(ErrorHandlingMethodView):
193
198
  default: false
194
199
  - name: recursive
195
200
  in: query
196
- description: "Recursively list chilred."
201
+ description: "Recursively list children."
197
202
  schema:
198
203
  type: boolean
199
204
  - name: created_before
@@ -246,7 +251,7 @@ class Search(ErrorHandlingMethodView):
246
251
  type: array
247
252
  items:
248
253
  type: object
249
- description: "The name of a DID or a dictionarie of a DID for long option."
254
+ description: "The name of a DID or a dictionary of a DID for long option."
250
255
  401:
251
256
  description: "Invalid Auth Token"
252
257
  404:
@@ -273,8 +278,15 @@ class Search(ErrorHandlingMethodView):
273
278
  recursive = request.args.get('recursive', type='True'.__eq__, default=False)
274
279
  try:
275
280
  def generate(vo):
276
- for did in list_dids(scope=scope, filters=filters, did_type=did_type, limit=limit, long=long, recursive=recursive, vo=vo):
281
+ for did in list_dids(scope=scope,
282
+ filters=filters,
283
+ did_type=did_type,
284
+ limit=limit,
285
+ long=long,
286
+ recursive=recursive,
287
+ vo=vo):
277
288
  yield dumps(did) + '\n'
289
+
278
290
  return try_stream(generate(vo=request.environ['vo']))
279
291
  except UnsupportedOperation as error:
280
292
  return generate_http_error_flask(409, error)
@@ -417,7 +429,7 @@ class Attachments(ErrorHandlingMethodView):
417
429
  description: "The name of the DID."
418
430
  type: string
419
431
  dids:
420
- description: "The DIDs associated to the DID."
432
+ description: "The DIDs associated with the DID."
421
433
  type: array
422
434
  items:
423
435
  type: object
@@ -461,7 +473,10 @@ class Attachments(ErrorHandlingMethodView):
461
473
  return generate_http_error_flask(406, exc="Invalid attachment format.")
462
474
 
463
475
  try:
464
- attach_dids_to_dids(attachments=attachments, ignore_duplicate=ignore_duplicate, issuer=request.environ['issuer'], vo=request.environ['vo'])
476
+ attach_dids_to_dids(attachments=attachments,
477
+ ignore_duplicate=ignore_duplicate,
478
+ issuer=request.environ['issuer'],
479
+ vo=request.environ['vo'])
465
480
  except DataIdentifierNotFound as error:
466
481
  return generate_http_error_flask(404, error)
467
482
  except (DuplicateContent, DataIdentifierAlreadyExists, UnsupportedOperation, FileAlreadyExists) as error:
@@ -483,7 +498,7 @@ class DIDs(ErrorHandlingMethodView):
483
498
  summary: Get DID
484
499
  description: "Get a single data identifier."
485
500
  tags:
486
- - Data identifiers
501
+ - Data Identifiers
487
502
  parameters:
488
503
  - name: scope_name
489
504
  in: path
@@ -899,7 +914,11 @@ class Attachment(ErrorHandlingMethodView):
899
914
  attachments = json_parameters()
900
915
 
901
916
  try:
902
- attach_dids(scope=scope, name=name, attachment=attachments, issuer=request.environ['issuer'], vo=request.environ['vo'])
917
+ attach_dids(scope=scope,
918
+ name=name,
919
+ attachment=attachments,
920
+ issuer=request.environ['issuer'],
921
+ vo=request.environ['vo'])
903
922
  except InvalidPath as error:
904
923
  return generate_http_error_flask(400, error)
905
924
  except (DataIdentifierNotFound, RSENotFound) as error:
@@ -996,7 +1015,9 @@ class AttachmentHistory(ErrorHandlingMethodView):
996
1015
  content:
997
1016
  application/x-json-stream:
998
1017
  schema:
999
- description: "The DIDs with their information and history. Elements are separated by new line characters."
1018
+ description: |
1019
+ The DIDs with their information and history.
1020
+ Elements are separated by new line characters.
1000
1021
  type: array
1001
1022
  items:
1002
1023
  type: object
@@ -1229,7 +1250,6 @@ class BulkFiles(ErrorHandlingMethodView):
1229
1250
  return try_stream(generate(vo=request.environ['vo']))
1230
1251
  except AccessDenied as error:
1231
1252
  return generate_http_error_flask(401, error)
1232
- return 'Created', 201
1233
1253
 
1234
1254
 
1235
1255
  class Parents(ErrorHandlingMethodView):
@@ -1441,7 +1461,7 @@ class Meta(ErrorHandlingMethodView):
1441
1461
  type: boolean
1442
1462
  default: false
1443
1463
  - type: object
1444
- description: "Schema for **Multi-key mode** (`key` not included in path)."
1464
+ description: "Schema for **Multi-key mode** (`key` not included in the path)."
1445
1465
  required:
1446
1466
  - meta
1447
1467
  properties:
@@ -1601,68 +1621,307 @@ class Meta(ErrorHandlingMethodView):
1601
1621
 
1602
1622
 
1603
1623
  class BulkDIDsMeta(ErrorHandlingMethodView):
1624
+ # Public modes
1625
+ MODE_SET = "set" # POST /bulkdidsmeta
1626
+ MODE_GET = "get" # POST /bulkmeta
1604
1627
 
1605
- def post(self):
1628
+ # Endpoint‑specific configuration
1629
+ MODE_SET_DOC = \
1606
1630
  """
1607
1631
  ---
1608
- summary: Add metadata bulk
1609
- description: "Adds metadata in a bulk."
1632
+ summary: Bulk set metadata
1633
+ description: |
1634
+ Add or update metadata for **multiple** data identifiers (DIDs) in a single request.
1635
+
1636
+ * Every array element **must** contain the DID (`scope` + `name`) and a `meta`
1637
+ dictionary holding the key–value pairs to insert / update for that DID.
1638
+ * If a key already exists the exact action (overwrite, merge, reject)
1639
+ depends on the metadata plug‑in configured on the server.
1640
+ * The operation is atomic across the whole list: the request succeeds only if
1641
+ all DIDs are updated successfully; otherwise no metadata is written.
1610
1642
  tags:
1611
1643
  - Data Identifiers
1612
1644
  requestBody:
1645
+ required: true
1613
1646
  content:
1614
- 'application/json':
1647
+ application/json:
1615
1648
  schema:
1616
1649
  type: object
1617
1650
  required:
1618
- - dids
1651
+ - dids
1619
1652
  properties:
1620
1653
  dids:
1621
- description: "A list with all the DIDs and the metadata."
1654
+ description: "List of DIDs with the metadata to apply."
1622
1655
  type: array
1623
1656
  items:
1624
- description: "The DID and associated metadata."
1625
1657
  type: object
1658
+ required:
1659
+ - scope
1660
+ - name
1661
+ - meta
1626
1662
  properties:
1627
1663
  scope:
1628
- description: "The scope of the DID."
1664
+ description: "Scope of the DID."
1629
1665
  type: string
1630
1666
  name:
1631
- description: "The name of the DID."
1667
+ description: "Name of the DID."
1632
1668
  type: string
1633
1669
  meta:
1634
- description: "The metadata to add. A dictionary with the meta key as key and the value as value."
1670
+ description: >
1671
+ Dictionary of metadata key–value pairs to set for this DID.
1672
+ Values may be strings, numbers, booleans, etc. – consult
1673
+ the plug‑in documentation for supported types.
1635
1674
  type: object
1675
+ examples:
1676
+ minimal:
1677
+ summary: "Two DIDs, simple values"
1678
+ value:
1679
+ dids:
1680
+ - scope: "user"
1681
+ name: "dataset_001"
1682
+ meta:
1683
+ experiment: "CMS"
1684
+ year: 2024
1685
+ - scope: "user"
1686
+ name: "dataset_002"
1687
+ meta:
1688
+ experiment: "ATLAS"
1689
+ is_open: true
1690
+
1636
1691
  responses:
1637
- 200:
1638
- description: "Created"
1692
+ 201:
1693
+ description: "Created – all metadata updates were accepted."
1639
1694
  content:
1640
- application/json:
1695
+ text/plain:
1641
1696
  schema:
1642
1697
  type: string
1643
1698
  enum: ["Created"]
1699
+ 400:
1700
+ description: |
1701
+ Bad Request – malformed JSON or missing/invalid `dids` structure.
1702
+ (Raised by the generic JSON‑parameter parser before reaching the
1703
+ business logic.)
1644
1704
  401:
1645
- description: "Invalid Auth Token"
1705
+ description: |
1706
+ Unauthorized – invalid Auth Token or insufficient privileges to
1707
+ modify at least one DID.
1646
1708
  404:
1647
- description: "DID not found"
1648
- 406:
1649
- description: "Not acceptable"
1709
+ description: "Not found – at least one DID in the request does not exist."
1650
1710
  409:
1651
- description: "Unsupported Operation"
1711
+ description: "Conflict – the operation is not supported for at least one DID."
1712
+ """
1713
+
1714
+ MODE_GET_DOC = \
1715
+ """
1716
+ ---
1717
+ summary: Bulk get metadata
1718
+ description: |
1719
+ Retrieve the metadata of **multiple** data identifiers (DIDs) with one request.
1720
+
1721
+ * The request body is ordinary JSON (`Content‑Type: application/json`).
1722
+ * The **response** is a *newline‑delimited JSON* stream
1723
+ (`Content‑Type: application/x-json-stream`).
1724
+ Each line is a complete JSON object containing the metadata of a single
1725
+ DID. The client **must** send `Accept: application/x-json-stream`; any
1726
+ other `Accept` value is rejected with **406 Not Acceptable**.
1727
+ * If `inherit=true`, metadata from parent containers is concatenated
1728
+ (plug‑in permitting).
1729
+ * `plugin` chooses the metadata plug‑in; `"ALL"` returns the union of every
1730
+ available plug‑in.
1731
+
1732
+ tags:
1733
+ - Data Identifiers
1734
+ requestBody:
1735
+ required: true
1736
+ content:
1737
+ application/json:
1738
+ schema:
1739
+ type: object
1740
+ required:
1741
+ - dids
1742
+ properties:
1743
+ dids:
1744
+ description: "List of DIDs to query."
1745
+ type: array
1746
+ items:
1747
+ type: object
1748
+ required:
1749
+ - scope
1750
+ - name
1751
+ properties:
1752
+ scope:
1753
+ description: "Scope of the DID."
1754
+ type: string
1755
+ name:
1756
+ description: "Name of the DID."
1757
+ type: string
1758
+ inherit:
1759
+ description: >
1760
+ If **true**, the server will also return metadata inherited
1761
+ from parent DIDs (default: `false`).
1762
+ type: boolean
1763
+ default: false
1764
+ plugin:
1765
+ description: >
1766
+ Which metadata plug‑in to query
1767
+ (`"JSON"`, `"DID_COLUMN"`, `"ALL"`, etc.; default: `"JSON"`).
1768
+ type: string
1769
+ default: "JSON"
1770
+ examples:
1771
+ defaultQuery:
1772
+ summary: "Query two DIDs with inheritance"
1773
+ value:
1774
+ dids:
1775
+ - scope: "user"
1776
+ name: "dataset_001"
1777
+ - scope: "user"
1778
+ name: "dataset_002"
1779
+ inherit: true
1780
+ plugin: "JSON"
1781
+
1782
+ responses:
1783
+ 200:
1784
+ description: "OK – stream of newline‑delimited JSON objects, one per DID."
1785
+ content:
1786
+ application/x-json-stream:
1787
+ schema:
1788
+ type: string
1789
+ example: |
1790
+ {"scope":"user","name":"dataset_001","experiment":"CMS", ...}\n
1791
+ {"scope":"user","name":"dataset_002","experiment":"ATLAS", ...}\n
1792
+
1793
+ 400:
1794
+ description: >
1795
+ Bad Request – cannot decode JSON parameter list (malformed body or
1796
+ missing `dids` array).
1797
+ 401:
1798
+ description: "Unauthorized – invalid Auth Token."
1799
+ 404:
1800
+ description: "Not found – none of the requested DIDs exist."
1801
+ 406:
1802
+ description: |
1803
+ Not Acceptable – an `Accept` header was sent, but it does not
1804
+ include `application/x-json-stream`.
1805
+ """
1806
+
1807
+ _MODE_DOC: dict[str, str] = {
1808
+ MODE_SET: MODE_SET_DOC,
1809
+ MODE_GET: MODE_GET_DOC
1810
+ }
1811
+ _MODE_ACCEPT: dict[str, Optional[list[str]]] = {
1812
+ MODE_SET: None,
1813
+ MODE_GET: ["application/x-json-stream"],
1814
+ }
1815
+
1816
+ # cache for the on‑the‑fly subclasses
1817
+ _SUBCLASSES: dict[str, type["BulkDIDsMeta"]] = {}
1818
+
1819
+ def __init__(self, mode: str, *args: Any, **kwargs: Any) -> None:
1820
+ if mode not in (self.MODE_SET, self.MODE_GET):
1821
+ raise ValueError(f"Unsupported mode {mode!r}")
1822
+ self.mode = mode
1823
+ super().__init__(*args, **kwargs)
1824
+
1825
+ # Main factory
1826
+ @classmethod
1827
+ def as_view(
1828
+ cls,
1829
+ name: str,
1830
+ *class_args: Any,
1831
+ **class_kwargs: Any
1832
+ ) -> 'RouteCallable':
1833
+ """
1834
+ Create the Flask view function for *mode* with the correct
1835
+ docstring and (if required) an Accept‑header wrapper.
1652
1836
  """
1653
- parameters = json_parameters()
1654
- dids = param_get(parameters, 'dids')
1837
+
1838
+ # 0. Extract & validate the mode argument
1839
+ mode_opt = class_kwargs.pop("mode", None)
1840
+ if mode_opt not in (cls.MODE_SET, cls.MODE_GET):
1841
+ raise ValueError("BulkDIDsMeta.as_view() needs mode='set' or mode='get'")
1842
+
1843
+ # Tell the type checker that `mode_opt` is definitely str here
1844
+ mode = cast('str', mode_opt)
1845
+
1846
+ # 1. Build / fetch the dedicated subclass for this mode
1847
+ if mode not in cls._SUBCLASSES:
1848
+ sub_name = f"{cls.__name__}_{mode}"
1849
+ sub: type["BulkDIDsMeta"] = cast(
1850
+ 'type[BulkDIDsMeta]',
1851
+ type(sub_name, (cls,), {}),
1852
+ )
1853
+ new_post = clone_function(cls.post) # independent copy
1854
+ new_post.__doc__ = cls._MODE_DOC[mode] # mode‑specific spec
1855
+ setattr(sub, "post", new_post)
1856
+ cls._SUBCLASSES[mode] = sub
1857
+ sub = cls._SUBCLASSES[mode]
1858
+
1859
+ # 2. Let MethodView build the dispatch function
1860
+ class_kwargs["mode"] = mode # forward to __init__
1861
+
1862
+ raw_view = super(BulkDIDsMeta, sub).as_view(
1863
+ name, *class_args, **class_kwargs
1864
+ )
1865
+
1866
+ # 3. Add Accept‑header checker when needed
1867
+ accept = cls._MODE_ACCEPT[mode]
1868
+ if accept:
1869
+ raw_view = check_accept_header_wrapper_flask(accept)(raw_view)
1870
+
1871
+ view_func = cast('RouteCallable', raw_view)
1872
+ return view_func
1873
+
1874
+ # ------------------------------------------------------------------
1875
+ # Single entry‑point for both logical endpoints
1876
+ # ------------------------------------------------------------------
1877
+ def post(self) -> 'ResponseReturnValue':
1878
+ if self.mode == self.MODE_SET:
1879
+ return self._handle_set()
1880
+ return self._handle_get()
1881
+
1882
+ # ------------------------------------------------------------------
1883
+ # Implementation of the SET variant (/bulkdidsmeta)
1884
+ # ------------------------------------------------------------------
1885
+ def _handle_set(self) -> 'ResponseReturnValue':
1886
+ params = json_parameters()
1887
+ dids = param_get(params, "dids")
1655
1888
 
1656
1889
  try:
1657
- set_dids_metadata_bulk(dids=dids, issuer=request.environ['issuer'], vo=request.environ['vo'])
1658
- except DataIdentifierNotFound as error:
1659
- return generate_http_error_flask(404, error)
1660
- except UnsupportedOperation as error:
1661
- return generate_http_error_flask(409, error)
1662
- except AccessDenied as error:
1663
- return generate_http_error_flask(401, error)
1890
+ set_dids_metadata_bulk(
1891
+ dids=dids,
1892
+ issuer=request.environ["issuer"],
1893
+ vo=request.environ["vo"],
1894
+ )
1895
+ except DataIdentifierNotFound as err:
1896
+ return generate_http_error_flask(404, err)
1897
+ except UnsupportedOperation as err:
1898
+ return generate_http_error_flask(409, err)
1899
+ except AccessDenied as err:
1900
+ return generate_http_error_flask(401, err)
1901
+
1902
+ return "Created", 201
1903
+
1904
+ # ------------------------------------------------------------------
1905
+ # Implementation of the GET variant (/bulkmeta)
1906
+ # ------------------------------------------------------------------
1907
+ def _handle_get(self) -> 'ResponseReturnValue':
1908
+ params = json_parameters()
1909
+ dids = param_get(params, "dids")
1910
+ inherit = param_get(params, "inherit", default=False)
1911
+ plugin = param_get(params, "plugin", default="JSON")
1664
1912
 
1665
- return 'Created', 201
1913
+ try:
1914
+ def generate(vo):
1915
+ for meta in get_metadata_bulk(dids, inherit=inherit, plugin=plugin, vo=vo):
1916
+ yield render_json(**meta) + "\n"
1917
+
1918
+ return try_stream(generate(vo=request.environ["vo"]))
1919
+ except ValueError as err:
1920
+ return generate_http_error_flask(
1921
+ 400, err, "Cannot decode json parameter list"
1922
+ )
1923
+ except DataIdentifierNotFound as err:
1924
+ return generate_http_error_flask(404, err)
1666
1925
 
1667
1926
 
1668
1927
  class Rules(ErrorHandlingMethodView):
@@ -1717,82 +1976,6 @@ class Rules(ErrorHandlingMethodView):
1717
1976
  return generate_http_error_flask(404, error)
1718
1977
 
1719
1978
 
1720
- class BulkMeta(ErrorHandlingMethodView):
1721
-
1722
- @check_accept_header_wrapper_flask(['application/x-json-stream'])
1723
- def post(self):
1724
- """
1725
- ---
1726
- summary: Get metadata bulk
1727
- description: "List all metadata of a list of data identifiers."
1728
- tags:
1729
- - Data Identifiers
1730
- requestBody:
1731
- content:
1732
- 'application/x-json-stream':
1733
- schema:
1734
- type: object
1735
- required:
1736
- - dids
1737
- properties:
1738
- dids:
1739
- description: "The DIDs."
1740
- type: array
1741
- items:
1742
- description: "A DID."
1743
- type: object
1744
- properties:
1745
- name:
1746
- description: "The name of the DID."
1747
- type: string
1748
- scope:
1749
- description: "The scope of the DID."
1750
- type: string
1751
- inherit:
1752
- description: "Concatenated the metadata of the parent if set to true."
1753
- type: boolean
1754
- default: false
1755
- plugin:
1756
- description: "The DID meta plugin to query or 'ALL' for all available plugins"
1757
- type: string
1758
- default: "JSON"
1759
- responses:
1760
- 200:
1761
- description: "OK"
1762
- content:
1763
- application/json:
1764
- schema:
1765
- description: "A list of metadata identifiers for the DIDs. Separated by new lines."
1766
- type: array
1767
- items:
1768
- description: "The metadata for one DID."
1769
- type: object
1770
- 400:
1771
- description: "Cannot decode json parameter list"
1772
- 401:
1773
- description: "Invalid Auth Token"
1774
- 404:
1775
- description: "DID not found"
1776
- 406:
1777
- description: "Not acceptable"
1778
- """
1779
- parameters = json_parameters()
1780
- dids = param_get(parameters, 'dids')
1781
- inherit = param_get(parameters, 'inherit', default=False)
1782
- plugin = param_get(parameters, 'plugin', default='JSON')
1783
-
1784
- try:
1785
- def generate(vo):
1786
- for meta in get_metadata_bulk(dids, inherit=inherit, plugin=plugin, vo=vo):
1787
- yield render_json(**meta) + '\n'
1788
-
1789
- return try_stream(generate(vo=request.environ['vo']))
1790
- except ValueError as error:
1791
- return generate_http_error_flask(400, error, 'Cannot decode json parameter list')
1792
- except DataIdentifierNotFound as error:
1793
- return generate_http_error_flask(404, error)
1794
-
1795
-
1796
1979
  class AssociatedRules(ErrorHandlingMethodView):
1797
1980
 
1798
1981
  @check_accept_header_wrapper_flask(['application/x-json-stream'])
@@ -1887,7 +2070,9 @@ class GUIDLookup(ErrorHandlingMethodView):
1887
2070
  content:
1888
2071
  application/x-json-stream:
1889
2072
  schema:
1890
- description: "A list of all datasets associated with the guid. Items are separated by new line character."
2073
+ description: |
2074
+ A list of all datasets associated with the guid.
2075
+ Items are separated by new line character.
1891
2076
  type: array
1892
2077
  items:
1893
2078
  description: "A dataset associated with a guid."
@@ -2325,60 +2510,162 @@ class Follow(ErrorHandlingMethodView):
2325
2510
  account = param_get(parameters, 'account')
2326
2511
 
2327
2512
  try:
2328
- remove_did_from_followed(scope=scope, name=name, account=account, issuer=request.environ['issuer'], vo=request.environ['vo'])
2513
+ remove_did_from_followed(scope=scope,
2514
+ name=name,
2515
+ account=account,
2516
+ issuer=request.environ['issuer'],
2517
+ vo=request.environ['vo'])
2329
2518
  except DataIdentifierNotFound as error:
2330
2519
  return generate_http_error_flask(404, error)
2331
2520
 
2332
2521
  return '', 200
2333
2522
 
2334
2523
 
2335
- def blueprint():
2524
+ def blueprint() -> AuthenticatedBlueprint:
2525
+ """
2526
+ Creates and configures an authenticated Flask Blueprint for handling various routes
2527
+ related to Data Identifiers (DIDs) and their associated functionalities.
2528
+ """
2336
2529
  bp = AuthenticatedBlueprint('dids', __name__, url_prefix='/dids')
2337
2530
 
2338
- scope_view = Scope.as_view('scope')
2339
- bp.add_url_rule('/<scope>/', view_func=scope_view, methods=['get', ])
2340
- guid_lookup_view = GUIDLookup.as_view('guid_lookup')
2341
- bp.add_url_rule('/<guid>/guid', view_func=guid_lookup_view, methods=['get', ])
2342
- search_view = Search.as_view('search')
2343
- bp.add_url_rule('/<scope>/dids/search', view_func=search_view, methods=['get', ])
2344
2531
  dids_view = DIDs.as_view('dids')
2345
- bp.add_url_rule('/<path:scope_name>/status', view_func=dids_view, methods=['put', 'get'])
2346
- files_view = Files.as_view('files')
2347
- bp.add_url_rule('/<path:scope_name>/files', view_func=files_view, methods=['get', ])
2348
- attachment_history_view = AttachmentHistory.as_view('attachment_history')
2349
- bp.add_url_rule('/<path:scope_name>/dids/history', view_func=attachment_history_view, methods=['get', ])
2350
- attachment_view = Attachment.as_view('attachment')
2351
- bp.add_url_rule('/<path:scope_name>/dids', view_func=attachment_view, methods=['get', 'post', 'delete'])
2532
+ bp.add_url_rule(
2533
+ '/<path:scope_name>/status',
2534
+ view_func=dids_view,
2535
+ methods=['put', 'get'],
2536
+ )
2537
+ bp.add_url_rule(
2538
+ '/<path:scope_name>',
2539
+ view_func=dids_view,
2540
+ methods=['get', 'post'],
2541
+ )
2542
+
2352
2543
  meta_view = Meta.as_view('meta')
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', ])
2355
- bulkdidsmeta_view = BulkDIDsMeta.as_view('bulkdidsmeta')
2356
- bp.add_url_rule('/bulkdidsmeta', view_func=bulkdidsmeta_view, methods=['post', ])
2357
- rules_view = Rules.as_view('rules')
2358
- bp.add_url_rule('/<path:scope_name>/rules', view_func=rules_view, methods=['get', ])
2359
- parents_view = Parents.as_view('parents')
2360
- bp.add_url_rule('/<path:scope_name>/parents', view_func=parents_view, methods=['get', ])
2361
- associated_rules_view = AssociatedRules.as_view('associated_rules')
2362
- bp.add_url_rule('/<path:scope_name>/associated_rules', view_func=associated_rules_view, methods=['get', ])
2363
- follow_view = Follow.as_view('follow')
2364
- bp.add_url_rule('/<path:scope_name>/follow', view_func=follow_view, methods=['get', 'post', 'delete'])
2365
- bp.add_url_rule('/<path:scope_name>', view_func=dids_view, methods=['get', 'post'])
2366
- bulkdids_view = BulkDIDS.as_view('bulkdids')
2367
- bp.add_url_rule('', view_func=bulkdids_view, methods=['post', ])
2368
- sample_view_legacy = SampleLegacy.as_view('sample')
2369
- bp.add_url_rule('/<input_scope>/<input_name>/<output_scope>/<output_name>/<nbfiles>/sample', view_func=sample_view_legacy, methods=['post', ])
2370
- sample_view = Sample.as_view('sample_new')
2371
- bp.add_url_rule('/sample', view_func=sample_view, methods=['post', ])
2372
- attachements_view = Attachments.as_view('attachments')
2373
- bp.add_url_rule('/attachments', view_func=attachements_view, methods=['post', ])
2374
- new_dids_view = NewDIDs.as_view('new_dids')
2375
- bp.add_url_rule('/new', view_func=new_dids_view, methods=['get', ])
2376
- resurrect_view = Resurrect.as_view('resurrect')
2377
- bp.add_url_rule('/resurrect', view_func=resurrect_view, methods=['post', ])
2378
- bulkmeta_view = BulkMeta.as_view('bulkmeta')
2379
- bp.add_url_rule('/bulkmeta', view_func=bulkmeta_view, methods=['post', ])
2380
- files_view = BulkFiles.as_view('bulkfiles')
2381
- bp.add_url_rule('/bulkfiles', view_func=files_view, methods=['post', ])
2544
+ bp.add_url_rule(
2545
+ '/<path:scope_name>/meta',
2546
+ defaults={'key': None},
2547
+ view_func=meta_view,
2548
+ methods=['get', 'post', 'delete'],
2549
+ )
2550
+ bp.add_url_rule(
2551
+ '/<path:scope_name>/meta/<key>',
2552
+ view_func=meta_view,
2553
+ methods=['post'],
2554
+ )
2555
+
2556
+ bp.add_url_rule(
2557
+ "/bulkdidsmeta",
2558
+ view_func=BulkDIDsMeta.as_view("bulkdidsmeta", mode=BulkDIDsMeta.MODE_SET),
2559
+ methods=["post"],
2560
+ )
2561
+
2562
+ bp.add_url_rule(
2563
+ "/bulkmeta",
2564
+ view_func=BulkDIDsMeta.as_view("bulkmeta", mode=BulkDIDsMeta.MODE_GET),
2565
+ methods=["post"],
2566
+ )
2567
+
2568
+ bp.add_url_rule(
2569
+ '/<path:scope_name>/dids',
2570
+ view_func=Attachment.as_view('attachment'),
2571
+ methods=['get', 'post', 'delete'],
2572
+ )
2573
+
2574
+ bp.add_url_rule(
2575
+ '/new',
2576
+ view_func=NewDIDs.as_view('new_dids'),
2577
+ methods=['get'],
2578
+ )
2579
+
2580
+ bp.add_url_rule(
2581
+ '',
2582
+ view_func=BulkDIDS.as_view('bulkdids'),
2583
+ methods=['post'],
2584
+ )
2585
+
2586
+ bp.add_url_rule(
2587
+ '/<path:scope_name>/dids/history',
2588
+ view_func=AttachmentHistory.as_view('attachment_history'),
2589
+ methods=['get'],
2590
+ )
2591
+
2592
+ bp.add_url_rule(
2593
+ '/attachments',
2594
+ view_func=Attachments.as_view('attachments'),
2595
+ methods=['post'],
2596
+ )
2597
+
2598
+ bp.add_url_rule(
2599
+ '/<scope>/dids/search',
2600
+ view_func=Search.as_view('search'),
2601
+ methods=['get'],
2602
+ )
2603
+
2604
+ bp.add_url_rule(
2605
+ '/<scope>/',
2606
+ view_func=Scope.as_view('scope'),
2607
+ methods=['get'],
2608
+ )
2609
+
2610
+ bp.add_url_rule(
2611
+ '/<guid>/guid',
2612
+ view_func=GUIDLookup.as_view('guid_lookup'),
2613
+ methods=['get'],
2614
+ )
2615
+
2616
+ bp.add_url_rule(
2617
+ '/<path:scope_name>/files',
2618
+ view_func=Files.as_view('files'),
2619
+ methods=['get'],
2620
+ )
2621
+
2622
+ bp.add_url_rule(
2623
+ '/bulkfiles',
2624
+ view_func=BulkFiles.as_view('bulkfiles'),
2625
+ methods=['post'],
2626
+ )
2627
+
2628
+ bp.add_url_rule(
2629
+ '/<path:scope_name>/rules',
2630
+ view_func=Rules.as_view('rules'),
2631
+ methods=['get'],
2632
+ )
2633
+
2634
+ bp.add_url_rule(
2635
+ '/<path:scope_name>/parents',
2636
+ view_func=Parents.as_view('parents'),
2637
+ methods=['get'],
2638
+ )
2639
+
2640
+ bp.add_url_rule(
2641
+ '/<path:scope_name>/associated_rules',
2642
+ view_func=AssociatedRules.as_view('associated_rules'),
2643
+ methods=['get'],
2644
+ )
2645
+
2646
+ bp.add_url_rule(
2647
+ '/<path:scope_name>/follow',
2648
+ view_func=Follow.as_view('follow'),
2649
+ methods=['get', 'post', 'delete'],
2650
+ )
2651
+
2652
+ bp.add_url_rule(
2653
+ '/<input_scope>/<input_name>/<output_scope>/<output_name>/<nbfiles>/sample',
2654
+ view_func=SampleLegacy.as_view('sample'),
2655
+ methods=['post'],
2656
+ )
2657
+
2658
+ bp.add_url_rule(
2659
+ '/sample',
2660
+ view_func=Sample.as_view('sample_new'),
2661
+ methods=['post'],
2662
+ )
2663
+
2664
+ bp.add_url_rule(
2665
+ '/resurrect',
2666
+ view_func=Resurrect.as_view('resurrect'),
2667
+ methods=['post'],
2668
+ )
2382
2669
 
2383
2670
  bp.after_request(response_headers)
2384
2671
  return bp