rucio 37.2.0__py3-none-any.whl → 37.4.0__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (127) hide show
  1. rucio/cli/rule.py +1 -1
  2. rucio/client/accountclient.py +205 -60
  3. rucio/client/accountlimitclient.py +84 -25
  4. rucio/client/baseclient.py +85 -48
  5. rucio/client/client.py +49 -41
  6. rucio/client/configclient.py +36 -13
  7. rucio/client/credentialclient.py +16 -6
  8. rucio/client/didclient.py +321 -133
  9. rucio/client/diracclient.py +13 -6
  10. rucio/client/downloadclient.py +435 -165
  11. rucio/client/exportclient.py +8 -2
  12. rucio/client/fileclient.py +10 -3
  13. rucio/client/importclient.py +4 -1
  14. rucio/client/lifetimeclient.py +48 -31
  15. rucio/client/lockclient.py +22 -7
  16. rucio/client/metaconventionsclient.py +59 -21
  17. rucio/client/pingclient.py +3 -1
  18. rucio/client/replicaclient.py +213 -96
  19. rucio/client/requestclient.py +123 -16
  20. rucio/client/rseclient.py +385 -160
  21. rucio/client/ruleclient.py +147 -51
  22. rucio/client/scopeclient.py +35 -10
  23. rucio/client/subscriptionclient.py +60 -27
  24. rucio/client/touchclient.py +16 -7
  25. rucio/common/plugins.py +1 -1
  26. rucio/core/did.py +2 -3
  27. rucio/core/permission/generic.py +37 -1
  28. rucio/core/replica.py +6 -6
  29. rucio/core/rule.py +5 -3
  30. rucio/daemons/judge/evaluator.py +1 -1
  31. rucio/db/sqla/util.py +1 -1
  32. rucio/gateway/authentication.py +58 -88
  33. rucio/gateway/config.py +63 -75
  34. rucio/gateway/did.py +245 -329
  35. rucio/gateway/dirac.py +33 -34
  36. rucio/gateway/exporter.py +27 -30
  37. rucio/gateway/importer.py +12 -14
  38. rucio/gateway/lifetime_exception.py +16 -24
  39. rucio/gateway/lock.py +27 -40
  40. rucio/gateway/replica.py +334 -249
  41. rucio/gateway/request.py +176 -103
  42. rucio/gateway/rse.py +191 -218
  43. rucio/gateway/rule.py +115 -146
  44. rucio/gateway/scope.py +18 -25
  45. rucio/gateway/subscription.py +90 -108
  46. rucio/gateway/trace.py +48 -0
  47. rucio/vcsversion.py +3 -3
  48. rucio/web/rest/flaskapi/v1/accounts.py +2 -2
  49. rucio/web/rest/flaskapi/v1/auth.py +15 -0
  50. rucio/web/rest/flaskapi/v1/common.py +3 -0
  51. rucio/web/rest/flaskapi/v1/config.py +7 -7
  52. rucio/web/rest/flaskapi/v1/dids.py +55 -55
  53. rucio/web/rest/flaskapi/v1/dirac.py +2 -2
  54. rucio/web/rest/flaskapi/v1/export.py +1 -1
  55. rucio/web/rest/flaskapi/v1/import.py +1 -1
  56. rucio/web/rest/flaskapi/v1/lifetime_exceptions.py +5 -5
  57. rucio/web/rest/flaskapi/v1/locks.py +4 -4
  58. rucio/web/rest/flaskapi/v1/main.py +17 -10
  59. rucio/web/rest/flaskapi/v1/redirect.py +1 -1
  60. rucio/web/rest/flaskapi/v1/replicas.py +30 -29
  61. rucio/web/rest/flaskapi/v1/requests.py +211 -20
  62. rucio/web/rest/flaskapi/v1/rses.py +37 -37
  63. rucio/web/rest/flaskapi/v1/rules.py +15 -15
  64. rucio/web/rest/flaskapi/v1/scopes.py +3 -3
  65. rucio/web/rest/flaskapi/v1/subscriptions.py +9 -9
  66. rucio/web/rest/flaskapi/v1/traces.py +75 -77
  67. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/rucio.cfg.template +0 -1
  68. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/rucio_multi_vo.cfg.template +0 -1
  69. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/METADATA +1 -1
  70. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/RECORD +127 -126
  71. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/WHEEL +1 -1
  72. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/alembic.ini.template +0 -0
  73. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/alembic_offline.ini.template +0 -0
  74. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/globus-config.yml.template +0 -0
  75. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/ldap.cfg.template +0 -0
  76. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_approval_request.tmpl +0 -0
  77. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_approved_admin.tmpl +0 -0
  78. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_approved_user.tmpl +0 -0
  79. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_denied_admin.tmpl +0 -0
  80. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_denied_user.tmpl +0 -0
  81. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/mail_templates/rule_ok_notification.tmpl +0 -0
  82. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/rse-accounts.cfg.template +0 -0
  83. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/etc/rucio.cfg.atlas.client.template +0 -0
  84. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/requirements.server.txt +0 -0
  85. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/tools/bootstrap.py +0 -0
  86. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/tools/merge_rucio_configs.py +0 -0
  87. {rucio-37.2.0.data → rucio-37.4.0.data}/data/rucio/tools/reset_database.py +0 -0
  88. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio +0 -0
  89. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-abacus-account +0 -0
  90. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-abacus-collection-replica +0 -0
  91. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-abacus-rse +0 -0
  92. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-admin +0 -0
  93. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-atropos +0 -0
  94. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-auditor +0 -0
  95. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-automatix +0 -0
  96. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-bb8 +0 -0
  97. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-cache-client +0 -0
  98. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-cache-consumer +0 -0
  99. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-finisher +0 -0
  100. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-poller +0 -0
  101. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-preparer +0 -0
  102. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-receiver +0 -0
  103. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-stager +0 -0
  104. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-submitter +0 -0
  105. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-conveyor-throttler +0 -0
  106. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-dark-reaper +0 -0
  107. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-dumper +0 -0
  108. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-follower +0 -0
  109. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-hermes +0 -0
  110. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-judge-cleaner +0 -0
  111. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-judge-evaluator +0 -0
  112. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-judge-injector +0 -0
  113. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-judge-repairer +0 -0
  114. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-kronos +0 -0
  115. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-minos +0 -0
  116. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-minos-temporary-expiration +0 -0
  117. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-necromancer +0 -0
  118. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-oauth-manager +0 -0
  119. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-reaper +0 -0
  120. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-replica-recoverer +0 -0
  121. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-rse-decommissioner +0 -0
  122. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-storage-consistency-actions +0 -0
  123. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-transmogrifier +0 -0
  124. {rucio-37.2.0.data → rucio-37.4.0.data}/scripts/rucio-undertaker +0 -0
  125. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
  126. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/licenses/LICENSE +0 -0
  127. {rucio-37.2.0.dist-info → rucio-37.4.0.dist-info}/top_level.txt +0 -0
