rucio-clients 35.7.0__py3-none-any.whl → 37.0.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 (85) hide show
  1. rucio/alembicrevision.py +1 -1
  2. rucio/cli/__init__.py +14 -0
  3. rucio/cli/account.py +216 -0
  4. rucio/cli/bin_legacy/__init__.py +13 -0
  5. rucio_clients-35.7.0.data/scripts/rucio → rucio/cli/bin_legacy/rucio.py +769 -486
  6. rucio_clients-35.7.0.data/scripts/rucio-admin → rucio/cli/bin_legacy/rucio_admin.py +476 -423
  7. rucio/cli/command.py +272 -0
  8. rucio/cli/config.py +72 -0
  9. rucio/cli/did.py +191 -0
  10. rucio/cli/download.py +128 -0
  11. rucio/cli/lifetime_exception.py +33 -0
  12. rucio/cli/replica.py +162 -0
  13. rucio/cli/rse.py +293 -0
  14. rucio/cli/rule.py +158 -0
  15. rucio/cli/scope.py +40 -0
  16. rucio/cli/subscription.py +73 -0
  17. rucio/cli/upload.py +60 -0
  18. rucio/cli/utils.py +226 -0
  19. rucio/client/accountclient.py +0 -1
  20. rucio/client/baseclient.py +33 -24
  21. rucio/client/client.py +45 -1
  22. rucio/client/didclient.py +5 -3
  23. rucio/client/downloadclient.py +6 -8
  24. rucio/client/replicaclient.py +0 -2
  25. rucio/client/richclient.py +317 -0
  26. rucio/client/rseclient.py +4 -4
  27. rucio/client/uploadclient.py +26 -12
  28. rucio/common/bittorrent.py +234 -0
  29. rucio/common/cache.py +66 -29
  30. rucio/common/checksum.py +168 -0
  31. rucio/common/client.py +122 -0
  32. rucio/common/config.py +22 -35
  33. rucio/common/constants.py +61 -3
  34. rucio/common/didtype.py +72 -24
  35. rucio/common/exception.py +65 -8
  36. rucio/common/extra.py +5 -10
  37. rucio/common/logging.py +13 -13
  38. rucio/common/pcache.py +8 -7
  39. rucio/common/plugins.py +59 -27
  40. rucio/common/policy.py +12 -3
  41. rucio/common/schema/__init__.py +84 -34
  42. rucio/common/schema/generic.py +0 -17
  43. rucio/common/schema/generic_multi_vo.py +0 -17
  44. rucio/common/test_rucio_server.py +12 -6
  45. rucio/common/types.py +132 -52
  46. rucio/common/utils.py +93 -643
  47. rucio/rse/__init__.py +3 -3
  48. rucio/rse/protocols/bittorrent.py +11 -1
  49. rucio/rse/protocols/cache.py +0 -11
  50. rucio/rse/protocols/dummy.py +0 -11
  51. rucio/rse/protocols/gfal.py +14 -9
  52. rucio/rse/protocols/globus.py +1 -1
  53. rucio/rse/protocols/http_cache.py +1 -1
  54. rucio/rse/protocols/posix.py +2 -2
  55. rucio/rse/protocols/protocol.py +84 -317
  56. rucio/rse/protocols/rclone.py +2 -1
  57. rucio/rse/protocols/rfio.py +10 -1
  58. rucio/rse/protocols/ssh.py +2 -1
  59. rucio/rse/protocols/storm.py +2 -13
  60. rucio/rse/protocols/webdav.py +74 -30
  61. rucio/rse/protocols/xrootd.py +2 -1
  62. rucio/rse/rsemanager.py +170 -53
  63. rucio/rse/translation.py +260 -0
  64. rucio/vcsversion.py +4 -4
  65. rucio/version.py +7 -0
  66. {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rucio.cfg.atlas.client.template +3 -2
  67. {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rucio.cfg.template +3 -19
  68. {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/requirements.client.txt +11 -7
  69. rucio_clients-37.0.0.data/scripts/rucio +133 -0
  70. rucio_clients-37.0.0.data/scripts/rucio-admin +97 -0
  71. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/METADATA +18 -14
  72. rucio_clients-37.0.0.dist-info/RECORD +104 -0
  73. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/licenses/AUTHORS.rst +3 -0
  74. rucio/common/schema/atlas.py +0 -413
  75. rucio/common/schema/belleii.py +0 -408
  76. rucio/common/schema/domatpc.py +0 -401
  77. rucio/common/schema/escape.py +0 -426
  78. rucio/common/schema/icecube.py +0 -406
  79. rucio/rse/protocols/gsiftp.py +0 -92
  80. rucio_clients-35.7.0.dist-info/RECORD +0 -88
  81. {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/etc/rse-accounts.cfg.template +0 -0
  82. {rucio_clients-35.7.0.data → rucio_clients-37.0.0.data}/data/rucio_client/merge_rucio_configs.py +0 -0
  83. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/WHEEL +0 -0
  84. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/licenses/LICENSE +0 -0
  85. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0.dist-info}/top_level.txt +0 -0
rucio/common/utils.py CHANGED
@@ -14,18 +14,13 @@
14
14
 
15
15
  import argparse
16
16
  import base64
17
- import copy
18
17
  import datetime
19
18
  import errno
20
19
  import getpass
21
- import hashlib
22
- import io
23
20
  import ipaddress
24
21
  import itertools
25
22
  import json
26
23
  import logging
27
- import math
28
- import mmap
29
24
  import os
30
25
  import os.path
31
26
  import re
@@ -35,11 +30,9 @@ import subprocess
35
30
  import tempfile
36
31
  import threading
37
32
  import time
38
- import zlib
39
33
  from collections import OrderedDict
40
- from collections.abc import Callable, Iterable, Iterator, Sequence
41
34
  from enum import Enum
42
- from functools import partial, wraps
35
+ from functools import wraps
43
36
  from io import StringIO
44
37
  from itertools import zip_longest
45
38
  from typing import TYPE_CHECKING, Any, Optional, TypeVar, Union
@@ -49,11 +42,11 @@ from xml.etree import ElementTree
49
42
 
50
43
  import requests
51
44
 
52
- from rucio.common.config import config_get, config_has_section
53
- from rucio.common.exception import ConfigNotFound, DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, PolicyPackageVersionError, RucioException
45
+ from rucio.common.config import config_get
46
+ from rucio.common.exception import DIDFilterSyntaxError, DuplicateCriteriaInDIDFilter, InputValidationError, InvalidType, MetalinkJsonParsingError, MissingModuleException, RucioException
54
47
  from rucio.common.extra import import_extras
55
48
  from rucio.common.plugins import PolicyPackageAlgorithms
56
- from rucio.common.types import InternalAccount, InternalScope, TraceDict
49
+ from rucio.common.types import InternalAccount, InternalScope, LFNDict, TraceDict
57
50
 
58
51
  EXTRA_MODULES = import_extras(['paramiko'])
59
52
 
@@ -61,14 +54,17 @@ if EXTRA_MODULES['paramiko']:
61
54
  try:
62
55
  from paramiko import RSAKey
63
56
  except Exception:
64
- EXTRA_MODULES['paramiko'] = False
57
+ EXTRA_MODULES['paramiko'] = None
65
58
 
66
59
  if TYPE_CHECKING:
60
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence
67
61
  T = TypeVar('T')
62
+ HashableKT = TypeVar('HashableKT')
63
+ HashableVT = TypeVar('HashableVT')
68
64
  from _typeshed import FileDescriptorOrPath
69
65
  from sqlalchemy.orm import Session
70
66
 
71
- from rucio.common.types import IPDict, LoggerFunction
67
+ from rucio.common.types import LoggerFunction
72
68
 
73
69
 
74
70
  # HTTP code dictionary. Not complete. Can be extended if needed.
@@ -101,7 +97,7 @@ codes = {
101
97
  DATE_FORMAT = '%a, %d %b %Y %H:%M:%S UTC'
102
98
 
103
99
 
104
- def invert_dict(d: dict[Any, Any]) -> dict[Any, Any]:
100
+ def invert_dict(d: "Mapping[HashableKT, HashableVT]") -> "Mapping[HashableVT, HashableKT]":
105
101
  """
106
102
  Invert the dictionary.
107
103
  CAUTION: this function is not deterministic unless the input dictionary is one-to-one mapping.
@@ -112,26 +108,6 @@ def invert_dict(d: dict[Any, Any]) -> dict[Any, Any]:
112
108
  return {value: key for key, value in d.items()}
113
109
 
114
110
 
115
- def dids_as_dicts(did_list: Iterable[Union[str, dict[str, str]]]) -> list[dict[str, str]]:
116
- """
117
- Converts list of DIDs to list of dictionaries
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"}
120
- """
121
- out = []
122
- for did in did_list:
123
- if isinstance(did, str):
124
- scope, name = did.split(":", 1)
125
- did = dict(scope=scope, name=name)
126
- if isinstance(did, dict):
127
- if not ("name" in did and "scope" in did):
128
- raise ValueError("Scope or name missing in: %s" % (did,))
129
- else:
130
- raise ValueError("Can not convert item %s (%s) to a DID" % (did, type(did)))
131
- out.append(did)
132
- return out
133
-
134
-
135
111
  def build_url(
136
112
  url: str,
137
113
  path: Optional[str] = None,
@@ -148,11 +124,27 @@ def build_url(
148
124
  if path is not None:
149
125
  complete_url += "/" + path
150
126
  if params is not None:
151
- complete_url += "?"
152
- if isinstance(params, str):
153
- complete_url += quote(params)
154
- else:
155
- complete_url += urlencode(params, doseq=doseq)
127
+ complete_url += _encode_params_as_url_query_string(params, doseq)
128
+ return complete_url
129
+
130
+
131
+ def _encode_params_as_url_query_string(
132
+ params: Union[str, dict[Any, Any], list[tuple[Any, Any]]],
133
+ doseq: bool
134
+ ) -> str:
135
+ """
136
+ Encode params into a URL query string.
137
+
138
+ :param params: the parameters to encode
139
+ :param doseq: if True, individual key=value pairs separated by '&' are generated for each element of the value sequence for the key
140
+
141
+ :returns: params as a URL query string
142
+ """
143
+ complete_url = "?"
144
+ if isinstance(params, str):
145
+ complete_url += quote(params)
146
+ else:
147
+ complete_url += urlencode(params, doseq=doseq)
156
148
  return complete_url
157
149
 
158
150
 
@@ -229,349 +221,6 @@ def generate_uuid_bytes() -> bytes:
229
221
  return uuid().bytes
230
222
 
231
223
 
232
- # GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5', 'sha256', 'crc32']
233
- GLOBALLY_SUPPORTED_CHECKSUMS = ['adler32', 'md5']
234
- CHECKSUM_ALGO_DICT = {}
235
- PREFERRED_CHECKSUM = GLOBALLY_SUPPORTED_CHECKSUMS[0]
236
- CHECKSUM_KEY = 'supported_checksums'
237
-
238
-
239
- def is_checksum_valid(checksum_name: str) -> bool:
240
- """
241
- A simple function to check whether a checksum algorithm is supported.
242
- Relies on GLOBALLY_SUPPORTED_CHECKSUMS to allow for expandability.
243
-
244
- :param checksum_name: The name of the checksum to be verified.
245
- :returns: True if checksum_name is in GLOBALLY_SUPPORTED_CHECKSUMS list, False otherwise.
246
- """
247
-
248
- return checksum_name in GLOBALLY_SUPPORTED_CHECKSUMS
249
-
250
-
251
- def set_preferred_checksum(checksum_name: str) -> None:
252
- """
253
- If the input checksum name is valid,
254
- set it as PREFERRED_CHECKSUM.
255
-
256
- :param checksum_name: The name of the checksum to be verified.
257
- """
258
- if is_checksum_valid(checksum_name):
259
- global PREFERRED_CHECKSUM
260
- PREFERRED_CHECKSUM = checksum_name
261
-
262
-
263
- def adler32(file: "FileDescriptorOrPath") -> str:
264
- """
265
- An Adler-32 checksum is obtained by calculating two 16-bit checksums A and B
266
- and concatenating their bits into a 32-bit integer. A is the sum of all bytes in the
267
- stream plus one, and B is the sum of the individual values of A from each step.
268
-
269
- :param file: file name
270
- :returns: Hexified string, padded to 8 values.
271
- """
272
-
273
- # adler starting value is _not_ 0
274
- adler = 1
275
-
276
- can_mmap = False
277
- # try:
278
- # with open(file, 'r+b') as f:
279
- # can_mmap = True
280
- # except:
281
- # pass
282
-
283
- try:
284
- # use mmap if possible
285
- if can_mmap:
286
- with open(file, 'r+b') as f:
287
- m = mmap.mmap(f.fileno(), 0)
288
- # partial block reads at slightly increased buffer sizes
289
- for block in iter(partial(m.read, io.DEFAULT_BUFFER_SIZE * 8), b''):
290
- adler = zlib.adler32(block, adler)
291
- else:
292
- with open(file, 'rb') as f:
293
- # partial block reads at slightly increased buffer sizes
294
- for block in iter(partial(f.read, io.DEFAULT_BUFFER_SIZE * 8), b''):
295
- adler = zlib.adler32(block, adler)
296
-
297
- except Exception as e:
298
- raise Exception('FATAL - could not get Adler-32 checksum of file %s: %s' % (file, e))
299
-
300
- # backflip on 32bit -- can be removed once everything is fully migrated to 64bit
301
- if adler < 0:
302
- adler = adler + 2 ** 32
303
-
304
- return str('%08x' % adler)
305
-
306
-
307
- CHECKSUM_ALGO_DICT['adler32'] = adler32
308
-
309
-
310
- def md5(file: "FileDescriptorOrPath") -> str:
311
- """
312
- Runs the MD5 algorithm (RFC-1321) on the binary content of the file named file and returns the hexadecimal digest
313
-
314
- :param file: file name
315
- :returns: string of 32 hexadecimal digits
316
- """
317
- hash_md5 = hashlib.md5()
318
- try:
319
- with open(file, "rb") as f:
320
- list(map(hash_md5.update, iter(lambda: f.read(4096), b"")))
321
- except Exception as e:
322
- raise Exception('FATAL - could not get MD5 checksum of file %s - %s' % (file, e))
323
-
324
- return hash_md5.hexdigest()
325
-
326
-
327
- CHECKSUM_ALGO_DICT['md5'] = md5
328
-
329
-
330
- def sha256(file: "FileDescriptorOrPath") -> str:
331
- """
332
- Runs the SHA256 algorithm on the binary content of the file named file and returns the hexadecimal digest
333
-
334
- :param file: file name
335
- :returns: string of 32 hexadecimal digits
336
- """
337
- with open(file, "rb") as f:
338
- bytes_ = f.read() # read entire file as bytes
339
- readable_hash = hashlib.sha256(bytes_).hexdigest()
340
- print(readable_hash)
341
- return readable_hash
342
-
343
-
344
- CHECKSUM_ALGO_DICT['sha256'] = sha256
345
-
346
-
347
- def crc32(file: "FileDescriptorOrPath") -> str:
348
- """
349
- Runs the CRC32 algorithm on the binary content of the file named file and returns the hexadecimal digest
350
-
351
- :param file: file name
352
- :returns: string of 32 hexadecimal digits
353
- """
354
- prev = 0
355
- for eachLine in open(file, "rb"):
356
- prev = zlib.crc32(eachLine, prev)
357
- return "%X" % (prev & 0xFFFFFFFF)
358
-
359
-
360
- CHECKSUM_ALGO_DICT['crc32'] = crc32
361
-
362
-
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
224
  def str_to_date(string: str) -> Optional[datetime.datetime]:
576
225
  """ Converts a RFC-1123 string to the corresponding datetime value.
577
226
 
@@ -583,7 +232,7 @@ def str_to_date(string: str) -> Optional[datetime.datetime]:
583
232
  def val_to_space_sep_str(vallist: list[str]) -> str:
584
233
  """ Converts a list of values into a string of space separated values
585
234
 
586
- :param vallist: the list of values to to convert into string
235
+ :param vallist: the list of values to convert into string
587
236
  :return: the string of space separated values or the value initially passed as parameter
588
237
  """
589
238
  try:
@@ -591,7 +240,7 @@ def val_to_space_sep_str(vallist: list[str]) -> str:
591
240
  return str(" ".join(vallist))
592
241
  else:
593
242
  return str(vallist)
594
- except:
243
+ except Exception:
595
244
  return ''
596
245
 
597
246
 
@@ -685,7 +334,7 @@ def rse_supported_protocol_domains() -> list[str]:
685
334
  return ['lan', 'wan']
686
335
 
687
336
 
688
- def grouper(iterable: Iterable[Any], n: int, fillvalue: Optional[object] = None) -> zip_longest:
337
+ def grouper(iterable: 'Iterable[Any]', n: int, fillvalue: Optional[object] = None) -> zip_longest:
689
338
  """ Collect data into fixed-length chunks or blocks """
690
339
  # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx
691
340
  args = [iter(iterable)] * n
@@ -708,7 +357,7 @@ def chunks(iterable, n):
708
357
  yield chunk
709
358
 
710
359
 
711
- def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
360
+ def dict_chunks(dict_: dict[Any, Any], n: int) -> 'Iterator[dict[Any, Any]]':
712
361
  """
713
362
  Iterate over the dictionary in groups of the requested size
714
363
  """
@@ -717,7 +366,7 @@ def dict_chunks(dict_: dict[Any, Any], n: int) -> Iterator[dict[Any, Any]]:
717
366
  yield {k: dict_[k] for k in itertools.islice(it, n)}
718
367
 
719
368
 
720
- def my_key_generator(namespace: str, fn: Callable, **kw) -> Callable[..., str]:
369
+ def my_key_generator(namespace: str, fn: 'Callable', **kw) -> 'Callable[..., str]':
721
370
  """
722
371
  Customized key generator for dogpile
723
372
  """
@@ -764,19 +413,17 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
764
413
  """
765
414
  Registers the included non-deterministic PFN algorithms
766
415
  """
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)
416
+ cls.register('def', cls.construct_non_deterministic_pfn_default)
770
417
 
771
418
  @classmethod
772
- def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> Callable[[str, Optional[str], str], str]:
419
+ def get_algorithm(cls: type[NonDeterministicPFNAlgorithmsT], naming_convention: str) -> 'Callable[[str, Optional[str], str], str]':
773
420
  """
774
421
  Looks up a non-deterministic PFN algorithm by name
775
422
  """
776
423
  return super()._get_one_algorithm(cls._algorithm_type, naming_convention)
777
424
 
778
425
  @classmethod
779
- def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: Callable[[str, Optional[str], str], Optional[str]]) -> None:
426
+ def register(cls: type[NonDeterministicPFNAlgorithmsT], name: str, fn_construct_non_deterministic_pfn: 'Callable[[str, Optional[str], str], Optional[str]]') -> None:
780
427
  """
781
428
  Register a new non-deterministic PFN algorithm
782
429
  """
@@ -820,7 +467,7 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
820
467
  return stripped_tag
821
468
 
822
469
  @staticmethod
823
- def construct_non_deterministic_pfn_DQ2(dsn: str, scope: Optional[str], filename: str) -> str:
470
+ def construct_non_deterministic_pfn_default(dsn: str, scope: Optional[str], filename: str) -> str:
824
471
  """
825
472
  Defines relative PFN for new replicas. This method
826
473
  contains DQ2 convention. To be used for non-deterministic sites.
@@ -858,44 +505,7 @@ class NonDeterministicPFNAlgorithms(PolicyPackageAlgorithms):
858
505
  stripped_dsn = NonDeterministicPFNAlgorithms.__strip_dsn(dsn)
859
506
  return '/%s/%s/%s/%s/%s' % (project, dataset_type, tag, stripped_dsn, filename)
860
507
 
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
508
 
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)
896
-
897
-
898
- _DEFAULT_NON_DETERMINISTIC_PFN = 'DQ2'
899
509
  NonDeterministicPFNAlgorithms._module_init_()
900
510
 
901
511
 
@@ -909,11 +519,11 @@ def construct_non_deterministic_pfn(dsn: str, scope: Optional[str], filename: st
909
519
  """
910
520
  pfn_algorithms = NonDeterministicPFNAlgorithms()
911
521
  if naming_convention is None or not NonDeterministicPFNAlgorithms.supports(naming_convention):
912
- naming_convention = _DEFAULT_NON_DETERMINISTIC_PFN
522
+ naming_convention = 'def'
913
523
  return pfn_algorithms.construct_non_deterministic_pfn(dsn, scope, filename, naming_convention)
914
524
 
915
525
 
916
- def clean_pfns(pfns: Iterable[str]) -> list[str]:
526
+ def clean_pfns(pfns: 'Iterable[str]') -> list[str]:
917
527
  res = []
918
528
  for pfn in pfns:
919
529
  if pfn.startswith('srm'):
@@ -946,7 +556,7 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
946
556
  """
947
557
  super().__init__()
948
558
 
949
- def extract_scope(self, did: str, scopes: Optional[Sequence[str]], extract_scope_convention: str) -> Sequence[str]:
559
+ def extract_scope(self, did: str, scopes: Optional['Sequence[str]'], extract_scope_convention: str) -> 'Sequence[str]':
950
560
  """
951
561
  Calls the correct algorithm for scope extraction
952
562
  """
@@ -964,19 +574,18 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
964
574
  """
965
575
  Registers the included scope extraction algorithms
966
576
  """
967
- cls.register('atlas', cls.extract_scope_atlas)
968
- cls.register('belleii', cls.extract_scope_belleii)
577
+ cls.register('def', cls.extract_scope_default)
969
578
  cls.register('dirac', cls.extract_scope_dirac)
970
579
 
971
580
  @classmethod
972
- def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> Callable[[str, Optional[Sequence[str]]], Sequence[str]]:
581
+ def get_algorithm(cls: type[ScopeExtractionAlgorithmsT], extract_scope_convention: str) -> 'Callable[[str, Optional[Sequence[str]]], Sequence[str]]':
973
582
  """
974
583
  Looks up a scope extraction algorithm by name
975
584
  """
976
585
  return super()._get_one_algorithm(cls._algorithm_type, extract_scope_convention)
977
586
 
978
587
  @classmethod
979
- def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: Callable[[str, Optional[Sequence[str]]], Sequence[str]]) -> None:
588
+ def register(cls: type[ScopeExtractionAlgorithmsT], name: str, fn_extract_scope: 'Callable[[str, Optional[Sequence[str]]], Sequence[str]]') -> None:
980
589
  """
981
590
  Registers a new scope extraction algorithm
982
591
  """
@@ -984,8 +593,14 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
984
593
  super()._register(cls._algorithm_type, algorithm_dict)
985
594
 
986
595
  @staticmethod
987
- def extract_scope_atlas(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
988
- # Try to extract the scope from the DSN
596
+ def extract_scope_default(did: str, scopes: Optional['Sequence[str]']) -> 'Sequence[str]':
597
+ """
598
+ Default fallback scope extraction algorithm, based on the ATLAS scope extraction algorithm.
599
+
600
+ :param did: The DID to extract the scope from.
601
+
602
+ :returns: A tuple containing the extracted scope and the DID.
603
+ """
989
604
  if did.find(':') > -1:
990
605
  if len(did.split(':')) > 2:
991
606
  raise RucioException('Too many colons. Cannot extract scope and name')
@@ -1002,7 +617,7 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
1002
617
  return scope, did
1003
618
 
1004
619
  @staticmethod
1005
- def extract_scope_dirac(did: str, scopes: Optional[Sequence[str]]) -> Sequence[str]:
620
+ def extract_scope_dirac(did: str, scopes: Optional['Sequence[str]']) -> 'Sequence[str]':
1006
621
  # Default dirac scope extract algorithm. Scope is the second element in the LFN or the first one (VO name)
1007
622
  # if only one element is the result of a split.
1008
623
  elem = did.rstrip('/').split('/')
@@ -1012,91 +627,15 @@ class ScopeExtractionAlgorithms(PolicyPackageAlgorithms):
1012
627
  scope = elem[1]
1013
628
  return scope, did
1014
629
 
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
1089
-
1090
-
1091
- _DEFAULT_EXTRACT = 'atlas'
630
+
1092
631
  ScopeExtractionAlgorithms._module_init_()
1093
632
 
1094
633
 
1095
634
  def extract_scope(
1096
635
  did: str,
1097
- scopes: Optional[Sequence[str]] = None,
1098
- default_extract: str = _DEFAULT_EXTRACT
1099
- ) -> Sequence[str]:
636
+ scopes: Optional['Sequence[str]'] = None,
637
+ default_extract: str = 'def'
638
+ ) -> 'Sequence[str]':
1100
639
  scope_extraction_algorithms = ScopeExtractionAlgorithms()
1101
640
  extract_scope_convention = config_get('common', 'extract_scope', False, None) or config_get('policy', 'extract_scope', False, None)
1102
641
  if extract_scope_convention is None or not ScopeExtractionAlgorithms.supports(extract_scope_convention):
@@ -1228,62 +767,6 @@ def resolve_ip(hostname: str) -> str:
1228
767
  return hostname
1229
768
 
1230
769
 
1231
- def detect_client_location() -> "IPDict":
1232
- """
1233
- Normally client IP will be set on the server side (request.remote_addr)
1234
- Here setting ip on the one seen by the host itself. There is no connection
1235
- to Google DNS servers.
1236
- Try to determine the sitename automatically from common environment variables,
1237
- in this order: SITE_NAME, ATLAS_SITE_NAME, OSG_SITE_NAME. If none of these exist
1238
- use the fixed string 'ROAMING'.
1239
-
1240
- If environment variables sets location, it uses it.
1241
- """
1242
-
1243
- ip = None
1244
-
1245
- try:
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]
1249
- except Exception:
1250
- pass
1251
-
1252
- if not ip:
1253
- try:
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]
1257
- except Exception:
1258
- pass
1259
-
1260
- if not ip:
1261
- ip = '0.0.0.0' # noqa: S104
1262
-
1263
- site = os.environ.get('SITE_NAME',
1264
- os.environ.get('ATLAS_SITE_NAME',
1265
- os.environ.get('OSG_SITE_NAME',
1266
- 'ROAMING')))
1267
-
1268
- latitude = os.environ.get('RUCIO_LATITUDE')
1269
- longitude = os.environ.get('RUCIO_LONGITUDE')
1270
- if latitude and longitude:
1271
- try:
1272
- latitude = float(latitude)
1273
- longitude = float(longitude)
1274
- except ValueError:
1275
- latitude = longitude = 0
1276
- print('Client set latitude and longitude are not valid.')
1277
- else:
1278
- latitude = longitude = None
1279
-
1280
- return {'ip': ip,
1281
- 'fqdn': socket.getfqdn(),
1282
- 'site': site,
1283
- 'latitude': latitude,
1284
- 'longitude': longitude}
1285
-
1286
-
1287
770
  def ssh_sign(private_key: str, message: str) -> str:
1288
771
  """
1289
772
  Sign a string message using the private key.
@@ -1305,7 +788,7 @@ def ssh_sign(private_key: str, message: str) -> str:
1305
788
  return base64_encoded
1306
789
 
1307
790
 
1308
- def make_valid_did(lfn_dict: dict[str, Any]) -> dict[str, Any]:
791
+ def make_valid_did(lfn_dict: LFNDict) -> LFNDict:
1309
792
  """
1310
793
  When managing information about a LFN (such as in `rucio upload` or
1311
794
  the RSE manager's upload), we add the `filename` attribute to record
@@ -1322,7 +805,7 @@ def make_valid_did(lfn_dict: dict[str, Any]) -> dict[str, Any]:
1322
805
  lfn_copy = dict(lfn_dict)
1323
806
  lfn_copy['name'] = lfn_copy.get('name', lfn_copy['filename'])
1324
807
  del lfn_copy['filename']
1325
- return lfn_copy
808
+ return lfn_copy # type: ignore
1326
809
 
1327
810
 
1328
811
  def send_trace(trace: TraceDict, trace_endpoint: str, user_agent: str, retries: int = 5) -> int:
@@ -1479,7 +962,7 @@ def parse_did_filter_from_string_fe(
1479
962
  """
1480
963
  # lookup table unifying all comprehended operators to a nominal suffix.
1481
964
  # note that the order matters as the regex engine is eager, e.g. don't want to evaluate '<=' as '<' and '='.
1482
- operators_suffix_LUT = OrderedDict({
965
+ operators_suffix_lut = OrderedDict({
1483
966
  '<=': 'lte',
1484
967
  '>=': 'gte',
1485
968
  '==': '',
@@ -1490,11 +973,11 @@ def parse_did_filter_from_string_fe(
1490
973
  })
1491
974
 
1492
975
  # lookup table mapping operator opposites, used to reverse compound inequalities.
1493
- operator_opposites_LUT = {
976
+ operator_opposites_lut = {
1494
977
  'lt': 'gt',
1495
978
  'lte': 'gte'
1496
979
  }
1497
- operator_opposites_LUT.update({op2: op1 for op1, op2 in operator_opposites_LUT.items()})
980
+ operator_opposites_lut.update({op2: op1 for op1, op2 in operator_opposites_lut.items()})
1498
981
 
1499
982
  filters = []
1500
983
  if input_string:
@@ -1506,13 +989,13 @@ def parse_did_filter_from_string_fe(
1506
989
  for and_group in and_groups:
1507
990
  and_group = and_group.strip()
1508
991
  # tokenise this AND clause using operators as delimiters.
1509
- tokenisation_regex = "({})".format('|'.join(operators_suffix_LUT.keys()))
992
+ tokenisation_regex = "({})".format('|'.join(operators_suffix_lut.keys()))
1510
993
  and_group_split_by_operator = list(filter(None, re.split(tokenisation_regex, and_group)))
1511
994
  if len(and_group_split_by_operator) == 3: # this is a one-sided inequality or expression
1512
995
  key, operator, value = [token.strip() for token in and_group_split_by_operator]
1513
996
 
1514
997
  # substitute input operator with the nominal operator defined by the LUT, <operators_suffix_LUT>.
1515
- operator_mapped = operators_suffix_LUT.get(operator)
998
+ operator_mapped = operators_suffix_lut.get(operator)
1516
999
 
1517
1000
  filter_key_full = key
1518
1001
  if operator_mapped is not None:
@@ -1529,8 +1012,8 @@ def parse_did_filter_from_string_fe(
1529
1012
  value1, operator1, key, operator2, value2 = [token.strip() for token in and_group_split_by_operator]
1530
1013
 
1531
1014
  # substitute input operator with the nominal operator defined by the LUT, <operators_suffix_LUT>.
1532
- operator1_mapped = operator_opposites_LUT.get(operators_suffix_LUT.get(operator1))
1533
- operator2_mapped = operators_suffix_LUT.get(operator2)
1015
+ operator1_mapped = operator_opposites_lut.get(operators_suffix_lut.get(operator1))
1016
+ operator2_mapped = operators_suffix_lut.get(operator2)
1534
1017
 
1535
1018
  filter_key1_full = filter_key2_full = key
1536
1019
  if operator1_mapped is not None and operator2_mapped is not None:
@@ -1665,7 +1148,7 @@ def parse_replicas_metalink(root: ElementTree.Element) -> list[dict[str, Any]]:
1665
1148
 
1666
1149
  def get_thread_with_periodic_running_function(
1667
1150
  interval: Union[int, float],
1668
- action: Callable[..., Any],
1151
+ action: 'Callable[..., Any]',
1669
1152
  graceful_stop: threading.Event
1670
1153
  ) -> threading.Thread:
1671
1154
  """
@@ -1816,8 +1299,8 @@ def setup_logger(
1816
1299
  def add_handler(logger: logging.Logger) -> None:
1817
1300
  hdlr = logging.StreamHandler()
1818
1301
 
1819
- def emit_decorator(fnc: Callable[..., Any]) -> Callable[..., Any]:
1820
- def func(*args) -> Callable[..., Any]:
1302
+ def emit_decorator(fnc: 'Callable[..., Any]') -> 'Callable[..., Any]':
1303
+ def func(*args) -> 'Callable[..., Any]':
1821
1304
  if 'RUCIO_LOGGING_FORMAT' not in os.environ:
1822
1305
  levelno = args[0].levelno
1823
1306
  format_str = '%(asctime)s\t%(levelname)s\t%(message)s\033[0m'
@@ -1864,36 +1347,10 @@ def daemon_sleep(
1864
1347
  graceful_stop.wait(sleep_time - time_diff)
1865
1348
 
1866
1349
 
1867
- def is_client() -> bool:
1868
- """"
1869
- Checks if the function is called from a client or from a server/daemon
1870
-
1871
- :returns client_mode: True if is called from a client, False if it is called from a server/daemon
1872
- """
1873
- if 'RUCIO_CLIENT_MODE' not in os.environ:
1874
- try:
1875
- if config_has_section('database'):
1876
- client_mode = False
1877
- elif config_has_section('client'):
1878
- client_mode = True
1879
- else:
1880
- client_mode = False
1881
- except (RuntimeError, ConfigNotFound):
1882
- # If no configuration file is found the default value should be True
1883
- client_mode = True
1884
- else:
1885
- if os.environ['RUCIO_CLIENT_MODE']:
1886
- client_mode = True
1887
- else:
1888
- client_mode = False
1889
-
1890
- return client_mode
1891
-
1892
-
1893
- class retry:
1350
+ class retry: # noqa: N801
1894
1351
  """Retry callable object with configuragle number of attempts"""
1895
1352
 
1896
- def __init__(self, func: Callable[..., Any], *args, **kwargs):
1353
+ def __init__(self, func: 'Callable[..., Any]', *args, **kwargs):
1897
1354
  '''
1898
1355
  :param func: a method that should be executed with retries
1899
1356
  :param args: parameters of the func
@@ -1901,7 +1358,7 @@ class retry:
1901
1358
  '''
1902
1359
  self.func, self.args, self.kwargs = func, args, kwargs
1903
1360
 
1904
- def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> Callable[..., Any]:
1361
+ def __call__(self, mtries: int = 3, logger: "LoggerFunction" = logging.log) -> 'Callable[..., Any]':
1905
1362
  '''
1906
1363
  :param mtries: maximum number of attempts to execute the function
1907
1364
  :param logger: preferred logger
@@ -1927,7 +1384,7 @@ class StoreAndDeprecateWarningAction(argparse.Action):
1927
1384
  '''
1928
1385
 
1929
1386
  def __init__(self,
1930
- option_strings: Sequence[str],
1387
+ option_strings: 'Sequence[str]',
1931
1388
  new_option_string: str,
1932
1389
  dest: str,
1933
1390
  **kwargs):
@@ -1961,7 +1418,7 @@ class StoreTrueAndDeprecateWarningAction(argparse._StoreConstAction):
1961
1418
  '''
1962
1419
 
1963
1420
  def __init__(self,
1964
- option_strings: Sequence[str],
1421
+ option_strings: 'Sequence[str]',
1965
1422
  new_option_string: str,
1966
1423
  dest: str,
1967
1424
  default: bool = False,
@@ -2095,30 +1552,6 @@ class PriorityQueue:
2095
1552
  return heap_changed
2096
1553
 
2097
1554
 
2098
- def check_policy_package_version(package: str) -> None:
2099
- import importlib
2100
-
2101
- from rucio.version import version_string
2102
- '''
2103
- Checks that the Rucio version supported by the policy package is compatible
2104
- with this version. Raises an exception if not.
2105
- :param package: the fully qualified name of the policy package
2106
- '''
2107
- try:
2108
- module = importlib.import_module(package)
2109
- except ImportError:
2110
- # package not found. Will be picked up elsewhere
2111
- return
2112
- if not hasattr(module, 'SUPPORTED_VERSION'):
2113
- # package is not versioned
2114
- return
2115
- supported_version = module.SUPPORTED_VERSION if isinstance(module.SUPPORTED_VERSION, list) else [module.SUPPORTED_VERSION]
2116
- components = 2 if version_string().startswith("1.") else 1
2117
- current_version = ".".join(version_string().split(".")[:components])
2118
- if current_version not in supported_version:
2119
- raise PolicyPackageVersionError(package)
2120
-
2121
-
2122
1555
  class Availability:
2123
1556
  """
2124
1557
  This util class acts as a translator between the availability stored as
@@ -2236,3 +1669,20 @@ def deep_merge_dict(source: dict, destination: dict) -> dict:
2236
1669
  destination[key] = value
2237
1670
 
2238
1671
  return destination
1672
+
1673
+
1674
+ def is_method_overridden(obj, base_cls, method_name):
1675
+ """
1676
+ Return True if `obj` (an instance of a subclass of `base_cls`) has overridden the given method_name from base_cls.
1677
+ That is, `type(obj).<method_name>` is not the same function object as `base_cls.<method_name>`.
1678
+
1679
+ :param obj: An instance of (a subclass of) base_cls.
1680
+ :param base_cls: The base class which may define the method.
1681
+ :param method_name: Name of the method (str) to check.
1682
+ :returns: Boolean, True if the subclass provides a real override.
1683
+ """
1684
+ if not hasattr(obj, method_name):
1685
+ return False
1686
+ if getattr(type(obj), method_name, None) is getattr(base_cls, method_name, None): # Caring for bound/unbound cases
1687
+ return False
1688
+ return True