rucio-clients 32.8.6__py3-none-any.whl → 35.8.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-clients might be problematic. Click here for more details.

Files changed (92) hide show
  1. rucio/__init__.py +0 -1
  2. rucio/alembicrevision.py +1 -2
  3. rucio/client/__init__.py +0 -1
  4. rucio/client/accountclient.py +45 -25
  5. rucio/client/accountlimitclient.py +37 -9
  6. rucio/client/baseclient.py +199 -154
  7. rucio/client/client.py +2 -3
  8. rucio/client/configclient.py +19 -6
  9. rucio/client/credentialclient.py +9 -4
  10. rucio/client/didclient.py +238 -63
  11. rucio/client/diracclient.py +13 -5
  12. rucio/client/downloadclient.py +162 -51
  13. rucio/client/exportclient.py +4 -4
  14. rucio/client/fileclient.py +3 -4
  15. rucio/client/importclient.py +4 -4
  16. rucio/client/lifetimeclient.py +21 -5
  17. rucio/client/lockclient.py +18 -8
  18. rucio/client/{metaclient.py → metaconventionsclient.py} +18 -15
  19. rucio/client/pingclient.py +0 -1
  20. rucio/client/replicaclient.py +15 -5
  21. rucio/client/requestclient.py +35 -19
  22. rucio/client/rseclient.py +133 -51
  23. rucio/client/ruleclient.py +29 -22
  24. rucio/client/scopeclient.py +8 -6
  25. rucio/client/subscriptionclient.py +47 -35
  26. rucio/client/touchclient.py +8 -4
  27. rucio/client/uploadclient.py +166 -82
  28. rucio/common/__init__.py +0 -1
  29. rucio/common/cache.py +4 -4
  30. rucio/common/config.py +52 -47
  31. rucio/common/constants.py +69 -2
  32. rucio/common/constraints.py +0 -1
  33. rucio/common/didtype.py +24 -22
  34. rucio/common/exception.py +281 -222
  35. rucio/common/extra.py +0 -1
  36. rucio/common/logging.py +54 -38
  37. rucio/common/pcache.py +122 -101
  38. rucio/common/plugins.py +153 -0
  39. rucio/common/policy.py +4 -4
  40. rucio/common/schema/__init__.py +17 -10
  41. rucio/common/schema/atlas.py +7 -5
  42. rucio/common/schema/belleii.py +7 -5
  43. rucio/common/schema/domatpc.py +7 -5
  44. rucio/common/schema/escape.py +7 -5
  45. rucio/common/schema/generic.py +8 -6
  46. rucio/common/schema/generic_multi_vo.py +7 -5
  47. rucio/common/schema/icecube.py +7 -5
  48. rucio/common/stomp_utils.py +0 -1
  49. rucio/common/stopwatch.py +0 -1
  50. rucio/common/test_rucio_server.py +2 -2
  51. rucio/common/types.py +262 -17
  52. rucio/common/utils.py +743 -451
  53. rucio/rse/__init__.py +3 -4
  54. rucio/rse/protocols/__init__.py +0 -1
  55. rucio/rse/protocols/bittorrent.py +184 -0
  56. rucio/rse/protocols/cache.py +1 -2
  57. rucio/rse/protocols/dummy.py +1 -2
  58. rucio/rse/protocols/gfal.py +12 -10
  59. rucio/rse/protocols/globus.py +7 -7
  60. rucio/rse/protocols/gsiftp.py +2 -3
  61. rucio/rse/protocols/http_cache.py +1 -2
  62. rucio/rse/protocols/mock.py +1 -2
  63. rucio/rse/protocols/ngarc.py +1 -2
  64. rucio/rse/protocols/posix.py +12 -13
  65. rucio/rse/protocols/protocol.py +116 -52
  66. rucio/rse/protocols/rclone.py +6 -7
  67. rucio/rse/protocols/rfio.py +4 -5
  68. rucio/rse/protocols/srm.py +9 -10
  69. rucio/rse/protocols/ssh.py +8 -9
  70. rucio/rse/protocols/storm.py +2 -3
  71. rucio/rse/protocols/webdav.py +17 -14
  72. rucio/rse/protocols/xrootd.py +23 -17
  73. rucio/rse/rsemanager.py +19 -7
  74. rucio/vcsversion.py +4 -4
  75. rucio/version.py +5 -13
  76. rucio_clients-35.8.0.data/data/requirements.client.txt +15 -0
  77. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/rucio_client/merge_rucio_configs.py +2 -5
  78. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/scripts/rucio +87 -85
  79. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/scripts/rucio-admin +45 -32
  80. {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/METADATA +13 -13
  81. rucio_clients-35.8.0.dist-info/RECORD +88 -0
  82. {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/WHEEL +1 -1
  83. {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/licenses/AUTHORS.rst +3 -0
  84. rucio/common/schema/cms.py +0 -478
  85. rucio/common/schema/lsst.py +0 -423
  86. rucio_clients-32.8.6.data/data/requirements.txt +0 -55
  87. rucio_clients-32.8.6.dist-info/RECORD +0 -88
  88. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rse-accounts.cfg.template +0 -0
  89. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rucio.cfg.atlas.client.template +0 -0
  90. {rucio_clients-32.8.6.data → rucio_clients-35.8.0.data}/data/etc/rucio.cfg.template +0 -0
  91. {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/licenses/LICENSE +0 -0
  92. {rucio_clients-32.8.6.dist-info → rucio_clients-35.8.0.dist-info}/top_level.txt +0 -0
rucio/common/utils.py CHANGED
@@ -1,4 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
1
  # Copyright European Organization for Nuclear Research (CERN) since 2012
3
2
  #
4
3
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,14 +14,18 @@
15
14
 
16
15
  import argparse
17
16
  import base64
17
+ import copy
18
18
  import datetime
19
19
  import errno
20
20
  import getpass
21
21
  import hashlib
22
22
  import io
23
+ import ipaddress
23
24
  import itertools
24
25
  import json
25
26
  import logging
27
+ import math
28
+ import mmap
26
29
  import os
27
30
  import os.path
28
31
  import re
@@ -32,26 +35,25 @@ import subprocess
32
35
  import tempfile
33
36
  import threading
34
37
  import time
38
+ import zlib
35
39
  from collections import OrderedDict
36
- from configparser import NoOptionError, NoSectionError
40
+ from collections.abc import Callable, Iterable, Iterator, Sequence
37
41
  from enum import Enum
38
42
  from functools import partial, wraps
39
43
  from io import StringIO
40
44
  from itertools import zip_longest
41
- from typing import TYPE_CHECKING
42
- from urllib.parse import urlparse, urlencode, quote, parse_qsl, urlunparse
45
+ from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
46
+ from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse
43
47
  from uuid import uuid4 as uuid
44
48
  from xml.etree import ElementTree
45
49
 
46
- import mmap
47
50
  import requests
48
- import zlib
49
51
 
50
52
  from rucio.common.config import config_get, config_has_section
51
- from rucio.common.exception import MissingModuleException, InvalidType, InputValidationError, MetalinkJsonParsingError, RucioException, \
52
- DuplicateCriteriaInDIDFilter, DIDFilterSyntaxError, InvalidAlgorithmName, PolicyPackageVersionError
53
+ from rucio.common.exception import ConfigNotFound, DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, PolicyPackageVersionError, RucioException
53
54
  from rucio.common.extra import import_extras
54
- from rucio.common.types import InternalAccount, InternalScope
55
+ from rucio.common.plugins import PolicyPackageAlgorithms
56
+ from rucio.common.types import InternalAccount, InternalScope, TraceDict
55
57
 
56
58
  EXTRA_MODULES = import_extras(['paramiko'])
57
59
 
@@ -62,10 +64,11 @@ if EXTRA_MODULES['paramiko']:
62
64
  EXTRA_MODULES['paramiko'] = False
63
65
 
64
66
  if TYPE_CHECKING:
65
- from collections.abc import Callable
66
- from typing import TypeVar
67
-
68
67
  T = TypeVar('T')
68
+ from _typeshed import FileDescriptorOrPath
69
+ from sqlalchemy.orm import Session
70
+
71
+ from rucio.common.types import IPDict, LoggerFunction
69
72
 
70
73
 
71
74
  # HTTP code dictionary. Not complete. Can be extended if needed.
@@ -98,7 +101,7 @@ codes = {
98
101
  DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'
99
102
 
100
103
 
101
- def invert_dict(d):
104
+ def invert_dict(d: dict[Any, Any]) -> dict[Any, Any]:
102
105
  """
103
106
  Invert the dictionary.
104
107
  CAUTION: this function is not deterministic unless the input dictionary is one-to-one mapping.
@@ -109,11 +112,11 @@ def invert_dict(d):
109
112
  return {value: key for key, value in d.items()}
110
113
 
111
114
 
112
- def dids_as_dicts(did_list):
115
+ def dids_as_dicts(did_list: Iterable[Union[str, dict[str, str]]]) -> list[dict[str, str]]:
113
116
  """
114
117
  Converts list of DIDs to list of dictionaries
115
- :param did_list: list of DIDs as either "scope:name" or {"scope":"scope", "name","name"}
116
- :returns: list of dictionaries {"scope":"scope", "name","name"}
118
+ :param did_list: list of DIDs as either "scope:name" or {"scope":"scope", "name":"name"}
119
+ :returns: list of dictionaries {"scope":"scope", "name":"name"}
117
120
  """
118
121
  out = []
119
122
  for did in did_list:
@@ -129,7 +132,12 @@ def dids_as_dicts(did_list):
129
132
  return out
130
133
 
131
134
 
132
- def build_url(url, path=None, params=None, doseq=False):
135
+ def build_url(
136
+ url: str,
137
+ path: Optional[str] = None,
138
+ params: Optional[Union[str, dict[Any, Any], list[tuple[Any, Any]]]] = None,
139
+ doseq: bool = False
140
+ ) -> str:
133
141
  """
134
142
  utitily function to build an url for requests to the rucio system.
135
143
 
@@ -148,7 +156,13 @@ def build_url(url, path=None, params=None, doseq=False):
148
156
  return complete_url
149
157
 
150
158
 
151
- def all_oidc_req_claims_present(scope, audience, required_scope, required_audience, sepatator=" "):
159
+ def all_oidc_req_claims_present(
160
+ scope: Optional[Union[str, list[str]]],
161
+ audience: Optional[Union[str, list[str]]],
162
+ required_scope: Optional[Union[str, list[str]]],
163
+ required_audience: Optional[Union[str, list[str]]],
164
+ separator: str = " "
165
+ ) -> bool:
152
166
  """
153
167
  Checks if both of the following statements are true:
154
168
  - all items in required_scope are present in scope string
@@ -160,7 +174,7 @@ def all_oidc_req_claims_present(scope, audience, required_scope, required_audien
160
174
  :params audience: list of strings or one string where items are separated by a separator input variable
161
175
  :params required_scope: list of strings or one string where items are separated by a separator input variable
162
176
  :params required_audience: list of strings or one string where items are separated by a separator input variable
163
- :params sepatator: separator string, space by default
177
+ :params separator: separator string, space by default
164
178
  :returns : True or False
165
179
  """
166
180
  if not scope:
@@ -184,34 +198,34 @@ def all_oidc_req_claims_present(scope, audience, required_scope, required_audien
184
198
  audience = str(audience)
185
199
  required_scope = str(required_scope)
186
200
  required_audience = str(required_audience)
187
- req_scope_present = all(elem in scope.split(sepatator) for elem in required_scope.split(sepatator))
188
- req_audience_present = all(elem in audience.split(sepatator) for elem in required_audience.split(sepatator))
201
+ req_scope_present = all(elem in scope.split(separator) for elem in required_scope.split(separator))
202
+ req_audience_present = all(elem in audience.split(separator) for elem in required_audience.split(separator))
189
203
  return req_scope_present and req_audience_present
190
204
  elif (isinstance(scope, list) and isinstance(audience, list) and isinstance(required_scope, str) and isinstance(required_audience, str)):
191
205
  scope = [str(it) for it in scope]
192
206
  audience = [str(it) for it in audience]
193
207
  required_scope = str(required_scope)
194
208
  required_audience = str(required_audience)
195
- req_scope_present = all(elem in scope for elem in required_scope.split(sepatator))
196
- req_audience_present = all(elem in audience for elem in required_audience.split(sepatator))
209
+ req_scope_present = all(elem in scope for elem in required_scope.split(separator))
210
+ req_audience_present = all(elem in audience for elem in required_audience.split(separator))
197
211
  return req_scope_present and req_audience_present
198
212
  elif (isinstance(scope, str) and isinstance(audience, str) and isinstance(required_scope, list) and isinstance(required_audience, list)):
199
213
  scope = str(scope)
200
214
  audience = str(audience)
201
215
  required_scope = [str(it) for it in required_scope]
202
216
  required_audience = [str(it) for it in required_audience]
203
- req_scope_present = all(elem in scope.split(sepatator) for elem in required_scope)
204
- req_audience_present = all(elem in audience.split(sepatator) for elem in required_audience)
217
+ req_scope_present = all(elem in scope.split(separator) for elem in required_scope)
218
+ req_audience_present = all(elem in audience.split(separator) for elem in required_audience)
205
219
  return req_scope_present and req_audience_present
206
220
  else:
207
221
  return False
208
222
 
209
223
 
210
- def generate_uuid():
224
+ def generate_uuid() -> str:
211
225
  return str(uuid()).replace('-', '').lower()
212
226
 
213
227
 
214
- def generate_uuid_bytes():
228
+ def generate_uuid_bytes() -> bytes:
215
229
  return uuid().bytes
216
230
 
217
231
 
@@ -222,9 +236,9 @@ PREFERRED_CHECKSUM = GLOBALLY_SUPPORTED_CHECKSUMS[0]
222
236
  CHECKSUM_KEY = 'supported_checksums'
223
237
 
224
238
 
225
- def is_checksum_valid(checksum_name):
239
+ def is_checksum_valid(checksum_name: str) -> bool:
226
240
  """
227
- A simple function to check wether a checksum algorithm is supported.
241
+ A simple function to check whether a checksum algorithm is supported.
228
242
  Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
229
243
 
230
244
  :param checksum_name: The name of the checksum to be verified.
@@ -234,20 +248,19 @@ def is_checksum_valid(checksum_name):
234
248
  return checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS
235
249
 
236
250
 
237
- def set_preferred_checksum(checksum_name):
251
+ def set_preferred_checksum(checksum_name: str) -> None:
238
252
  """
239
- A simple function to check wether a checksum algorithm is supported.
240
- Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
253
+ If the input checksum name is valid,
254
+ set it as PREFERRED_CHECKSUM.
241
255
 
242
256
  :param checksum_name: The name of the checksum to be verified.
243
- :returns: True if checksum_name is in GLOBALLY_SUPPORTED_CHECKSUMS list, False otherwise.
244
257
  """
245
258
  if is_checksum_valid(checksum_name):
246
259
  global PREFERRED_CHECKSUM
247
260
  PREFERRED_CHECKSUM = checksum_name
248
261
 
249
262
 
250
- def adler32(file):
263
+ def adler32(file: "FileDescriptorOrPath") -> str:
251
264
  """
252
265
  An Adler-32 checksum is obtained by calculating two 16-bit checksums A and B
253
266
  and concatenating their bits into a 32-bit integer. A is the sum of all bytes in the
@@ -294,7 +307,7 @@ def adler32(file):
294
307
  CHECKSUM_ALGO_DICT['adler32'] = adler32
295
308
 
296
309
 
297
- def md5(file):
310
+ def md5(file: "FileDescriptorOrPath") -> str:
298
311
  """
299
312
  Runs the MD5 algorithm (RFC-1321) on the binary content of the file named file and returns the hexadecimal digest
300
313
 
@@ -314,7 +327,7 @@ def md5(file):
314
327
  CHECKSUM_ALGO_DICT['md5'] = md5
315
328
 
316
329
 
317
- def sha256(file):
330
+ def sha256(file: "FileDescriptorOrPath") -> str:
318
331
  """
319
332
  Runs the SHA256 algorithm on the binary content of the file named file and returns the hexadecimal digest
320
333
 
@@ -331,7 +344,7 @@ def sha256(file):
331
344
  CHECKSUM_ALGO_DICT['sha256'] = sha256
332
345
 
333
346
 
334
- def crc32(file):
347
+ def crc32(file: "FileDescriptorOrPath") -> str:
335
348
  """
336
349
  Runs the CRC32 algorithm on the binary content of the file named file and returns the hexadecimal digest
337
350
 
@@ -347,7 +360,219 @@ def crc32(file):
347
360
  CHECKSUM_ALGO_DICT['crc32'] = crc32
348
361
 
349
362
 
350
- def str_to_date(string):
363
+ def _next_pow2(num: int) -> int:
364
+ if not num:
365
+ return 0
366
+ return math.ceil(math.log2(num))
367
+
368
+
369
+ def _bittorrent_v2_piece_length_pow2(file_size: int) -> int:
370
+ """
371
+ Automatically chooses the `piece size` so that `piece layers`
372
+ is kept small(er) than usually. This is a balancing act:
373
+ having a big piece_length requires more work on bittorrent client
374
+ side to validate hashes, but having it small requires more
375
+ place to store the `piece layers` in the database.
376
+
377
+ Returns the result as the exponent 'x' for power of 2.
378
+ To get the actual length in bytes, the caller should compute 2^x.
379
+ """
380
+
381
+ # by the bittorrent v2 specification, the piece size is equal to block size = 16KiB
382
+ min_piece_len_pow2 = 14 # 2 ** 14 == 16 KiB
383
+ if not file_size:
384
+ return min_piece_len_pow2
385
+ # Limit the maximum size of pieces_layers hash chain for bittorrent v2,
386
+ # because we'll have to store it in the database
387
+ max_pieces_layers_size_pow2 = 20 # 2 ** 20 == 1 MiB
388
+ # sha256 requires 2 ** 5 == 32 Bytes == 256 bits
389
+ hash_size_pow2 = 5
390
+
391
+ # The closest power of two bigger than the file size
392
+ file_size_pow2 = _next_pow2(file_size)
393
+
394
+ # Compute the target size for the 'pieces layers' in the torrent
395
+ # (as power of two: the closest power-of-two smaller than the number)
396
+ # Will cap at max_pieces_layers_size for files larger than 1TB.
397
+ target_pieces_layers_size = math.sqrt(file_size)
398
+ target_pieces_layers_size_pow2 = min(math.floor(math.log2(target_pieces_layers_size)), max_pieces_layers_size_pow2)
399
+ target_piece_num_pow2 = max(target_pieces_layers_size_pow2 - hash_size_pow2, 0)
400
+
401
+ piece_length_pow2 = max(file_size_pow2 - target_piece_num_pow2, min_piece_len_pow2)
402
+ return piece_length_pow2
403
+
404
+
405
+ def bittorrent_v2_piece_length(file_size: int) -> int:
406
+ return 2 ** _bittorrent_v2_piece_length_pow2(file_size)
407
+
408
+
409
+ def bittorrent_v2_merkle_sha256(file: "FileDescriptorOrPath") -> tuple[bytes, bytes, int]:
410
+ """
411
+ Compute the .torrent v2 hash tree for the given file.
412
+ (http://www.bittorrent.org/beps/bep_0052.html)
413
+ In particular, it will return the root of the merkle hash
414
+ tree of the file, the 'piece layers' as described in the
415
+ previous BEP, and the chosen `piece size`
416
+
417
+ This function will read the file in chunks of 16KiB
418
+ (which is the imposed block size by bittorrent v2) and compute
419
+ the sha256 hash of each block. When enough blocks are read
420
+ to form a `piece`, will compute the merkle hash root of the
421
+ piece from the hashes of its blocks. At the end, the hashes
422
+ of pieces are combined to create the global pieces_root.
423
+ """
424
+
425
+ # by the bittorrent v2 specification, the block size and the
426
+ # minimum piece size are both fixed to 16KiB
427
+ block_size = 16384
428
+ block_size_pow2 = 14 # 2 ** 14 == 16 KiB
429
+ # sha256 requires 2 ** 5 == 32 Bytes == 256 bits
430
+ hash_size = 32
431
+
432
+ def _merkle_root(leafs: list[bytes], nb_levels: int, padding: bytes) -> bytes:
433
+ """
434
+ Build the root of the merkle hash tree from the (possibly incomplete) leafs layer.
435
+ If len(leafs) < 2 ** nb_levels, it will be padded with the padding repeated as many times
436
+ as needed to have 2 ** nb_levels leafs in total.
437
+ """
438
+ nodes = copy.copy(leafs)
439
+ level = nb_levels
440
+
441
+ while level > 0:
442
+ for i in range(2 ** (level - 1)):
443
+ node1 = nodes[2 * i] if 2 * i < len(nodes) else padding
444
+ node2 = nodes[2 * i + 1] if 2 * i + 1 < len(nodes) else padding
445
+ h = hashlib.sha256(node1)
446
+ h.update(node2)
447
+ if i < len(nodes):
448
+ nodes[i] = h.digest()
449
+ else:
450
+ nodes.append(h.digest())
451
+ level -= 1
452
+ return nodes[0] if nodes else padding
453
+
454
+ file_size = os.stat(file).st_size
455
+ piece_length_pow2 = _bittorrent_v2_piece_length_pow2(file_size)
456
+
457
+ block_per_piece_pow2 = piece_length_pow2 - block_size_pow2
458
+ piece_length = 2 ** piece_length_pow2
459
+ block_per_piece = 2 ** block_per_piece_pow2
460
+ piece_num = math.ceil(file_size / piece_length)
461
+
462
+ remaining = file_size
463
+ remaining_in_block = min(file_size, block_size)
464
+ block_hashes = []
465
+ piece_hashes = []
466
+ current_hash = hashlib.sha256()
467
+ block_padding = bytes(hash_size)
468
+ with open(file, 'rb') as f:
469
+ while True:
470
+ data = f.read(remaining_in_block)
471
+ if not data:
472
+ break
473
+
474
+ current_hash.update(data)
475
+
476
+ remaining_in_block -= len(data)
477
+ remaining -= len(data)
478
+
479
+ if not remaining_in_block:
480
+ block_hashes.append(current_hash.digest())
481
+ if len(block_hashes) == block_per_piece or not remaining:
482
+ piece_hashes.append(_merkle_root(block_hashes, nb_levels=block_per_piece_pow2, padding=block_padding))
483
+ block_hashes = []
484
+ current_hash = hashlib.sha256()
485
+ remaining_in_block = min(block_size, remaining)
486
+
487
+ if not remaining:
488
+ break
489
+
490
+ if remaining or remaining_in_block or len(piece_hashes) != piece_num:
491
+ raise RucioException(f'Error while computing merkle sha256 of {file}')
492
+
493
+ piece_padding = _merkle_root([], nb_levels=block_per_piece_pow2, padding=block_padding)
494
+ pieces_root = _merkle_root(piece_hashes, nb_levels=_next_pow2(piece_num), padding=piece_padding)
495
+ pieces_layers = b''.join(piece_hashes) if len(piece_hashes) > 1 else b''
496
+
497
+ return pieces_root, pieces_layers, piece_length
498
+
499
+
500
+ def merkle_sha256(file: "FileDescriptorOrPath") -> str:
501
+ """
502
+ The root of the sha256 merkle hash tree with leaf size of 16 KiB.
503
+ """
504
+ pieces_root, _, _ = bittorrent_v2_merkle_sha256(file)
505
+ return pieces_root.hex()
506
+
507
+
508
+ CHECKSUM_ALGO_DICT['merkle_sha256'] = merkle_sha256
509
+
510
+
511
+ def bencode(obj: Union[int, bytes, str, list, dict[bytes, Any]]) -> bytes:
512
+ """
513
+ Copied from the reference implementation of v2 bittorrent:
514
+ http://bittorrent.org/beps/bep_0052_torrent_creator.py
515
+ """
516
+
517
+ if isinstance(obj, int):
518
+ return b"i" + str(obj).encode() + b"e"
519
+ elif isinstance(obj, bytes):
520
+ return str(len(obj)).encode() + b":" + obj
521
+ elif isinstance(obj, str):
522
+ return bencode(obj.encode("utf-8"))
523
+ elif isinstance(obj, list):
524
+ return b"l" + b"".join(map(bencode, obj)) + b"e"
525
+ elif isinstance(obj, dict):
526
+ if all(isinstance(i, bytes) for i in obj.keys()):
527
+ items = list(obj.items())
528
+ items.sort()
529
+ return b"d" + b"".join(map(bencode, itertools.chain(*items))) + b"e"
530
+ else:
531
+ raise ValueError("dict keys should be bytes " + str(obj.keys()))
532
+ raise ValueError("Allowed types: int, bytes, str, list, dict; not %s", type(obj))
533
+
534
+
535
+ def construct_torrent(
536
+ scope: str,
537
+ name: str,
538
+ length: int,
539
+ piece_length: int,
540
+ pieces_root: bytes,
541
+ pieces_layers: "Optional[bytes]" = None,
542
+ trackers: "Optional[list[str]]" = None,
543
+ ) -> "tuple[str, bytes]":
544
+
545
+ torrent_dict = {
546
+ b'creation date': int(time.time()),
547
+ b'info': {
548
+ b'meta version': 2,
549
+ b'private': 1,
550
+ b'name': f'{scope}:{name}'.encode(),
551
+ b'piece length': piece_length,
552
+ b'file tree': {
553
+ name.encode(): {
554
+ b'': {
555
+ b'length': length,
556
+ b'pieces root': pieces_root,
557
+ }
558
+ }
559
+ }
560
+ },
561
+ b'piece layers': {},
562
+ }
563
+ if trackers:
564
+ torrent_dict[b'announce'] = trackers[0].encode()
565
+ if len(trackers) > 1:
566
+ torrent_dict[b'announce-list'] = [t.encode() for t in trackers]
567
+ if pieces_layers:
568
+ torrent_dict[b'piece layers'][pieces_root] = pieces_layers
569
+
570
+ torrent_id = hashlib.sha256(bencode(torrent_dict[b'info'])).hexdigest()[:40]
571
+ torrent = bencode(torrent_dict)
572
+ return torrent_id, torrent
573
+
574
+
575
+ def str_to_date(string: str) -> Optional[datetime.datetime]:
351
576
  """ Converts a RFC-1123 string to the corresponding datetime value.
352
577
 
353
578
  :param string: the RFC-1123 string to convert to datetime value.
@@ -355,7 +580,7 @@ def str_to_date(string):
355
580
  return datetime.datetime.strptime(string, DATE_FORMAT) if string else None
356
581
 
357
582
 
358
- def val_to_space_sep_str(vallist):
583
+ def val_to_space_sep_str(vallist: list[str]) -> str:
359
584
  """ Converts a list of values into a string of space separated values
360
585
 
361
586
  :param vallist: the list of values to to convert into string
@@ -367,10 +592,10 @@ def val_to_space_sep_str(vallist):
367
592
  else:
368
593
  return str(vallist)
369
594
  except:
370
- return str('')
595
+ return ''
371
596
 
372
597
 
373
- def date_to_str(date):
598
+ def date_to_str(date: datetime.datetime) -> Optional[str]:
374
599
  """ Converts a datetime value to the corresponding RFC-1123 string.
375
600
 
376
601
  :param date: the datetime value to convert.
@@ -400,19 +625,18 @@ class APIEncoder(json.JSONEncoder):
400
625
  return json.JSONEncoder.default(self, obj)
401
626
 
402
627
 
403
- def render_json(**data):
404
- """ JSON render function
405
- """
628
+ def render_json(*args, **kwargs) -> str:
629
+ """ Render a list or a dict as a JSON-formatted string. """
630
+ if args and isinstance(args[0], list):
631
+ data = args[0]
632
+ elif isinstance(kwargs, dict):
633
+ data = kwargs
634
+ else:
635
+ raise ValueError("Error while serializing object to JSON-formatted string: supported input types are list or dict.")
406
636
  return json.dumps(data, cls=APIEncoder)
407
637
 
408
638
 
409
- def render_json_list(list_):
410
- """ JSON render function for list
411
- """
412
- return json.dumps(list_, cls=APIEncoder)
413
-
414
-
415
- def datetime_parser(dct):
639
+ def datetime_parser(dct: dict[Any, Any]) -> dict[Any, Any]:
416
640
  """ datetime parser
417
641
  """
418
642
  for k, v in list(dct.items()):
@@ -424,17 +648,17 @@ def datetime_parser(dct):
424
648
  return dct
425
649
 
426
650
 
427
- def parse_response(data):
651
+ def parse_response(data: Union[str, bytes, bytearray]) -> Any:
428
652
  """
429
653
  JSON render function
430
654
  """
431
- if hasattr(data, 'decode'):
655
+ if isinstance(data, (bytes, bytearray)):
432
656
  data = data.decode('utf-8')
433
657
 
434
658
  return json.loads(data, object_hook=datetime_parser)
435
659
 
436
660
 
437
- def execute(cmd) -> tuple[int, str, str]:
661
+ def execute(cmd: str) -> tuple[int, str, str]:
438
662
  """
439
663
  Executes a command in a subprocess. Returns a tuple
440
664
  of (exitcode, out, err), where out is the string output
@@ -456,17 +680,12 @@ def execute(cmd) -> tuple[int, str, str]:
456
680
  return exitcode, out.decode(encoding='utf-8'), err.decode(encoding='utf-8')
457
681
 
458
682
 
459
- def rse_supported_protocol_operations():
460
- """ Returns a list with operations supported by all RSE protocols."""
461
- return ['read', 'write', 'delete', 'third_party_copy_read', 'third_party_copy_write']
462
-
463
-
464
- def rse_supported_protocol_domains():
465
- """ Returns a list with all supoorted RSE protocol domains."""
683
+ def rse_supported_protocol_domains() -> list[str]:
684
+ """ Returns a list with all supported RSE protocol domains."""
466
685
  return ['lan', 'wan']
467
686
 
468
687
 
469
- def grouper(iterable, n, fillvalue=None):
688
+ def grouper(iterable: Iterable[Any], n: int, fillvalue: Optional[object] = None) -> zip_longest:
470
689
  """ Collect data into fixed-length chunks or blocks """
471
690
  # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
472
691
  args = [iter(iterable)] * n
@@ -489,7 +708,7 @@ def chunks(iterable, n):
489
708
  yield chunk
490
709
 
491
710
 
492
- def dict_chunks(dict_, n):
711
+ def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
493
712
  """
494
713
  Iterate over the dictionary in groups of the requested size
495
714
  """
@@ -498,314 +717,394 @@ def dict_chunks(dict_, n):
498
717
  yield {k: dict_[k] for k in itertools.islice(it, n)}
499
718
 
500
719
 
501
- def my_key_generator(namespace, fn, **kw):
720
+ def my_key_generator(namespace: str, fn: Callable, **kw) -> Callable[..., str]:
502
721
  """
503
- Customyzed key generator for dogpile
722
+ Customized key generator for dogpile
504
723
  """
505
724
  fname = fn.__name__
506
725
 
507
- def generate_key(*arg, **kw):
726
+ def generate_key(*arg, **kw) -> str:
508
727
  return namespace + "_" + fname + "_".join(str(s) for s in filter(None, arg))
509
728
 
510
729
  return generate_key
511
730
 
512
731
 
513
- def construct_surl_DQ2(dsn: str, scope: str, filename: str) -> str:
514
- """
515
- Defines relative SURL for new replicas. This method
516
- contains DQ2 convention. To be used for non-deterministic sites.
517
- Method imported from DQ2.
518
-
519
- @return: relative SURL for new replica.
520
- @rtype: str
521
- """
522
- # check how many dots in dsn
523
- fields = dsn.split('.')
524
- nfields = len(fields)
525
-
526
- if nfields == 0:
527
- return '/other/other/%s' % (filename)
528
- elif nfields == 1:
529
- stripped_dsn = __strip_dsn(dsn)
530
- return '/other/%s/%s' % (stripped_dsn, filename)
531
- elif nfields == 2:
532
- project = fields[0]
533
- stripped_dsn = __strip_dsn(dsn)
534
- return '/%s/%s/%s' % (project, stripped_dsn, filename)
535
- elif nfields < 5 or re.match('user*|group*', fields[0]):
536
- project = fields[0]
537
- f2 = fields[1]
538
- f3 = fields[2]
539
- stripped_dsn = __strip_dsn(dsn)
540
- return '/%s/%s/%s/%s/%s' % (project, f2, f3, stripped_dsn, filename)
541
- else:
542
- project = fields[0]
543
- dataset_type = fields[4]
544
- if nfields == 5:
545
- tag = 'other'
546
- else:
547
- tag = __strip_tag(fields[-1])
548
- stripped_dsn = __strip_dsn(dsn)
549
- return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
732
+ NonDeterministicPFNAlgorithmsT = TypeVar('NonDeterministicPFNAlgorithmsT', bound='NonDeterministicPFNAlgorithms')
550
733
 
551
734
 
552
- def construct_surl_T0(dsn: str, scope: str, filename: str) -> str:
735
+ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
553
736
  """
554
- Defines relative SURL for new replicas. This method
555
- contains Tier0 convention. To be used for non-deterministic sites.
556
-
557
- @return: relative SURL for new replica.
558
- @rtype: str
737
+ Handle PFN construction for non-deterministic RSEs, including registration of algorithms
738
+ from policy packages
559
739
  """
560
- fields = dsn.split('.')
561
- nfields = len(fields)
562
- if nfields >= 3:
563
- return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], fields[1], dsn, filename)
564
- elif nfields == 1:
565
- return '/%s/%s/%s/%s/%s' % (fields[0], 'other', 'other', dsn, filename)
566
- elif nfields == 2:
567
- return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], 'other', dsn, filename)
568
- elif nfields == 0:
569
- return '/other/other/other/other/%s' % (filename)
570
740
 
741
+ _algorithm_type = 'non_deterministic_pfn'
571
742
 
572
- def construct_surl_BelleII(dsn: str, scope: str, filename: str) -> str:
573
- """
574
- Defines relative SURL for Belle II specific replicas.
575
- This method contains the Belle II convention.
576
- To be used for non-deterministic Belle II sites.
577
- DSN (or datablock in the Belle II naming) contains /
743
+ def __init__(self) -> None:
744
+ """
745
+ Initialises a non-deterministic PFN construction object
746
+ """
747
+ super().__init__()
578
748
 
579
- """
749
+ def construct_non_deterministic_pfn(self, dsn: str, scope: Optional[str], filename: str, naming_convention: str) -> str:
750
+ """
751
+ Calls the correct algorithm to generate a non-deterministic PFN
752
+ """
753
+ return self.get_algorithm(naming_convention)(dsn, scope, filename)
580
754
 
581
- fields = dsn.split("/")
582
- nfields = len(fields)
583
- if nfields == 0:
584
- return '/other/%s' % (filename)
585
- else:
586
- return '%s/%s' % (dsn, filename)
755
+ @classmethod
756
+ def supports(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> bool:
757
+ """
758
+ Checks whether a non-deterministic PFN algorithm is supported
759
+ """
760
+ return super()._supports(cls._algorithm_type, naming_convention)
587
761
 
762
+ @classmethod
763
+ def _module_init_(cls: type[NonDeterministicPFNAlgorithmsT]) -> None:
764
+ """
765
+ Registers the included non-deterministic PFN algorithms
766
+ """
767
+ cls.register('T0', cls.construct_non_deterministic_pfn_T0)
768
+ cls.register('DQ2', cls.construct_non_deterministic_pfn_DQ2)
769
+ cls.register('BelleII', cls.construct_non_deterministic_pfn_BelleII)
588
770
 
589
- _SURL_ALGORITHMS = {}
590
- _DEFAULT_SURL = 'DQ2'
591
- _loaded_policy_modules = False
771
+ @classmethod
772
+ def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> Callable[[str, Optional[str], str], str]:
773
+ """
774
+ Looks up a non-deterministic PFN algorithm by name
775
+ """
776
+ return super()._get_one_algorithm(cls._algorithm_type, naming_convention)
592
777
 
778
+ @classmethod
779
+ def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: Callable[[str, Optional[str], str], Optional[str]]) -> None:
780
+ """
781
+ Register a new non-deterministic PFN algorithm
782
+ """
783
+ algorithm_dict = {name: fn_construct_non_deterministic_pfn}
784
+ super()._register(cls._algorithm_type, algorithm_dict)
593
785
 
594
- def register_surl_algorithm(surl_callable, name=None):
595
- if name is None:
596
- name = surl_callable.__name__
597
- _SURL_ALGORITHMS[name] = surl_callable
786
+ @staticmethod
787
+ def __strip_dsn(dsn: str) -> str:
788
+ """
789
+ Drop the _sub and _dis suffixes for panda datasets from the lfc path
790
+ they will be registered in.
791
+ Method imported from DQ2.
792
+ """
598
793
 
794
+ suffixes_to_drop = ['_dis', '_sub', '_frag']
795
+ fields = dsn.split('.')
796
+ last_field = fields[-1]
797
+ try:
798
+ for suffix in suffixes_to_drop:
799
+ last_field = re.sub('%s.*$' % suffix, '', last_field)
800
+ except IndexError:
801
+ return dsn
802
+ fields[-1] = last_field
803
+ stripped_dsn = '.'.join(fields)
804
+ return stripped_dsn
805
+
806
+ @staticmethod
807
+ def __strip_tag(tag: str) -> str:
808
+ """
809
+ Drop the _sub and _dis suffixes for panda datasets from the lfc path
810
+ they will be registered in
811
+ Method imported from DQ2.
812
+ """
813
+ suffixes_to_drop = ['_dis', '_sub', '_tid']
814
+ stripped_tag = tag
815
+ try:
816
+ for suffix in suffixes_to_drop:
817
+ stripped_tag = re.sub('%s.*$' % suffix, '', stripped_tag)
818
+ except IndexError:
819
+ return stripped_tag
820
+ return stripped_tag
599
821
 
600
- register_surl_algorithm(construct_surl_T0, 'T0')
601
- register_surl_algorithm(construct_surl_DQ2, 'DQ2')
602
- register_surl_algorithm(construct_surl_BelleII, 'BelleII')
822
+ @staticmethod
823
+ def construct_non_deterministic_pfn_DQ2(dsn: str, scope: Optional[str], filename: str) -> str:
824
+ """
825
+ Defines relative PFN for new replicas. This method
826
+ contains DQ2 convention. To be used for non-deterministic sites.
827
+ Method imported from DQ2.
828
+
829
+ @return: relative PFN for new replica.
830
+ @rtype: str
831
+ """
832
+ # check how many dots in dsn
833
+ fields = dsn.split('.')
834
+ nfields = len(fields)
835
+
836
+ if nfields == 0:
837
+ return '/other/other/%s' % (filename)
838
+ elif nfields == 1:
839
+ stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
840
+ return '/other/%s/%s' % (stripped_dsn, filename)
841
+ elif nfields == 2:
842
+ project = fields[0]
843
+ stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
844
+ return '/%s/%s/%s' % (project, stripped_dsn, filename)
845
+ elif nfields < 5 or re.match('user*|group*', fields[0]):
846
+ project = fields[0]
847
+ f2 = fields[1]
848
+ f3 = fields[2]
849
+ stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
850
+ return '/%s/%s/%s/%s/%s' % (project, f2, f3, stripped_dsn, filename)
851
+ else:
852
+ project = fields[0]
853
+ dataset_type = fields[4]
854
+ if nfields == 5:
855
+ tag = 'other'
856
+ else:
857
+ tag = NonDeterministicPFNAlgorithms.__strip_tag(fields[-1])
858
+ stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
859
+ return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
860
+
861
+ @staticmethod
862
+ def construct_non_deterministic_pfn_T0(dsn: str, scope: Optional[str], filename: str) -> Optional[str]:
863
+ """
864
+ Defines relative PFN for new replicas. This method
865
+ contains Tier0 convention. To be used for non-deterministic sites.
866
+
867
+ @return: relative PFN for new replica.
868
+ @rtype: str
869
+ """
870
+ fields = dsn.split('.')
871
+ nfields = len(fields)
872
+ if nfields >= 3:
873
+ return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], fields[1], dsn, filename)
874
+ elif nfields == 1:
875
+ return '/%s/%s/%s/%s/%s' % (fields[0], 'other', 'other', dsn, filename)
876
+ elif nfields == 2:
877
+ return '/%s/%s/%s/%s/%s' % (fields[0], fields[2], 'other', dsn, filename)
878
+ elif nfields == 0:
879
+ return '/other/other/other/other/%s' % (filename)
880
+
881
+ @staticmethod
882
+ def construct_non_deterministic_pfn_BelleII(dsn: str, scope: Optional[str], filename: str) -> str:
883
+ """
884
+ Defines relative PFN for Belle II specific replicas.
885
+ This method contains the Belle II convention.
886
+ To be used for non-deterministic Belle II sites.
887
+ DSN (or datablock in the Belle II naming) contains /
888
+ """
889
+
890
+ fields = dsn.split("/")
891
+ nfields = len(fields)
892
+ if nfields == 0:
893
+ return '/other/%s' % (filename)
894
+ else:
895
+ return '%s/%s' % (dsn, filename)
603
896
 
604
897
 
605
- def construct_surl(dsn: str, scope: str, filename: str, naming_convention: str = None) -> str:
898
+ _DEFAULT_NON_DETERMINISTIC_PFN = 'DQ2'
899
+ NonDeterministicPFNAlgorithms._module_init_()
900
+
901
+
902
+ def construct_non_deterministic_pfn(dsn: str, scope: Optional[str], filename: str, naming_convention: Optional[str] = None) -> str:
606
903
  """
607
- Applies non-deterministic source url convention to the given replica.
904
+ Applies non-deterministic PFN convention to the given replica.
608
905
  use the naming_convention to call the actual function which will do the job.
609
- Rucio administrators can potentially register additional surl generation algorithms,
906
+ Rucio administrators can potentially register additional PFN generation algorithms,
610
907
  which are not implemented inside this main rucio repository, so changing the
611
908
  argument list must be done with caution.
612
909
  """
613
- global _loaded_policy_modules
614
- if not _loaded_policy_modules:
615
- # on first call, register any SURL functions from the policy packages
616
- register_policy_package_algorithms('surl', _SURL_ALGORITHMS)
617
- _loaded_policy_modules = True
910
+ pfn_algorithms = NonDeterministicPFNAlgorithms()
911
+ if naming_convention is None or not NonDeterministicPFNAlgorithms.supports(naming_convention):
912
+ naming_convention = _DEFAULT_NON_DETERMINISTIC_PFN
913
+ return pfn_algorithms.construct_non_deterministic_pfn(dsn, scope, filename, naming_convention)
618
914
 
619
- if naming_convention is None or naming_convention not in _SURL_ALGORITHMS:
620
- naming_convention = _DEFAULT_SURL
621
- return _SURL_ALGORITHMS[naming_convention](dsn, scope, filename)
622
915
 
916
+ def clean_pfns(pfns: Iterable[str]) -> list[str]:
917
+ res = []
918
+ for pfn in pfns:
919
+ if pfn.startswith('srm'):
920
+ pfn = re.sub(':[0-9]+/', '/', pfn)
921
+ pfn = re.sub(r'/srm/managerv1\?SFN=', '', pfn)
922
+ pfn = re.sub(r'/srm/v2/server\?SFN=', '', pfn)
923
+ pfn = re.sub(r'/srm/managerv2\?SFN=', '', pfn)
924
+ if '?GoogleAccessId' in pfn:
925
+ pfn = pfn.split('?GoogleAccessId')[0]
926
+ if '?X-Amz' in pfn:
927
+ pfn = pfn.split('?X-Amz')[0]
928
+ res.append(pfn)
929
+ res.sort()
930
+ return res
623
931
 
624
- def __strip_dsn(dsn):
625
- """
626
- Drop the _sub and _dis suffixes for panda datasets from the lfc path
627
- they will be registered in.
628
- Method imported from DQ2.
629
- """
630
932
 
631
- suffixes_to_drop = ['_dis', '_sub', '_frag']
632
- fields = dsn.split('.')
633
- last_field = fields[-1]
634
- try:
635
- for suffix in suffixes_to_drop:
636
- last_field = re.sub('%s.*$' % suffix, '', last_field)
637
- except IndexError:
638
- return dsn
639
- fields[-1] = last_field
640
- stripped_dsn = '.'.join(fields)
641
- return stripped_dsn
933
+ ScopeExtractionAlgorithmsT = TypeVar('ScopeExtractionAlgorithmsT', bound='ScopeExtractionAlgorithms')
642
934
 
643
935
 
644
- def __strip_tag(tag):
936
+ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
645
937
  """
646
- Drop the _sub and _dis suffixes for panda datasets from the lfc path
647
- they will be registered in
648
- Method imported from DQ2.
938
+ Handle scope extraction algorithms
649
939
  """
650
- suffixes_to_drop = ['_dis', '_sub', '_tid']
651
- stripped_tag = tag
652
- try:
653
- for suffix in suffixes_to_drop:
654
- stripped_tag = re.sub('%s.*$' % suffix, '', stripped_tag)
655
- except IndexError:
656
- return stripped_tag
657
- return stripped_tag
658
940
 
941
+ _algorithm_type = 'scope'
659
942
 
660
- def clean_surls(surls):
661
- res = []
662
- for surl in surls:
663
- if surl.startswith('srm'):
664
- surl = re.sub(':[0-9]+/', '/', surl)
665
- surl = re.sub(r'/srm/managerv1\?SFN=', '', surl)
666
- surl = re.sub(r'/srm/v2/server\?SFN=', '', surl)
667
- surl = re.sub(r'/srm/managerv2\?SFN=', '', surl)
668
- if '?GoogleAccessId' in surl:
669
- surl = surl.split('?GoogleAccessId')[0]
670
- if '?X-Amz' in surl:
671
- surl = surl.split('?X-Amz')[0]
672
- res.append(surl)
673
- res.sort()
674
- return res
943
+ def __init__(self) -> None:
944
+ """
945
+ Initialises scope extraction algorithms object
946
+ """
947
+ super().__init__()
675
948
 
949
+ def extract_scope(self, did: str, scopes: Optional[Sequence[str]], extract_scope_convention: str) -> Sequence[str]:
950
+ """
951
+ Calls the correct algorithm for scope extraction
952
+ """
953
+ return self.get_algorithm(extract_scope_convention)(did, scopes)
676
954
 
677
- _EXTRACT_SCOPE_ALGORITHMS = {}
678
- _DEFAULT_EXTRACT = 'atlas'
679
- _loaded_policy_package_scope_algorithms = False
680
-
681
-
682
- def extract_scope_atlas(did, scopes):
683
- # Try to extract the scope from the DSN
684
- if did.find(':') > -1:
685
- if len(did.split(':')) > 2:
686
- raise RucioException('Too many colons. Cannot extract scope and name')
687
- scope, name = did.split(':')[0], did.split(':')[1]
688
- if name.endswith('/'):
689
- name = name[:-1]
690
- return scope, name
691
- else:
692
- scope = did.split('.')[0]
693
- if did.startswith('user') or did.startswith('group'):
694
- scope = ".".join(did.split('.')[0:2])
695
- if did.endswith('/'):
696
- did = did[:-1]
697
- return scope, did
955
+ @classmethod
956
+ def supports(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> bool:
957
+ """
958
+ Checks whether the specified scope extraction algorithm is supported
959
+ """
960
+ return super()._supports(cls._algorithm_type, extract_scope_convention)
698
961
 
962
+ @classmethod
963
+ def _module_init_(cls: type[ScopeExtractionAlgorithmsT]) -> None:
964
+ """
965
+ Registers the included scope extraction algorithms
966
+ """
967
+ cls.register('atlas', cls.extract_scope_atlas)
968
+ cls.register('belleii', cls.extract_scope_belleii)
969
+ cls.register('dirac', cls.extract_scope_dirac)
699
970
 
700
- def extract_scope_dirac(did, scopes):
701
- # Default dirac scope extract algorithm. Scope is the second element in the LFN or the first one (VO name)
702
- # if only one element is the result of a split.
703
- elem = did.rstrip('/').split('/')
704
- if len(elem) > 2:
705
- scope = elem[2]
706
- else:
707
- scope = elem[1]
708
- return scope, did
709
-
710
-
711
- def extract_scope_belleii(did, scopes):
712
- split_did = did.split('/')
713
- if did.startswith('/belle/mock/'):
714
- return 'mock', did
715
- if did.startswith('/belle/MC/'):
716
- if did.startswith('/belle/MC/BG') or \
717
- did.startswith('/belle/MC/build') or \
718
- did.startswith('/belle/MC/generic') or \
719
- did.startswith('/belle/MC/log') or \
720
- did.startswith('/belle/MC/mcprod') or \
721
- did.startswith('/belle/MC/prerelease') or \
722
- did.startswith('/belle/MC/release'):
723
- return 'mc', did
724
- if did.startswith('/belle/MC/cert') or \
725
- did.startswith('/belle/MC/dirac') or \
726
- did.startswith('/belle/MC/dr3') or \
727
- did.startswith('/belle/MC/fab') or \
728
- did.startswith('/belle/MC/hideki') or \
729
- did.startswith('/belle/MC/merge') or \
730
- did.startswith('/belle/MC/migration') or \
731
- did.startswith('/belle/MC/skim') or \
732
- did.startswith('/belle/MC/test'):
733
- return 'mc_tmp', did
734
- if len(split_did) > 4:
735
- if split_did[3].find('fab') > -1 or split_did[3].find('merge') > -1 or split_did[3].find('skim') > -1:
736
- return 'mc_tmp', did
737
- if split_did[3].find('release') > -1:
738
- return 'mc', did
739
- return 'mc_tmp', did
740
- if did.startswith('/belle/Raw/'):
741
- return 'raw', did
742
- if did.startswith('/belle/hRaw'):
743
- return 'hraw', did
744
- if did.startswith('/belle/user/'):
745
- if len(split_did) > 4:
746
- if len(split_did[3]) == 1 and 'user.%s' % (split_did[4]) in scopes:
747
- return 'user.%s' % split_did[4], did
748
- if len(split_did) > 3:
749
- if 'user.%s' % (split_did[3]) in scopes:
750
- return 'user.%s' % split_did[3], did
751
- return 'user', did
752
- if did.startswith('/belle/group/'):
753
- if len(split_did) > 4:
754
- if 'group.%s' % (split_did[4]) in scopes:
755
- return 'group.%s' % split_did[4], did
756
- return 'group', did
757
- if did.startswith('/belle/data/') or did.startswith('/belle/Data/'):
758
- if len(split_did) > 4:
759
- if split_did[3] in ['fab', 'skim']: # /belle/Data/fab --> data_tmp
760
- return 'data_tmp', did
761
- if split_did[3].find('release') > -1: # /belle/Data/release --> data
762
- return 'data', did
763
- if len(split_did) > 5:
764
- if split_did[3] in ['proc']: # /belle/Data/proc
765
- if split_did[4].find('release') > -1: # /belle/Data/proc/release*
766
- if len(split_did) > 7 and split_did[6] in ['GCR2c', 'prod00000007', 'prod6b', 'proc7b',
767
- 'proc8b', 'Bucket4', 'Bucket6test', 'bucket6',
768
- 'proc9', 'bucket7', 'SKIMDATAx1', 'proc10Valid',
769
- 'proc10', 'SkimP10x1', 'SkimP11x1', 'SkimB9x1',
770
- 'SkimB10x1', 'SkimB11x1']: # /belle/Data/proc/release*/*/proc10/* --> data_tmp (Old convention)
771
- return 'data_tmp', did
772
- else: # /belle/Data/proc/release*/*/proc11/* --> data (New convention)
773
- return 'data', did
774
- if split_did[4].find('fab') > -1: # /belle/Data/proc/fab* --> data_tmp
775
- return 'data_tmp', did
776
- return 'data_tmp', did
777
- if did.startswith('/belle/ddm/functional_tests/') or did.startswith('/belle/ddm/tests/') or did.startswith('/belle/test/ddm_test'):
778
- return 'test', did
779
- if did.startswith('/belle/BG/'):
780
- return 'data', did
781
- if did.startswith('/belle/collection'):
782
- return 'collection', did
783
- return 'other', did
971
+ @classmethod
972
+ def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> Callable[[str, Optional[Sequence[str]]], Sequence[str]]:
973
+ """
974
+ Looks up a scope extraction algorithm by name
975
+ """
976
+ return super()._get_one_algorithm(cls._algorithm_type, extract_scope_convention)
784
977
 
978
+ @classmethod
979
+ def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: Callable[[str, Optional[Sequence[str]]], Sequence[str]]) -> None:
980
+ """
981
+ Registers a new scope extraction algorithm
982
+ """
983
+ algorithm_dict = {name: fn_extract_scope}
984
+ super()._register(cls._algorithm_type, algorithm_dict)
985
+
986
+ @staticmethod
987
+ def extract_scope_atlas(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
988
+ # Try to extract the scope from the DSN
989
+ if did.find(':') > -1:
990
+ if len(did.split(':')) > 2:
991
+ raise RucioException('Too many colons. Cannot extract scope and name')
992
+ scope, name = did.split(':')[0], did.split(':')[1]
993
+ if name.endswith('/'):
994
+ name = name[:-1]
995
+ return scope, name
996
+ else:
997
+ scope = did.split('.')[0]
998
+ if did.startswith('user') or did.startswith('group'):
999
+ scope = ".".join(did.split('.')[0:2])
1000
+ if did.endswith('/'):
1001
+ did = did[:-1]
1002
+ return scope, did
1003
+
1004
+ @staticmethod
1005
+ def extract_scope_dirac(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
1006
+ # Default dirac scope extract algorithm. Scope is the second element in the LFN or the first one (VO name)
1007
+ # if only one element is the result of a split.
1008
+ elem = did.rstrip('/').split('/')
1009
+ if len(elem) > 2:
1010
+ scope = elem[2]
1011
+ else:
1012
+ scope = elem[1]
1013
+ return scope, did
785
1014
 
786
- def register_extract_scope_algorithm(extract_callable, name=[]):
787
- if name is None:
788
- name = extract_callable.__name__
789
- _EXTRACT_SCOPE_ALGORITHMS[name] = extract_callable
1015
+ @staticmethod
1016
+ def extract_scope_belleii(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
1017
+ split_did = did.split('/')
1018
+ if did.startswith('/belle/mock/'):
1019
+ return 'mock', did
1020
+ if did.startswith('/belle/MC/'):
1021
+ if did.startswith('/belle/MC/BG') or \
1022
+ did.startswith('/belle/MC/build') or \
1023
+ did.startswith('/belle/MC/generic') or \
1024
+ did.startswith('/belle/MC/log') or \
1025
+ did.startswith('/belle/MC/mcprod') or \
1026
+ did.startswith('/belle/MC/prerelease') or \
1027
+ did.startswith('/belle/MC/release'):
1028
+ return 'mc', did
1029
+ if did.startswith('/belle/MC/cert') or \
1030
+ did.startswith('/belle/MC/dirac') or \
1031
+ did.startswith('/belle/MC/dr3') or \
1032
+ did.startswith('/belle/MC/fab') or \
1033
+ did.startswith('/belle/MC/hideki') or \
1034
+ did.startswith('/belle/MC/merge') or \
1035
+ did.startswith('/belle/MC/migration') or \
1036
+ did.startswith('/belle/MC/skim') or \
1037
+ did.startswith('/belle/MC/test'):
1038
+ return 'mc_tmp', did
1039
+ if len(split_did) > 4:
1040
+ if split_did[3].find('fab') > -1 or split_did[3].find('merge') > -1 or split_did[3].find('skim') > -1:
1041
+ return 'mc_tmp', did
1042
+ if split_did[3].find('release') > -1:
1043
+ return 'mc', did
1044
+ return 'mc_tmp', did
1045
+ if did.startswith('/belle/Raw/'):
1046
+ return 'raw', did
1047
+ if did.startswith('/belle/hRaw'):
1048
+ return 'hraw', did
1049
+ if did.startswith('/belle/user/'):
1050
+ if len(split_did) > 4:
1051
+ if len(split_did[3]) == 1 and scopes is not None and 'user.%s' % (split_did[4]) in scopes:
1052
+ return 'user.%s' % split_did[4], did
1053
+ if len(split_did) > 3:
1054
+ if scopes is not None and 'user.%s' % (split_did[3]) in scopes:
1055
+ return 'user.%s' % split_did[3], did
1056
+ return 'user', did
1057
+ if did.startswith('/belle/group/'):
1058
+ if len(split_did) > 4:
1059
+ if scopes is not None and 'group.%s' % (split_did[4]) in scopes:
1060
+ return 'group.%s' % split_did[4], did
1061
+ return 'group', did
1062
+ if did.startswith('/belle/data/') or did.startswith('/belle/Data/'):
1063
+ if len(split_did) > 4:
1064
+ if split_did[3] in ['fab', 'skim']: # /belle/Data/fab --> data_tmp
1065
+ return 'data_tmp', did
1066
+ if split_did[3].find('release') > -1: # /belle/Data/release --> data
1067
+ return 'data', did
1068
+ if len(split_did) > 5:
1069
+ if split_did[3] in ['proc']: # /belle/Data/proc
1070
+ if split_did[4].find('release') > -1: # /belle/Data/proc/release*
1071
+ if len(split_did) > 7 and split_did[6] in ['GCR2c', 'prod00000007', 'prod6b', 'proc7b',
1072
+ 'proc8b', 'Bucket4', 'Bucket6test', 'bucket6',
1073
+ 'proc9', 'bucket7', 'SKIMDATAx1', 'proc10Valid',
1074
+ 'proc10', 'SkimP10x1', 'SkimP11x1', 'SkimB9x1',
1075
+ 'SkimB10x1', 'SkimB11x1']: # /belle/Data/proc/release*/*/proc10/* --> data_tmp (Old convention)
1076
+ return 'data_tmp', did
1077
+ else: # /belle/Data/proc/release*/*/proc11/* --> data (New convention)
1078
+ return 'data', did
1079
+ if split_did[4].find('fab') > -1: # /belle/Data/proc/fab* --> data_tmp
1080
+ return 'data_tmp', did
1081
+ return 'data_tmp', did
1082
+ if did.startswith('/belle/ddm/functional_tests/') or did.startswith('/belle/ddm/tests/') or did.startswith('/belle/test/ddm_test'):
1083
+ return 'test', did
1084
+ if did.startswith('/belle/BG/'):
1085
+ return 'data', did
1086
+ if did.startswith('/belle/collection'):
1087
+ return 'collection', did
1088
+ return 'other', did
790
1089
 
791
1090
 
792
- register_extract_scope_algorithm(extract_scope_atlas, 'atlas')
793
- register_extract_scope_algorithm(extract_scope_belleii, 'belleii')
794
- register_extract_scope_algorithm(extract_scope_dirac, 'dirac')
1091
+ _DEFAULT_EXTRACT = 'atlas'
1092
+ ScopeExtractionAlgorithms._module_init_()
795
1093
 
796
1094
 
797
- def extract_scope(did, scopes=None, default_extract=_DEFAULT_EXTRACT):
798
- global _loaded_policy_package_scope_algorithms
799
- if not _loaded_policy_package_scope_algorithms:
800
- register_policy_package_algorithms('scope', _EXTRACT_SCOPE_ALGORITHMS)
801
- _loaded_policy_package_scope_algorithms = True
1095
+ def extract_scope(
1096
+ did: str,
1097
+ scopes: Optional[Sequence[str]] = None,
1098
+ default_extract: str = _DEFAULT_EXTRACT
1099
+ ) -> Sequence[str]:
1100
+ scope_extraction_algorithms = ScopeExtractionAlgorithms()
802
1101
  extract_scope_convention = config_get('common', 'extract_scope', False, None) or config_get('policy', 'extract_scope', False, None)
803
- if extract_scope_convention is None or extract_scope_convention not in _EXTRACT_SCOPE_ALGORITHMS:
1102
+ if extract_scope_convention is None or not ScopeExtractionAlgorithms.supports(extract_scope_convention):
804
1103
  extract_scope_convention = default_extract
805
- return _EXTRACT_SCOPE_ALGORITHMS[extract_scope_convention](did=did, scopes=scopes)
1104
+ return scope_extraction_algorithms.extract_scope(did, scopes, extract_scope_convention)
806
1105
 
807
1106
 
808
- def pid_exists(pid):
1107
+ def pid_exists(pid: int) -> bool:
809
1108
  """
810
1109
  Check whether pid exists in the current process table.
811
1110
  UNIX only.
@@ -835,7 +1134,7 @@ def pid_exists(pid):
835
1134
  return True
836
1135
 
837
1136
 
838
- def sizefmt(num, human=True):
1137
+ def sizefmt(num: Union[int, float, None], human: bool = True) -> str:
839
1138
  """
840
1139
  Print human readable file sizes
841
1140
  """
@@ -855,7 +1154,7 @@ def sizefmt(num, human=True):
855
1154
  return 'Inf'
856
1155
 
857
1156
 
858
- def get_tmp_dir():
1157
+ def get_tmp_dir() -> str:
859
1158
  """
860
1159
  Get a path where to store temporary files.
861
1160
 
@@ -883,7 +1182,7 @@ def get_tmp_dir():
883
1182
  return base_dir
884
1183
 
885
1184
 
886
- def is_archive(name):
1185
+ def is_archive(name: str) -> bool:
887
1186
  '''
888
1187
  Check if a file name is an archive file or not.
889
1188
 
@@ -908,7 +1207,28 @@ class Color:
908
1207
  END = '\033[0m'
909
1208
 
910
1209
 
911
- def detect_client_location():
1210
+ def resolve_ips(hostname: str) -> list[str]:
1211
+ try:
1212
+ ipaddress.ip_address(hostname)
1213
+ return [hostname]
1214
+ except ValueError:
1215
+ pass
1216
+ try:
1217
+ addrinfo = socket.getaddrinfo(hostname, 0, socket.AF_INET, 0, socket.IPPROTO_TCP)
1218
+ return [ai[4][0] for ai in addrinfo]
1219
+ except socket.gaierror:
1220
+ pass
1221
+ return []
1222
+
1223
+
1224
+ def resolve_ip(hostname: str) -> str:
1225
+ ips = resolve_ips(hostname)
1226
+ if ips:
1227
+ return ips[0]
1228
+ return hostname
1229
+
1230
+
1231
+ def detect_client_location() -> "IPDict":
912
1232
  """
913
1233
  Normally client IP will be set on the server side (request.remote_addr)
914
1234
  Here setting ip on the one seen by the host itself. There is no connection
@@ -923,22 +1243,22 @@ def detect_client_location():
923
1243
  ip = None
924
1244
 
925
1245
  try:
926
- s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
927
- s.connect(("2001:4860:4860:0:0:0:0:8888", 80))
928
- ip = s.getsockname()[0]
1246
+ with socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) as s:
1247
+ s.connect(("2001:4860:4860:0:0:0:0:8888", 80))
1248
+ ip = s.getsockname()[0]
929
1249
  except Exception:
930
1250
  pass
931
1251
 
932
1252
  if not ip:
933
1253
  try:
934
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
935
- s.connect(("8.8.8.8", 80))
936
- ip = s.getsockname()[0]
1254
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
1255
+ s.connect(("8.8.8.8", 80))
1256
+ ip = s.getsockname()[0]
937
1257
  except Exception:
938
1258
  pass
939
1259
 
940
1260
  if not ip:
941
- ip = '0.0.0.0'
1261
+ ip = '0.0.0.0' # noqa: S104
942
1262
 
943
1263
  site = os.environ.get('SITE_NAME',
944
1264
  os.environ.get('ATLAS_SITE_NAME',
@@ -964,7 +1284,7 @@ def detect_client_location():
964
1284
  'longitude': longitude}
965
1285
 
966
1286
 
967
- def ssh_sign(private_key, message):
1287
+ def ssh_sign(private_key: str, message: str) -> str:
968
1288
  """
969
1289
  Sign a string message using the private key.
970
1290
 
@@ -972,21 +1292,20 @@ def ssh_sign(private_key, message):
972
1292
  :param message: The message to sign as a string.
973
1293
  :return: Base64 encoded signature as a string.
974
1294
  """
975
- if isinstance(message, str):
976
- message = message.encode()
1295
+ encoded_message = message.encode()
977
1296
  if not EXTRA_MODULES['paramiko']:
978
1297
  raise MissingModuleException('The paramiko module is not installed or faulty.')
979
1298
  sio_private_key = StringIO(private_key)
980
1299
  priv_k = RSAKey.from_private_key(sio_private_key)
981
1300
  sio_private_key.close()
982
- signature_stream = priv_k.sign_ssh_data(message)
1301
+ signature_stream = priv_k.sign_ssh_data(encoded_message)
983
1302
  signature_stream.rewind()
984
1303
  base64_encoded = base64.b64encode(signature_stream.get_remainder())
985
1304
  base64_encoded = base64_encoded.decode()
986
1305
  return base64_encoded
987
1306
 
988
1307
 
989
- def make_valid_did(lfn_dict):
1308
+ def make_valid_did(lfn_dict: dict[str, Any]) -> dict[str, Any]:
990
1309
  """
991
1310
  When managing information about a LFN (such as in `rucio upload` or
992
1311
  the RSE manager's upload), we add the `filename` attribute to record
@@ -1006,7 +1325,7 @@ def make_valid_did(lfn_dict):
1006
1325
  return lfn_copy
1007
1326
 
1008
1327
 
1009
- def send_trace(trace, trace_endpoint, user_agent, retries=5):
1328
+ def send_trace(trace: TraceDict, trace_endpoint: str, user_agent: str, retries: int = 5) -> int:
1010
1329
  """
1011
1330
  Send the given trace to the trace endpoint
1012
1331
 
@@ -1027,7 +1346,7 @@ def send_trace(trace, trace_endpoint, user_agent, retries=5):
1027
1346
  return 1
1028
1347
 
1029
1348
 
1030
- def add_url_query(url, query):
1349
+ def add_url_query(url: str, query: dict[str, str]) -> str:
1031
1350
  """
1032
1351
  Add a new dictionary to URL parameters
1033
1352
 
@@ -1043,7 +1362,7 @@ def add_url_query(url, query):
1043
1362
  return urlunparse(url_parts)
1044
1363
 
1045
1364
 
1046
- def get_bytes_value_from_string(input_string):
1365
+ def get_bytes_value_from_string(input_string: str) -> Union[bool, int]:
1047
1366
  """
1048
1367
  Get bytes from a string that represents a storage value and unit
1049
1368
 
@@ -1073,7 +1392,7 @@ def get_bytes_value_from_string(input_string):
1073
1392
  return False
1074
1393
 
1075
1394
 
1076
- def parse_did_filter_from_string(input_string):
1395
+ def parse_did_filter_from_string(input_string: str) -> tuple[dict[str, Any], str]:
1077
1396
  """
1078
1397
  Parse DID filter options in format 'length<3,type=all' from string.
1079
1398
 
@@ -1110,13 +1429,13 @@ def parse_did_filter_from_string(input_string):
1110
1429
  value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
1111
1430
 
1112
1431
  if key == 'type':
1113
- if value.upper() in ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']:
1114
- type_ = value.lower()
1432
+ if value.upper() in ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']: # type: ignore
1433
+ type_ = value.lower() # type: ignore
1115
1434
  else:
1116
1435
  raise InvalidType('{0} is not a valid type. Valid types are {1}'.format(value, ['ALL', 'COLLECTION', 'CONTAINER', 'DATASET', 'FILE']))
1117
1436
  elif key in ('length.gt', 'length.lt', 'length.gte', 'length.lte', 'length'):
1118
1437
  try:
1119
- value = int(value)
1438
+ value = int(value) # type: ignore
1120
1439
  filters[key] = value
1121
1440
  except ValueError:
1122
1441
  raise ValueError('Length has to be an integer value.')
@@ -1133,7 +1452,12 @@ def parse_did_filter_from_string(input_string):
1133
1452
  return filters, type_
1134
1453
 
1135
1454
 
1136
- def parse_did_filter_from_string_fe(input_string, name='*', type='collection', omit_name=False):
1455
+ def parse_did_filter_from_string_fe(
1456
+ input_string: str,
1457
+ name: str = '*',
1458
+ type: str = 'collection',
1459
+ omit_name: bool = False
1460
+ ) -> tuple[list[dict[str, Any]], str]:
1137
1461
  """
1138
1462
  Parse DID filter string for the filter engine (fe).
1139
1463
 
@@ -1241,7 +1565,7 @@ def parse_did_filter_from_string_fe(input_string, name='*', type='collection', o
1241
1565
  return filters, type
1242
1566
 
1243
1567
 
1244
- def parse_replicas_from_file(path):
1568
+ def parse_replicas_from_file(path: "FileDescriptorOrPath") -> Any:
1245
1569
  """
1246
1570
  Parses the output of list_replicas from a json or metalink file
1247
1571
  into a dictionary. Metalink parsing is tried first and if it fails
@@ -1253,7 +1577,7 @@ def parse_replicas_from_file(path):
1253
1577
  """
1254
1578
  with open(path) as fp:
1255
1579
  try:
1256
- root = ElementTree.parse(fp).getroot()
1580
+ root = ElementTree.parse(fp).getroot() # noqa: S314
1257
1581
  return parse_replicas_metalink(root)
1258
1582
  except ElementTree.ParseError as xml_err:
1259
1583
  try:
@@ -1262,7 +1586,7 @@ def parse_replicas_from_file(path):
1262
1586
  raise MetalinkJsonParsingError(path, xml_err, json_err)
1263
1587
 
1264
1588
 
1265
- def parse_replicas_from_string(string):
1589
+ def parse_replicas_from_string(string: str) -> Any:
1266
1590
  """
1267
1591
  Parses the output of list_replicas from a json or metalink string
1268
1592
  into a dictionary. Metalink parsing is tried first and if it fails
@@ -1273,7 +1597,7 @@ def parse_replicas_from_string(string):
1273
1597
  :returns: a list with a dictionary for each file
1274
1598
  """
1275
1599
  try:
1276
- root = ElementTree.fromstring(string)
1600
+ root = ElementTree.fromstring(string) # noqa: S314
1277
1601
  return parse_replicas_metalink(root)
1278
1602
  except ElementTree.ParseError as xml_err:
1279
1603
  try:
@@ -1282,7 +1606,7 @@ def parse_replicas_from_string(string):
1282
1606
  raise MetalinkJsonParsingError(string, xml_err, json_err)
1283
1607
 
1284
1608
 
1285
- def parse_replicas_metalink(root):
1609
+ def parse_replicas_metalink(root: ElementTree.Element) -> list[dict[str, Any]]:
1286
1610
  """
1287
1611
  Transforms the metalink tree into a list of dictionaries where
1288
1612
  each dictionary describes a file with its replicas.
@@ -1339,11 +1663,15 @@ def parse_replicas_metalink(root):
1339
1663
  return files
1340
1664
 
1341
1665
 
1342
- def get_thread_with_periodic_running_function(interval, action, graceful_stop):
1666
+ def get_thread_with_periodic_running_function(
1667
+ interval: Union[int, float],
1668
+ action: Callable[..., Any],
1669
+ graceful_stop: threading.Event
1670
+ ) -> threading.Thread:
1343
1671
  """
1344
1672
  Get a thread where a function runs periodically.
1345
1673
 
1346
- :param interval: Interval in seconds when the action fucntion should run.
1674
+ :param interval: Interval in seconds when the action function should run.
1347
1675
  :param action: Function, that should run periodically.
1348
1676
  :param graceful_stop: Threading event used to check for graceful stop.
1349
1677
  """
@@ -1351,12 +1679,12 @@ def get_thread_with_periodic_running_function(interval, action, graceful_stop):
1351
1679
  while not graceful_stop.is_set():
1352
1680
  starttime = time.time()
1353
1681
  action()
1354
- time.sleep(interval - ((time.time() - starttime)))
1682
+ time.sleep(interval - (time.time() - starttime))
1355
1683
  t = threading.Thread(target=start)
1356
1684
  return t
1357
1685
 
1358
1686
 
1359
- def run_cmd_process(cmd, timeout=3600):
1687
+ def run_cmd_process(cmd: str, timeout: int = 3600) -> tuple[int, str]:
1360
1688
  """
1361
1689
  shell command parser with timeout
1362
1690
 
@@ -1397,7 +1725,10 @@ def run_cmd_process(cmd, timeout=3600):
1397
1725
  return returncode, stdout
1398
1726
 
1399
1727
 
1400
- def api_update_return_dict(dictionary, session=None):
1728
+ def gateway_update_return_dict(
1729
+ dictionary: dict[str, Any],
1730
+ session: Optional["Session"] = None
1731
+ ) -> dict[str, Any]:
1401
1732
  """
1402
1733
  Ensure that rse is in a dictionary returned from core
1403
1734
 
@@ -1435,7 +1766,12 @@ def api_update_return_dict(dictionary, session=None):
1435
1766
  return dictionary
1436
1767
 
1437
1768
 
1438
- def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=False):
1769
+ def setup_logger(
1770
+ module_name: Optional[str] = None,
1771
+ logger_name: Optional[str] = None,
1772
+ logger_level: Optional[int] = None,
1773
+ verbose: bool = False
1774
+ ) -> logging.Logger:
1439
1775
  '''
1440
1776
  Factory method to set logger with handlers.
1441
1777
  :param module_name: __name__ of the module that is calling this method
@@ -1444,10 +1780,10 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
1444
1780
  :param verbose: verbose option set in bin/rucio
1445
1781
  '''
1446
1782
  # helper method for cfg check
1447
- def _force_cfg_log_level(cfg_option):
1783
+ def _force_cfg_log_level(cfg_option: str) -> bool:
1448
1784
  cfg_forced_modules = config_get('logging', cfg_option, raise_exception=False, default=None, clean_cached=True,
1449
1785
  check_config_table=False)
1450
- if cfg_forced_modules:
1786
+ if cfg_forced_modules and module_name is not None:
1451
1787
  if re.match(str(cfg_forced_modules), module_name):
1452
1788
  return True
1453
1789
  return False
@@ -1477,11 +1813,11 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
1477
1813
  logger.setLevel(logger_level)
1478
1814
 
1479
1815
  # preferred logger handling
1480
- def add_handler(logger):
1816
+ def add_handler(logger: logging.Logger) -> None:
1481
1817
  hdlr = logging.StreamHandler()
1482
1818
 
1483
- def emit_decorator(fnc):
1484
- def func(*args):
1819
+ def emit_decorator(fnc: Callable[..., Any]) -> Callable[..., Any]:
1820
+ def func(*args) -> Callable[..., Any]:
1485
1821
  if 'RUCIO_LOGGING_FORMAT' not in os.environ:
1486
1822
  levelno = args[0].levelno
1487
1823
  format_str = '%(asctime)s\t%(levelname)s\t%(message)s\033[0m'
@@ -1514,7 +1850,12 @@ def setup_logger(module_name=None, logger_name=None, logger_level=None, verbose=
1514
1850
  return logger
1515
1851
 
1516
1852
 
1517
- def daemon_sleep(start_time, sleep_time, graceful_stop, logger=logging.log):
1853
+ def daemon_sleep(
1854
+ start_time: float,
1855
+ sleep_time: float,
1856
+ graceful_stop: threading.Event,
1857
+ logger: "LoggerFunction" = logging.log
1858
+ ) -> None:
1518
1859
  """Sleeps a daemon the time provided by sleep_time"""
1519
1860
  end_time = time.time()
1520
1861
  time_diff = end_time - start_time
@@ -1523,7 +1864,7 @@ def daemon_sleep(start_time, sleep_time, graceful_stop, logger=logging.log):
1523
1864
  graceful_stop.wait(sleep_time - time_diff)
1524
1865
 
1525
1866
 
1526
- def is_client():
1867
+ def is_client() -> bool:
1527
1868
  """"
1528
1869
  Checks if the function is called from a client or from a server/daemon
1529
1870
 
@@ -1537,7 +1878,7 @@ def is_client():
1537
1878
  client_mode = True
1538
1879
  else:
1539
1880
  client_mode = False
1540
- except RuntimeError:
1881
+ except (RuntimeError, ConfigNotFound):
1541
1882
  # If no configuration file is found the default value should be True
1542
1883
  client_mode = True
1543
1884
  else:
@@ -1552,15 +1893,15 @@ def is_client():
1552
1893
  class retry:
1553
1894
  """Retry callable object with configuragle number of attempts"""
1554
1895
 
1555
- def __init__(self, func, *args, **kwargs):
1896
+ def __init__(self, func: Callable[..., Any], *args, **kwargs):
1556
1897
  '''
1557
1898
  :param func: a method that should be executed with retries
1558
- :param args: parametres of the func
1899
+ :param args: parameters of the func
1559
1900
  :param kwargs: key word arguments of the func
1560
1901
  '''
1561
1902
  self.func, self.args, self.kwargs = func, args, kwargs
1562
1903
 
1563
- def __call__(self, mtries=3, logger=logging.log):
1904
+ def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> Callable[..., Any]:
1564
1905
  '''
1565
1906
  :param mtries: maximum number of attempts to execute the function
1566
1907
  :param logger: preferred logger
@@ -1586,9 +1927,9 @@ class StoreAndDeprecateWarningAction(argparse.Action):
1586
1927
  '''
1587
1928
 
1588
1929
  def __init__(self,
1589
- option_strings,
1590
- new_option_string,
1591
- dest,
1930
+ option_strings: Sequence[str],
1931
+ new_option_string: str,
1932
+ dest: str,
1592
1933
  **kwargs):
1593
1934
  """
1594
1935
  :param option_strings: all possible argument name strings
@@ -1600,10 +1941,11 @@ class StoreAndDeprecateWarningAction(argparse.Action):
1600
1941
  option_strings=option_strings,
1601
1942
  dest=dest,
1602
1943
  **kwargs)
1603
- assert new_option_string in option_strings
1944
+ if new_option_string not in option_strings:
1945
+ raise ValueError("%s not supported as a string option." % new_option_string)
1604
1946
  self.new_option_string = new_option_string
1605
1947
 
1606
- def __call__(self, parser, namespace, values, option_string=None):
1948
+ def __call__(self, parser, namespace, values, option_string: Optional[str] = None):
1607
1949
  if option_string and option_string != self.new_option_string:
1608
1950
  # The logger gets typically initialized after the argument parser
1609
1951
  # to set the verbosity of the logger. Thus using simple print to console.
@@ -1619,12 +1961,12 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
1619
1961
  '''
1620
1962
 
1621
1963
  def __init__(self,
1622
- option_strings,
1623
- new_option_string,
1624
- dest,
1625
- default=False,
1626
- required=False,
1627
- help=None):
1964
+ option_strings: Sequence[str],
1965
+ new_option_string: str,
1966
+ dest: str,
1967
+ default: bool = False,
1968
+ required: bool = False,
1969
+ help: Optional[str] = None):
1628
1970
  """
1629
1971
  :param option_strings: all possible argument name strings
1630
1972
  :param new_option_string: the new option string which replaces the old
@@ -1638,10 +1980,11 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
1638
1980
  default=default,
1639
1981
  required=required,
1640
1982
  help=help)
1641
- assert new_option_string in option_strings
1983
+ if new_option_string not in option_strings:
1984
+ raise ValueError("%s not supported as a string option." % new_option_string)
1642
1985
  self.new_option_string = new_option_string
1643
1986
 
1644
- def __call__(self, parser, namespace, values, option_string=None):
1987
+ def __call__(self, parser, namespace, values, option_string: Optional[str] = None):
1645
1988
  super(StoreTrueAndDeprecateWarningAction, self).__call__(parser, namespace, values, option_string=option_string)
1646
1989
  if option_string and option_string != self.new_option_string:
1647
1990
  # The logger gets typically initialized after the argument parser
@@ -1660,7 +2003,7 @@ class PriorityQueue:
1660
2003
  [1] https://en.wikipedia.org/wiki/Heap_(data_structure)
1661
2004
  """
1662
2005
  class ContainerSlot:
1663
- def __init__(self, position, priority):
2006
+ def __init__(self, position: int, priority: int):
1664
2007
  self.pos = position
1665
2008
  self.prio = priority
1666
2009
 
@@ -1752,78 +2095,9 @@ class PriorityQueue:
1752
2095
  return heap_changed
1753
2096
 
1754
2097
 
1755
- def register_policy_package_algorithms(algorithm_type, dictionary):
1756
- '''
1757
- Loads all the algorithms of a given type from the policy package(s) and registers them
1758
- :param algorithm_type: the type of algorithm to register (e.g. 'surl', 'lfn2pfn')
1759
- :param dictionary: the dictionary to register them in
1760
- :param vo: the name of the relevant VO (None for single VO)
1761
- '''
1762
- def try_importing_policy(algorithm_type, dictionary, vo=None):
1763
- import importlib
1764
- try:
1765
- env_name = 'RUCIO_POLICY_PACKAGE' + ('' if not vo else '_' + vo.upper())
1766
- if env_name in os.environ:
1767
- package = os.environ[env_name]
1768
- else:
1769
- package = config.config_get('policy', 'package' + ('' if not vo else '-' + vo))
1770
- check_policy_package_version(package)
1771
- module = importlib.import_module(package)
1772
- if hasattr(module, 'get_algorithms'):
1773
- all_algorithms = module.get_algorithms()
1774
- if algorithm_type in all_algorithms:
1775
- algorithms = all_algorithms[algorithm_type]
1776
- if not vo:
1777
- dictionary.update(algorithms)
1778
- else:
1779
- # check that the names are correctly prefixed
1780
- for k in algorithms.keys():
1781
- if k.lower().startswith(vo.lower()):
1782
- dictionary[k] = algorithms[k]
1783
- else:
1784
- raise InvalidAlgorithmName(k, vo)
1785
- except (NoOptionError, NoSectionError, ImportError):
1786
- pass
1787
-
1788
- from rucio.common import config
1789
- try:
1790
- multivo = config.config_get_bool('common', 'multi_vo')
1791
- except (NoOptionError, NoSectionError):
1792
- multivo = False
1793
- if not multivo:
1794
- # single policy package
1795
- try_importing_policy(algorithm_type, dictionary)
1796
- else:
1797
- # determine whether on client or server
1798
- client = False
1799
- if 'RUCIO_CLIENT_MODE' not in os.environ:
1800
- if not config.config_has_section('database') and config.config_has_section('client'):
1801
- client = True
1802
- else:
1803
- if os.environ['RUCIO_CLIENT_MODE']:
1804
- client = True
1805
-
1806
- # on client, only register algorithms for selected VO
1807
- if client:
1808
- if 'RUCIO_VO' in os.environ:
1809
- vo = os.environ['RUCIO_VO']
1810
- else:
1811
- try:
1812
- vo = config.config_get('client', 'vo')
1813
- except (NoOptionError, NoSectionError):
1814
- vo = 'def'
1815
- try_importing_policy(algorithm_type, dictionary, vo)
1816
- # on server, list all VOs and register their algorithms
1817
- else:
1818
- from rucio.core.vo import list_vos
1819
- # policy package per VO
1820
- vos = list_vos()
1821
- for vo in vos:
1822
- try_importing_policy(algorithm_type, dictionary, vo['vo'])
1823
-
1824
-
1825
- def check_policy_package_version(package):
2098
+ def check_policy_package_version(package: str) -> None:
1826
2099
  import importlib
2100
+
1827
2101
  from rucio.version import version_string
1828
2102
  '''
1829
2103
  Checks that the Rucio version supported by the policy package is compatible
@@ -1842,13 +2116,13 @@ def check_policy_package_version(package):
1842
2116
  components = 2 if version_string().startswith("1.") else 1
1843
2117
  current_version = ".".join(version_string().split(".")[:components])
1844
2118
  if current_version not in supported_version:
1845
- raise PolicyPackageVersionError(package)
2119
+ raise PolicyPackageVersionError(package, current_version, supported_version)
1846
2120
 
1847
2121
 
1848
2122
  class Availability:
1849
2123
  """
1850
2124
  This util class acts as a translator between the availability stored as
1851
- integer and as boolen values.
2125
+ integer and as boolean values.
1852
2126
 
1853
2127
  `None` represents a missing value. This lets a user update a specific value
1854
2128
  without altering the other ones. If it needs to be evaluated, it will
@@ -1859,7 +2133,12 @@ class Availability:
1859
2133
  write = None
1860
2134
  delete = None
1861
2135
 
1862
- def __init__(self, read=None, write=None, delete=None):
2136
+ def __init__(
2137
+ self,
2138
+ read: Optional[bool] = None,
2139
+ write: Optional[bool] = None,
2140
+ delete: Optional[bool] = None
2141
+ ):
1863
2142
  self.read = read
1864
2143
  self.write = write
1865
2144
  self.delete = delete
@@ -1944,3 +2223,16 @@ def retrying(
1944
2223
  time.sleep(wait_fixed / 1000.0)
1945
2224
  return _wrapper
1946
2225
  return _decorator
2226
+
2227
+
2228
+ def deep_merge_dict(source: dict, destination: dict) -> dict:
2229
+ """Merge two dictionaries together recursively"""
2230
+ for key, value in source.items():
2231
+ if isinstance(value, dict):
2232
+ # get node or create one
2233
+ node = destination.setdefault(key, {})
2234
+ deep_merge_dict(value, node)
2235
+ else:
2236
+ destination[key] = value
2237
+
2238
+ return destination