@@ -226,7 +226,7 @@ class Replicas(ErrorHandlingMethodView):
226
226
  description: Not acceptable
227
227
  """
228
228
  try:
229
- scope, name = parse_scope_name(scope_name, request.environ.get('vo'))
229
+ scope, name = parse_scope_name(scope_name, request.environ['vo'])
230
230
  except ValueError as error:
231
231
  return generate_http_error_flask(400, error)
232
232
 
@@ -252,7 +252,7 @@ class Replicas(ErrorHandlingMethodView):
252
252
  def _list_and_sort_replicas(vo):
253
253
  # we need to call list_replicas before starting to reply
254
254
  # otherwise the exceptions won't be propagated correctly
255
- for rfile in list_replicas(dids=dids, schemes=schemes, vo=vo):
255
+ for rfile in list_replicas(dids=dids, schemes=schemes, vo=vo): # type: ignore (pending https://github.com/rucio/rucio/issues/7739)
256
256
  replicas = []
257
257
  dictreplica = {}
258
258
  for rse in rfile['rses']:
@@ -264,7 +264,7 @@ class Replicas(ErrorHandlingMethodView):
264
264
  rfile['pfns'] = dict(_sorted_with_priorities(rfile['pfns'], replicas, limit=limit))
265
265
  yield rfile
266
266
 
267
- rfiles = _list_and_sort_replicas(vo=request.environ.get('vo'))
267
+ rfiles = _list_and_sort_replicas(vo=request.environ['vo'])
268
268
  if metalink:
269
269
  response_generator = _generate_metalink_response(rfiles, 'atlas', detailed_url=False)
270
270
  else:
@@ -359,8 +359,8 @@ class Replicas(ErrorHandlingMethodView):
359
359
  add_replicas(
360
360
  rse=rse,
361
361
  files=files,
362
- issuer=request.environ.get('issuer'),
363
- vo=request.environ.get('vo'),
362
+ issuer=request.environ['issuer'],
363
+ vo=request.environ['vo'],
364
364
  ignore_availability=param_get(parameters, 'ignore_availability', default=False),
365
365
  )
366
366
  except InvalidPath as error:
@@ -432,7 +432,7 @@ class Replicas(ErrorHandlingMethodView):
432
432
  files = param_get(parameters, 'files')
433
433
 
434
434
  try:
435
- update_replicas_states(rse=rse, files=files, issuer=request.environ.get('issuer'), vo=request.environ.get('vo'))
435
+ update_replicas_states(rse=rse, files=files, issuer=request.environ['issuer'], vo=request.environ['vo'])
436
436
  except AccessDenied as error:
437
437
  return generate_http_error_flask(401, error)
438
438
 
@@ -486,8 +486,8 @@ class Replicas(ErrorHandlingMethodView):
486
486
  delete_replicas(
487
487
  rse=rse,
488
488
  files=files,
489
- issuer=request.environ.get('issuer'),
490
- vo=request.environ.get('vo'),
489
+ issuer=request.environ['issuer'],
490
+ vo=request.environ['vo'],
491
491
  ignore_availability=param_get(parameters, 'ignore_availability', default=False),
492
492
  )
493
493
  except AccessDenied as error:
@@ -746,8 +746,8 @@ class ListReplicas(ErrorHandlingMethodView):
746
746
  yield rfile
747
747
 
748
748
  rfiles = _list_and_sort_replicas(request_id=request.environ.get('request_id'),
749
- issuer=request.environ.get('issuer'),
750
- vo=request.environ.get('vo'))
749
+ issuer=request.environ['issuer'],
750
+ vo=request.environ['vo'])
751
751
  if metalink:
752
752
  policy_schema = config_get('policy', 'schema', raise_exception=False, default='generic')
753
753
  response_generator = _generate_metalink_response(rfiles, policy_schema)
@@ -822,7 +822,7 @@ class ReplicasDIDs(ErrorHandlingMethodView):
822
822
  for pfn in get_did_from_pfns(pfns, rse, vo=vo):
823
823
  yield dumps(pfn) + '\n'
824
824
 
825
- return try_stream(generate(vo=request.environ.get('vo')))
825
+ return try_stream(generate(vo=request.environ['vo']))
826
826
  except AccessDenied as error:
827
827
  return generate_http_error_flask(401, error)
828
828
 
@@ -882,7 +882,7 @@ class BadReplicas(ErrorHandlingMethodView):
882
882
 
883
883
  try:
884
884
  not_declared_files = declare_bad_file_replicas(replicas, reason=reason,
885
- issuer=request.environ.get('issuer'), vo=request.environ.get('vo'),
885
+ issuer=request.environ['issuer'], vo=request.environ['vo'],
886
886
  force=force)
887
887
  return not_declared_files, 201
888
888
  except AccessDenied as error:
@@ -1002,7 +1002,7 @@ class SuspiciousReplicas(ErrorHandlingMethodView):
1002
1002
  reason = param_get(parameters, 'reason', default=None)
1003
1003
 
1004
1004
  try:
1005
- not_declared_files = declare_suspicious_file_replicas(pfns=pfns, reason=reason, issuer=request.environ.get('issuer'), vo=request.environ.get('vo'))
1005
+ not_declared_files = declare_suspicious_file_replicas(pfns=pfns, reason=reason, issuer=request.environ['issuer'], vo=request.environ['vo'])
1006
1006
  return not_declared_files, 201
1007
1007
  except AccessDenied as error:
1008
1008
  return generate_http_error_flask(401, error)
@@ -1081,7 +1081,7 @@ class SuspiciousReplicas(ErrorHandlingMethodView):
1081
1081
  if 'nattempts' in params:
1082
1082
  nattempts = int(params['nattempts'][0])
1083
1083
 
1084
- result = get_suspicious_files(rse_expression=rse_expression, younger_than=younger_than, nattempts=nattempts, vo=request.environ.get('vo'))
1084
+ result = get_suspicious_files(rse_expression=rse_expression, younger_than=younger_than, nattempts=nattempts, vo=request.environ['vo'])
1085
1085
  return Response(render_json(result), 200, content_type='application/json')
1086
1086
 
1087
1087
 
@@ -1180,7 +1180,8 @@ class BadReplicasStates(ErrorHandlingMethodView):
1180
1180
  406:
1181
1181
  description: Not acceptable
1182
1182
  """
1183
- state, rse, younger_than, older_than, limit, list_pfns = None, None, None, None, None, None
1183
+ list_pfns = False
1184
+ state, rse, younger_than, older_than, limit = None, None, None, None, None
1184
1185
  if request.query_string:
1185
1186
  query_string = request.query_string.decode(encoding='utf-8')
1186
1187
  try:
@@ -1208,7 +1209,7 @@ class BadReplicasStates(ErrorHandlingMethodView):
1208
1209
  vo=vo):
1209
1210
  yield dumps(row, cls=APIEncoder) + '\n'
1210
1211
 
1211
- return try_stream(generate(vo=request.environ.get('vo')))
1212
+ return try_stream(generate(vo=request.environ['vo']))
1212
1213
 
1213
1214
 
1214
1215
  class BadReplicasSummary(ErrorHandlingMethodView):
@@ -1287,7 +1288,7 @@ class BadReplicasSummary(ErrorHandlingMethodView):
1287
1288
  to_date=to_date, vo=vo):
1288
1289
  yield dumps(row, cls=APIEncoder) + '\n'
1289
1290
 
1290
- return try_stream(generate(vo=request.environ.get('vo')))
1291
+ return try_stream(generate(vo=request.environ['vo']))
1291
1292
 
1292
1293
 
1293
1294
  class DatasetReplicas(ErrorHandlingMethodView):
@@ -1370,7 +1371,7 @@ class DatasetReplicas(ErrorHandlingMethodView):
1370
1371
  description: Not acceptable
1371
1372
  """
1372
1373
  try:
1373
- scope, name = parse_scope_name(scope_name, request.environ.get('vo'))
1374
+ scope, name = parse_scope_name(scope_name, request.environ['vo'])
1374
1375
 
1375
1376
  def generate(_deep, vo):
1376
1377
  for row in list_dataset_replicas(scope=scope, name=name, deep=_deep, vo=vo):
@@ -1378,7 +1379,7 @@ class DatasetReplicas(ErrorHandlingMethodView):
1378
1379
 
1379
1380
  deep = request.args.get('deep', default=False)
1380
1381
 
1381
- return try_stream(generate(_deep=deep, vo=request.environ.get('vo')))
1382
+ return try_stream(generate(_deep=deep, vo=request.environ['vo']))
1382
1383
  except ValueError as error:
1383
1384
  return generate_http_error_flask(400, error)
1384
1385
 
@@ -1483,7 +1484,7 @@ class DatasetReplicasBulk(ErrorHandlingMethodView):
1483
1484
  for row in list_dataset_replicas_bulk(dids=dids, vo=vo):
1484
1485
  yield dumps(row, cls=APIEncoder) + '\n'
1485
1486
 
1486
- return try_stream(generate(vo=request.environ.get('vo')))
1487
+ return try_stream(generate(vo=request.environ['vo']))
1487
1488
  except InvalidObject as error:
1488
1489
  return generate_http_error_flask(400, error, f'Cannot validate DIDs: {error}')
1489
1490
 
@@ -1520,7 +1521,7 @@ class DatasetReplicasVP(ErrorHandlingMethodView):
1520
1521
  description: Not acceptable
1521
1522
  """
1522
1523
  try:
1523
- scope, name = parse_scope_name(scope_name, request.environ.get('vo'))
1524
+ scope, name = parse_scope_name(scope_name, request.environ['vo'])
1524
1525
 
1525
1526
  def generate(_deep, vo):
1526
1527
  for row in list_dataset_replicas_vp(scope=scope, name=name, deep=_deep, vo=vo):
@@ -1528,7 +1529,7 @@ class DatasetReplicasVP(ErrorHandlingMethodView):
1528
1529
 
1529
1530
  deep = request.args.get('deep', default=False)
1530
1531
 
1531
- return try_stream(generate(_deep=deep, vo=request.environ.get('vo')))
1532
+ return try_stream(generate(_deep=deep, vo=request.environ['vo']))
1532
1533
  except ValueError as error:
1533
1534
  return generate_http_error_flask(400, error)
1534
1535
 
@@ -1610,7 +1611,7 @@ class ReplicasRSE(ErrorHandlingMethodView):
1610
1611
  for row in list_datasets_per_rse(rse=rse, vo=vo):
1611
1612
  yield dumps(row, cls=APIEncoder) + '\n'
1612
1613
 
1613
- return try_stream(generate(vo=request.environ.get('vo')))
1614
+ return try_stream(generate(vo=request.environ['vo']))
1614
1615
 
1615
1616
 
1616
1617
  class BadDIDs(ErrorHandlingMethodView):
@@ -1676,11 +1677,11 @@ class BadDIDs(ErrorHandlingMethodView):
1676
1677
  not_declared_files = add_bad_dids(
1677
1678
  dids=param_get(parameters, 'dids', default=[]),
1678
1679
  rse=param_get(parameters, 'rse', default=None),
1679
- issuer=request.environ.get('issuer'),
1680
+ issuer=request.environ['issuer'],
1680
1681
  state=BadFilesStatus.BAD,
1681
1682
  reason=param_get(parameters, 'reason', default=None),
1682
1683
  expires_at=expires_at,
1683
- vo=request.environ.get('vo'),
1684
+ vo=request.environ['vo'],
1684
1685
  )
1685
1686
  except (ValueError, InvalidType) as error:
1686
1687
  return generate_http_error_flask(400, ValueError.__name__, error.args[0])
@@ -1745,11 +1746,11 @@ class BadPFNs(ErrorHandlingMethodView):
1745
1746
  try:
1746
1747
  add_bad_pfns(
1747
1748
  pfns=param_get(parameters, 'pfns', default=[]),
1748
- issuer=request.environ.get('issuer'),
1749
+ issuer=request.environ['issuer'],
1749
1750
  state=param_get(parameters, 'state', default=None),
1750
1751
  reason=param_get(parameters, 'reason', default=None),
1751
1752
  expires_at=expires_at,
1752
- vo=request.environ.get('vo'),
1753
+ vo=request.environ['vo'],
1753
1754
  )
1754
1755
  except (ValueError, InvalidType) as error:
1755
1756
  return generate_http_error_flask(400, ValueError.__name__, error.args[0])
@@ -1816,8 +1817,8 @@ class Tombstone(ErrorHandlingMethodView):
1816
1817
  rse=replica['rse'],
1817
1818
  scope=replica['scope'],
1818
1819
  name=replica['name'],
1819
- issuer=request.environ.get('issuer'),
1820
- vo=request.environ.get('vo'),
1820
+ issuer=request.environ['issuer'],
1821
+ vo=request.environ['vo'],
1821
1822
  )
1822
1823
  except ReplicaNotFound as error:
1823
1824
  return generate_http_error_flask(404, error)
@@ -13,18 +13,18 @@
13
13
  # limitations under the License.
14
14
 
15
15
  import json
16
- from typing import TYPE_CHECKING
16
+ from typing import TYPE_CHECKING, Union, cast
17
17
 
18
18
  import flask
19
19
  from flask import Flask, Response
20
20
 
21
- from rucio.common.exception import RequestNotFound
21
+ from rucio.common.exception import AccessDenied, RequestNotFound
22
22
  from rucio.common.utils import APIEncoder, render_json
23
23
  from rucio.core.rse import get_rses_with_attribute_value
24
- from rucio.db.sqla.constants import RequestState
24
+ from rucio.db.sqla.constants import RequestState, TransferLimitDirection
25
25
  from rucio.gateway import request
26
26
  from rucio.web.rest.flaskapi.authenticated_bp import AuthenticatedBlueprint
27
- 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
27
+ from rucio.web.rest.flaskapi.v1.common import ErrorHandlingMethodView, check_accept_header_wrapper_flask, generate_http_error_flask, json_parameters, param_get, parse_scope_name, response_headers, try_stream
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from collections.abc import Iterator
@@ -178,7 +178,7 @@ class RequestGet(ErrorHandlingMethodView):
178
178
  description: Not acceptable
179
179
  """
180
180
  try:
181
- scope, name = parse_scope_name(scope_name, flask.request.environ.get('vo'))
181
+ scope, name = parse_scope_name(scope_name, flask.request.environ['vo'])
182
182
  except ValueError as error:
183
183
  return generate_http_error_flask(400, error)
184
184
 
@@ -187,8 +187,8 @@ class RequestGet(ErrorHandlingMethodView):
187
187
  scope=scope,
188
188
  name=name,
189
189
  rse=rse,
190
- issuer=flask.request.environ.get('issuer'),
191
- vo=flask.request.environ.get('vo'),
190
+ issuer=flask.request.environ['issuer'],
191
+ vo=flask.request.environ['vo'],
192
192
  )
193
193
  return Response(json.dumps(request_data, cls=APIEncoder), content_type='application/json')
194
194
  except RequestNotFound as error:
@@ -343,7 +343,7 @@ class RequestHistoryGet(ErrorHandlingMethodView):
343
343
  description: Not acceptable
344
344
  """
345
345
  try:
346
- scope, name = parse_scope_name(scope_name, flask.request.environ.get('vo'))
346
+ scope, name = parse_scope_name(scope_name, flask.request.environ['vo'])
347
347
  except ValueError as error:
348
348
  return generate_http_error_flask(400, error)
349
349
 
@@ -352,8 +352,8 @@ class RequestHistoryGet(ErrorHandlingMethodView):
352
352
  scope=scope,
353
353
  name=name,
354
354
  rse=rse,
355
- issuer=flask.request.environ.get('issuer'),
356
- vo=flask.request.environ.get('vo'),
355
+ issuer=flask.request.environ['issuer'],
356
+ vo=flask.request.environ['vo'],
357
357
  )
358
358
  return Response(json.dumps(request_data, cls=APIEncoder), content_type='application/json')
359
359
  except RequestNotFound as error:
@@ -564,11 +564,11 @@ class RequestList(ErrorHandlingMethodView):
564
564
  src_rses = []
565
565
  dst_rses = []
566
566
  if src_site:
567
- src_rses = get_rses_with_attribute_value(key='site', value=src_site, vo=flask.request.environ.get('vo'))
567
+ src_rses = get_rses_with_attribute_value(key='site', value=src_site, vo=flask.request.environ['vo'])
568
568
  if not src_rses:
569
569
  return generate_http_error_flask(404, 'NotFound', f'Could not resolve site name {src_site} to RSE')
570
570
  src_rses = [rse['rse_name'] for rse in src_rses]
571
- dst_rses = get_rses_with_attribute_value(key='site', value=dst_site, vo=flask.request.environ.get('vo'))
571
+ dst_rses = get_rses_with_attribute_value(key='site', value=dst_site, vo=flask.request.environ['vo'])
572
572
  if not dst_rses:
573
573
  return generate_http_error_flask(404, 'NotFound', f'Could not resolve site name {dst_site} to RSE')
574
574
  dst_rses = [rse['rse_name'] for rse in dst_rses]
@@ -576,11 +576,15 @@ class RequestList(ErrorHandlingMethodView):
576
576
  dst_rses = [dst_rse]
577
577
  src_rses = [src_rse]
578
578
 
579
+ # Manual cast to list[str] as static code analysis erroneously sees these as list[Optional[str]]
580
+ src_rses = cast("list[str]", src_rses)
581
+ dst_rses = cast("list[str]", dst_rses)
582
+
579
583
  def generate(issuer, vo):
580
584
  for result in request.list_requests(src_rses, dst_rses, states, issuer=issuer, vo=vo):
581
585
  yield render_json(**result) + '\n'
582
586
 
583
- return try_stream(generate(issuer=flask.request.environ.get('issuer'), vo=flask.request.environ.get('vo')))
587
+ return try_stream(generate(issuer=flask.request.environ['issuer'], vo=flask.request.environ['vo']))
584
588
 
585
589
 
586
590
  class RequestHistoryList(ErrorHandlingMethodView):
@@ -779,8 +783,8 @@ class RequestHistoryList(ErrorHandlingMethodView):
779
783
  src_site = flask.request.args.get('src_site', default=None)
780
784
  dst_site = flask.request.args.get('dst_site', default=None)
781
785
  request_states = flask.request.args.get('request_states', default=None)
782
- offset = flask.request.args.get('offset', default=0)
783
- limit = flask.request.args.get('limit', default=100)
786
+ offset = flask.request.args.get('offset', type=int, default=0)
787
+ limit = flask.request.args.get('limit', type=int, default=100)
784
788
 
785
789
  if not request_states:
786
790
  return generate_http_error_flask(400, 'MissingParameter', 'Request state is missing')
@@ -801,11 +805,11 @@ class RequestHistoryList(ErrorHandlingMethodView):
801
805
  src_rses = []
802
806
  dst_rses = []
803
807
  if src_site:
804
- src_rses = get_rses_with_attribute_value(key='site', value=src_site, vo=flask.request.environ.get('vo'))
808
+ src_rses = get_rses_with_attribute_value(key='site', value=src_site, vo=flask.request.environ['vo'])
805
809
  if not src_rses:
806
810
  return generate_http_error_flask(404, 'NotFound', f'Could not resolve site name {src_site} to RSE')
807
811
  src_rses = [rse['rse_name'] for rse in src_rses]
808
- dst_rses = get_rses_with_attribute_value(key='site', value=dst_site, vo=flask.request.environ.get('vo'))
812
+ dst_rses = get_rses_with_attribute_value(key='site', value=dst_site, vo=flask.request.environ['vo'])
809
813
  if not dst_rses:
810
814
  return generate_http_error_flask(404, 'NotFound', f'Could not resolve site name {dst_site} to RSE')
811
815
  dst_rses = [rse['rse_name'] for rse in dst_rses]
@@ -813,11 +817,15 @@ class RequestHistoryList(ErrorHandlingMethodView):
813
817
  dst_rses = [dst_rse]
814
818
  src_rses = [src_rse]
815
819
 
820
+ # Manual cast to list[str] as static code analysis erroneously sees these as list[Optional[str]]
821
+ src_rses = cast("list[str]", src_rses)
822
+ dst_rses = cast("list[str]", dst_rses)
823
+
816
824
  def generate(issuer, vo):
817
825
  for result in request.list_requests_history(src_rses, dst_rses, states, issuer=issuer, vo=vo, offset=offset, limit=limit):
818
826
  yield render_json(**result) + '\n'
819
827
 
820
- return try_stream(generate(issuer=flask.request.environ.get('issuer'), vo=flask.request.environ.get('vo')))
828
+ return try_stream(generate(issuer=flask.request.environ['issuer'], vo=flask.request.environ['vo']))
821
829
 
822
830
 
823
831
  class RequestMetricsGet(ErrorHandlingMethodView):
@@ -960,8 +968,8 @@ class RequestMetricsGet(ErrorHandlingMethodView):
960
968
  src_rse=src_rse,
961
969
  activity=activity,
962
970
  group_by_rse_attribute=group_by_rse_attribute,
963
- issuer=flask.request.environ.get('issuer'),
964
- vo=flask.request.environ.get('vo')
971
+ issuer=flask.request.environ['issuer'],
972
+ vo=flask.request.environ['vo']
965
973
  )
966
974
 
967
975
  if format == 'panda':
@@ -972,6 +980,187 @@ class RequestMetricsGet(ErrorHandlingMethodView):
972
980
  yield render_json(**result) + '\n'
973
981
  return try_stream(generate())
974
982
 
983
+ class TransferLimits(ErrorHandlingMethodView):
984
+ """ REST API to get, set or delete transfer limits. """
985
+
986
+ @check_accept_header_wrapper_flask(['application/x-json-stream'])
987
+ def get(self) -> flask.Response:
988
+ """
989
+ ---
990
+ summary: Get Transfer Limits
991
+ description: Get all the transfer limits.
992
+ tags:
993
+ - Requests
994
+ responses:
995
+ 200:
996
+ description: OK
997
+ content:
998
+ application/x-json-stream:
999
+ schema:
1000
+ description: All the transfer limits
1001
+ type: array
1002
+ items:
1003
+ type: object
1004
+ properties:
1005
+ id:
1006
+ description: The transfer limit id.
1007
+ type: string
1008
+ rse_expression:
1009
+ description: The RSE expression for which the limit applies.
1010
+ type: string
1011
+ direction:
1012
+ description: The direction in which this limit applies (source/destination)
1013
+ type: string
1014
+ max_transfers:
1015
+ description: Maximum number of transfers allowed.
1016
+ type: integer
1017
+ volume:
1018
+ description: Maximum transfer volume in bytes.
1019
+ type: integer
1020
+ deadline:
1021
+ description: Maximum waiting time in hours until a datasets gets released.
1022
+ type: integer
1023
+ strategy:
1024
+ description: defines how to handle datasets: `fifo` (each file released separately) or `grouped_fifo` (wait for the entire dataset to fit)
1025
+ type: string
1026
+ transfers:
1027
+ description: Current number of active transfers
1028
+ type: integer
1029
+ waitings:
1030
+ description: Current number of waiting transfers
1031
+ type: integer
1032
+ updated_at:
1033
+ description: Datetime of the last update.
1034
+ type: string
1035
+ created_at:
1036
+ description: Datetime of the creation of the transfer limit.
1037
+ type: string
1038
+ 401:
1039
+ description: Invalid Auth Token
1040
+ """
1041
+ transfer_limits = request.list_transfer_limits(issuer=flask.request.environ['issuer'], vo=flask.request.environ['vo'])
1042
+ def generate() -> "Iterator[str]":
1043
+ for limit in transfer_limits:
1044
+ yield json.dumps(limit, cls=APIEncoder) + '\n'
1045
+ return try_stream(generate())
1046
+
1047
+ def put(self) -> Union[flask.Response, tuple[str, int]]:
1048
+ """
1049
+ ---
1050
+ summary: Set Transfer Limit
1051
+ description: Create or update a transfer limit for a specific RSE expression and activity.
1052
+ tags:
1053
+ - Requests
1054
+ requestBody:
1055
+ content:
1056
+ application/json:
1057
+ schema:
1058
+ type: object
1059
+ required:
1060
+ - rse_expression
1061
+ - max_transfers
1062
+ properties:
1063
+ rse_expression:
1064
+ type: string
1065
+ description: The RSE expression for which the transfer limit is being set.
1066
+ activity:
1067
+ type: string
1068
+ description: The activity to which the transfer limit applies.
1069
+ max_transfers:
1070
+ type: integer
1071
+ description: The maximum number of transfers allowed.
1072
+ direction:
1073
+ type: string
1074
+ description: The direction of the transfer limit (source or destination).
1075
+ enum: ["SOURCE", "DESTINATION"]
1076
+ default: "DESTINATION"
1077
+ volume:
1078
+ type: integer
1079
+ description: The maximum transfer volume in bytes.
1080
+ deadline:
1081
+ type: integer
1082
+ description: The maximum waiting time in hours until a dataset is released.
1083
+ strategy:
1084
+ type: string
1085
+ description: The strategy for handling datasets (e.g., `fifo` or `grouped_fifo`).
1086
+ transfers:
1087
+ type: integer
1088
+ description: The current number of active transfers.
1089
+ waitings:
1090
+ type: integer
1091
+ description: The current number of waiting transfers.
1092
+ responses:
1093
+ 201:
1094
+ description: Transfer limit set successfully.
1095
+ 400:
1096
+ description: Invalid input data.
1097
+ 401:
1098
+ description: Invalid Auth Token.
1099
+ 500:
1100
+ description: Internal server error.
1101
+ """
1102
+ parameters = json_parameters()
1103
+ rse_expression = param_get(parameters, 'rse_expression')
1104
+ max_transfers = param_get(parameters, 'max_transfers')
1105
+
1106
+ try:
1107
+ request.set_transfer_limit(
1108
+ rse_expression=rse_expression,
1109
+ max_transfers=max_transfers,
1110
+ activity=param_get(parameters, 'activity', default=None),
1111
+ direction=param_get(parameters, 'direction', default=TransferLimitDirection.DESTINATION),
1112
+ volume=param_get(parameters, 'volume', default=None),
1113
+ deadline=param_get(parameters, 'deadline', default=None),
1114
+ strategy=param_get(parameters, 'strategy', default=None),
1115
+ transfers=param_get(parameters, 'transfers', default=None),
1116
+ waitings=param_get(parameters, 'waitings', default=None),
1117
+ issuer=flask.request.environ['issuer'],
1118
+ vo=flask.request.environ['vo']
1119
+ )
1120
+ except AccessDenied as error:
1121
+ return generate_http_error_flask(401, error)
1122
+
1123
+ return '', 201
1124
+
1125
+ def delete(self) -> Union[flask.Response, tuple[str, int]]:
1126
+ """
1127
+ ---
1128
+ summary: Delete Transfer Limit
1129
+ description: Delete a transfer limit for an RSE expression.
1130
+ tags:
1131
+ - Requests
1132
+ parameters:
1133
+ - name: rse_expression
1134
+ in: query
1135
+ description: The RSE expression to delete the limit for.
1136
+ required: true
1137
+ schema:
1138
+ type: string
1139
+ responses:
1140
+ 200:
1141
+ description: Transfer limit deleted successfully.
1142
+ 400:
1143
+ description: Invalid input data.
1144
+ 401:
1145
+ description: Invalid Auth Token.
1146
+ 500:
1147
+ description: Internal server error.
1148
+ """
1149
+ parameters = json_parameters()
1150
+ rse_expression = param_get(parameters, 'rse_expression')
1151
+
1152
+ try:
1153
+ request.delete_transfer_limit(
1154
+ rse_expression=rse_expression,
1155
+ activity=param_get(parameters, 'activity', default=None),
1156
+ direction=param_get(parameters, 'direction', default=TransferLimitDirection.DESTINATION),
1157
+ issuer=flask.request.environ['issuer'],
1158
+ vo=flask.request.environ['vo']
1159
+ )
1160
+ except AccessDenied as error:
1161
+ return generate_http_error_flask(401, error)
1162
+
1163
+ return '', 200
975
1164
 
976
1165
  def blueprint():
977
1166
  bp = AuthenticatedBlueprint('requests', __name__, url_prefix='/requests')
@@ -986,6 +1175,8 @@ def blueprint():
986
1175
  bp.add_url_rule('/history/list', view_func=request_history_list_view, methods=['get', ])
987
1176
  request_metrics_view = RequestMetricsGet.as_view('request_metrics_get')
988
1177
  bp.add_url_rule('/metrics', view_func=request_metrics_view, methods=['get', ])
1178
+ transfer_limits_view = TransferLimits.as_view('transfer_limits_get')
1179
+ bp.add_url_rule('/transfer_limits', view_func=transfer_limits_view, methods=['get', 'put', 'delete'])
989
1180
 
990
1181
  bp.after_request(response_headers)
991
1182
  return bp