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
@@ -12,12 +12,13 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import logging
15
16
  import os
16
17
  import sys
17
- import xml.etree.ElementTree as ET
18
18
  from dataclasses import dataclass
19
19
  from typing import Any, Optional
20
20
  from urllib.parse import urlparse
21
+ from xml.etree import ElementTree
21
22
 
22
23
  import requests
23
24
  from requests.adapters import HTTPAdapter
@@ -96,7 +97,7 @@ class _PropfindFile:
96
97
  size: Optional[int]
97
98
 
98
99
  @classmethod
99
- def from_xml_node(cls, node: ET.Element):
100
+ def from_xml_node(cls, node: ElementTree.Element):
100
101
  """Extract file properties from a `<{DAV:}response>` node."""
101
102
 
102
103
  xml_href = node.find('./{DAV:}href')
@@ -133,8 +134,8 @@ class _PropfindResponse:
133
134
  """
134
135
 
135
136
  try:
136
- xml = ET.fromstring(document) # noqa: S314
137
- except ET.ParseError as ex:
137
+ xml = ElementTree.fromstring(document) # noqa: S314
138
+ except ElementTree.ParseError as ex:
138
139
  raise ValueError("Couldn't parse XML document") from ex
139
140
 
140
141
  if xml.tag != '{DAV:}multistatus':
@@ -161,6 +162,7 @@ class Default(protocol.RSEProtocol):
161
162
  :raises RSEAccessDenied
162
163
  """
163
164
  credentials = credentials or {}
165
+ using_presigned_urls = self.rse['sign_url'] is not None
164
166
  try:
165
167
  parse_url = urlparse(self.path2pfn(''))
166
168
  self.server = f'{parse_url.scheme}://{parse_url.netloc}'
@@ -177,23 +179,29 @@ class Default(protocol.RSEProtocol):
177
179
  except KeyError:
178
180
  self.auth_type = 'cert'
179
181
 
180
- try:
181
- self.cert = credentials['cert']
182
- except KeyError:
183
- x509 = os.getenv('X509_USER_PROXY')
184
- if not x509:
185
- # Trying to get the proxy from the default location
186
- proxy_path = '/tmp/x509up_u%s' % os.geteuid()
187
- if os.path.isfile(proxy_path):
188
- self.cert = (proxy_path, proxy_path)
189
- elif self.auth_token:
190
- # If no proxy is found, we set the cert to None and use the auth_token
191
- self.cert = None
192
- pass
182
+ if using_presigned_urls:
183
+ # Suppress all authentication, otherwise S3 servers will reject
184
+ # requests.
185
+ self.cert = None
186
+ self.auth_token = None
187
+ else:
188
+ try:
189
+ self.cert = credentials['cert']
190
+ except KeyError:
191
+ x509 = os.getenv('X509_USER_PROXY')
192
+ if not x509:
193
+ # Trying to get the proxy from the default location
194
+ proxy_path = '/tmp/x509up_u%s' % os.geteuid()
195
+ if os.path.isfile(proxy_path):
196
+ self.cert = (proxy_path, proxy_path)
197
+ elif self.auth_token:
198
+ # If no proxy is found, we set the cert to None and use the auth_token
199
+ self.cert = None
200
+ pass
201
+ else:
202
+ raise exception.RSEAccessDenied('X509_USER_PROXY is not set')
193
203
  else:
194
- raise exception.RSEAccessDenied('X509_USER_PROXY is not set')
195
- else:
196
- self.cert = (x509, x509)
204
+ self.cert = (x509, x509)
197
205
 
198
206
  try:
199
207
  self.timeout = credentials['timeout']
@@ -205,11 +213,16 @@ class Default(protocol.RSEProtocol):
205
213
  self.session.headers.update({'Authorization': 'Bearer ' + self.auth_token})
206
214
  # "ping" to see if the server is available
207
215
  try:
208
- res = self.session.request('HEAD', self.path2pfn(''), verify=False, timeout=self.timeout, cert=self.cert)
209
- if res.status_code != 200:
210
- raise exception.ServiceUnavailable('Problem to connect %s : %s' % (self.path2pfn(''), res.text))
216
+ test_url = self.path2pfn('')
217
+ res = self.session.request('HEAD', test_url, verify=False, timeout=self.timeout, cert=self.cert)
218
+ # REVISIT: this test checks some URL that doesn't correspond to
219
+ # any valid Rucio file. Although this works for normal WebDAV
220
+ # endpoints, it fails for endpoints using presigned URLs. As a
221
+ # work-around, accept 4xx status codes when using presigned URLs.
222
+ if res.status_code != 200 and not (using_presigned_urls and res.status_code < 500):
223
+ raise exception.ServiceUnavailable('Bad status code %s %s : %s' % (res.status_code, test_url, res.text))
211
224
  except requests.exceptions.ConnectionError as error:
212
- raise exception.ServiceUnavailable('Problem to connect %s : %s' % (self.path2pfn(''), error))
225
+ raise exception.ServiceUnavailable('Problem to connect %s : %s' % (test_url, error))
213
226
  except requests.exceptions.ReadTimeout as error:
214
227
  raise exception.ServiceUnavailable(error)
215
228
 
@@ -259,14 +272,16 @@ class Default(protocol.RSEProtocol):
259
272
 
260
273
  :param pfn: Physical file name of requested file
261
274
  :param dest: Name and path of the files when stored at the client
262
- :param transfer_timeout: Transfer timeout (in seconds) - dummy
275
+ :param transfer_timeout: Transfer timeout (in seconds)
263
276
 
264
277
  :raises DestinationNotAccessible, ServiceUnavailable, SourceNotFound, RSEAccessDenied
265
278
  """
266
279
  path = self.path2pfn(pfn)
267
280
  chunksize = 1024
281
+ transfer_timeout = self.timeout if transfer_timeout is None else transfer_timeout
282
+
268
283
  try:
269
- result = self.session.get(path, verify=False, stream=True, timeout=self.timeout, cert=self.cert)
284
+ result = self.session.get(path, verify=False, stream=True, timeout=transfer_timeout, cert=self.cert)
270
285
  if result and result.status_code in [200, ]:
271
286
  length = None
272
287
  if 'content-length' in result.headers:
@@ -297,7 +312,7 @@ class Default(protocol.RSEProtocol):
297
312
  :param source: Physical file name
298
313
  :param target: Name of the file on the storage system e.g. with prefixed scope
299
314
  :param source_dir Path where the to be transferred files are stored in the local file system
300
- :param transfer_timeout Transfer timeout (in seconds) - dummy
315
+ :param transfer_timeout Transfer timeout (in seconds)
301
316
 
302
317
  :raises DestinationNotAccessible, ServiceUnavailable, SourceNotFound, RSEAccessDenied
303
318
  """
@@ -305,11 +320,13 @@ class Default(protocol.RSEProtocol):
305
320
  full_name = source_dir + '/' + source if source_dir else source
306
321
  directories = path.split('/')
307
322
  # Try the upload without testing the existence of the destination directory
323
+ transfer_timeout = self.timeout if transfer_timeout is None else transfer_timeout
324
+
308
325
  try:
309
326
  if not os.path.exists(full_name):
310
327
  raise exception.SourceNotFound()
311
328
  it = UploadInChunks(full_name, 10000000, progressbar)
312
- result = self.session.put(path, data=IterableToFileAdapter(it), verify=False, allow_redirects=True, timeout=self.timeout, cert=self.cert)
329
+ result = self.session.put(path, data=IterableToFileAdapter(it), verify=False, allow_redirects=True, timeout=transfer_timeout, cert=self.cert)
313
330
  if result.status_code in [200, 201]:
314
331
  return
315
332
  if result.status_code in [409, ]:
@@ -323,7 +340,7 @@ class Default(protocol.RSEProtocol):
323
340
  if not os.path.exists(full_name):
324
341
  raise exception.SourceNotFound()
325
342
  it = UploadInChunks(full_name, 10000000, progressbar)
326
- result = self.session.put(path, data=IterableToFileAdapter(it), verify=False, allow_redirects=True, timeout=self.timeout, cert=self.cert)
343
+ result = self.session.put(path, data=IterableToFileAdapter(it), verify=False, allow_redirects=True, timeout=transfer_timeout, cert=self.cert)
327
344
  if result.status_code in [200, 201]:
328
345
  return
329
346
  if result.status_code in [409, ]:
@@ -537,7 +554,7 @@ class Default(protocol.RSEProtocol):
537
554
  headers = {'Depth': '0'}
538
555
 
539
556
  try:
540
- root = ET.fromstring(self.session.request('PROPFIND', endpoint_basepath, verify=False, headers=headers, cert=self.session.cert).text) # noqa: S314
557
+ root = ElementTree.fromstring(self.session.request('PROPFIND', endpoint_basepath, verify=False, headers=headers, cert=self.session.cert).text) # noqa: S314
541
558
  usedsize = root[0][1][0].find('{DAV:}quota-used-bytes').text
542
559
  try:
543
560
  unusedsize = root[0][1][0].find('{DAV:}quota-available-bytes').text
@@ -548,3 +565,30 @@ class Default(protocol.RSEProtocol):
548
565
  return totalsize, unusedsize
549
566
  except Exception as error:
550
567
  raise exception.ServiceUnavailable(error)
568
+
569
+
570
+ class NoRename(Default):
571
+ """ Implementing access to RSEs using the WebDAV protocol but without
572
+ renaming files on upload/download. Necessary for some storage endpoints.
573
+ """
574
+
575
+ def __init__(self, protocol_attr, rse_settings, logger=logging.log):
576
+ """ Initializes the object with information about the referred RSE.
577
+
578
+ :param protocol_attr: Properties of the requested protocol.
579
+ :param rse_settings: The RSE settings.
580
+ :param logger: Optional decorated logger that can be passed from the calling daemons or servers.
581
+ """
582
+ super(NoRename, self).__init__(protocol_attr, rse_settings, logger=logger)
583
+ self.renaming = False
584
+ self.attributes.pop('determinism_type', None)
585
+
586
+ def rename(self, pfn, new_pfn):
587
+ """ Allows to rename a file stored inside the connected RSE.
588
+
589
+ :param pfn: Current physical file name
590
+ :param new_pfn New physical file name
591
+
592
+ :raises DestinationNotAccessible, ServiceUnavailable, SourceNotFound
593
+ """
594
+ raise NotImplementedError
@@ -16,7 +16,8 @@ import logging
16
16
  import os
17
17
 
18
18
  from rucio.common import exception
19
- from rucio.common.utils import PREFERRED_CHECKSUM, execute
19
+ from rucio.common.checksum import PREFERRED_CHECKSUM
20
+ from rucio.common.utils import execute
20
21
  from rucio.rse.protocols import protocol
21
22
 
22
23
 
rucio/rse/rsemanager.py CHANGED
@@ -15,31 +15,43 @@
15
15
  import copy
16
16
  import logging
17
17
  import random
18
- from collections.abc import Callable
19
18
  from time import sleep
19
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast
20
20
  from urllib.parse import urlparse
21
21
 
22
22
  from rucio.common import constants, exception, types, utils
23
+ from rucio.common.checksum import GLOBALLY_SUPPORTED_CHECKSUMS
23
24
  from rucio.common.config import config_get_int
24
- from rucio.common.constants import RSE_SUPPORTED_PROTOCOL_OPERATIONS
25
25
  from rucio.common.constraints import STRING_TYPES
26
26
  from rucio.common.logging import formatted_logger
27
- from rucio.common.utils import GLOBALLY_SUPPORTED_CHECKSUMS, make_valid_did
27
+ from rucio.common.utils import make_valid_did
28
28
 
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Callable
29
31
 
30
- def get_scope_protocol(vo: str = 'def') -> Callable:
32
+ from sqlalchemy.orm import Session
33
+
34
+ from rucio.rse.protocols.protocol import RSEProtocol
35
+
36
+
37
+ def get_scope_protocol(vo: str = 'def') -> 'Callable':
31
38
  """
32
39
  Returns the callable protocol to translate the pfn to a name/scope pair
33
40
 
34
41
  :returns:
35
42
  Callable: Scope Parser function
36
43
  """
37
- from rucio.rse.protocols.protocol import RSEDeterministicScopeTranslation
44
+ from rucio.rse.translation import RSEDeterministicScopeTranslation
38
45
  translation = RSEDeterministicScopeTranslation(vo=vo)
39
46
  return translation.parser
40
47
 
41
48
 
42
- def get_rse_info(rse=None, vo='def', rse_id=None, session=None) -> types.RSESettingsDict:
49
+ def get_rse_info(
50
+ rse: Optional[str] = None,
51
+ vo: str = 'def',
52
+ rse_id: Optional[str] = None,
53
+ session: Optional["Session"] = None
54
+ ) -> types.RSESettingsDict:
43
55
  """
44
56
  Returns all protocol related RSE attributes.
45
57
  Call with either rse and vo, or (in server mode) rse_id
@@ -79,7 +91,13 @@ def get_rse_info(rse=None, vo='def', rse_id=None, session=None) -> types.RSESett
79
91
  return rse_info
80
92
 
81
93
 
82
- def _get_possible_protocols(rse_settings: types.RSESettingsDict, operation, scheme=None, domain=None, impl=None):
94
+ def _get_possible_protocols(
95
+ rse_settings: types.RSESettingsDict,
96
+ operation: str,
97
+ scheme: Optional[Union[list[str], str]] = None,
98
+ domain: Optional[str] = None,
99
+ impl: Optional[str] = None
100
+ ) -> list[types.RSEProtocolDict]:
83
101
  """
84
102
  Filter the list of available protocols or provided by the supported ones.
85
103
 
@@ -129,8 +147,14 @@ def _get_possible_protocols(rse_settings: types.RSESettingsDict, operation, sche
129
147
  return [c for c in candidates if c not in tbr]
130
148
 
131
149
 
132
- def get_protocols_ordered(rse_settings: types.RSESettingsDict, operation, scheme=None, domain='wan', impl=None):
133
- if operation not in RSE_SUPPORTED_PROTOCOL_OPERATIONS:
150
+ def get_protocols_ordered(
151
+ rse_settings: types.RSESettingsDict,
152
+ operation: constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL,
153
+ scheme: Optional[Union[list[str], str]] = None,
154
+ domain: str = 'wan',
155
+ impl: Optional[str] = None
156
+ ) -> list[types.RSEProtocolDict]:
157
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
134
158
  raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
135
159
 
136
160
  if domain and domain not in utils.rse_supported_protocol_domains():
@@ -141,8 +165,13 @@ def get_protocols_ordered(rse_settings: types.RSESettingsDict, operation, scheme
141
165
  return candidates
142
166
 
143
167
 
144
- def select_protocol(rse_settings: types.RSESettingsDict, operation, scheme=None, domain='wan'):
145
- if operation not in RSE_SUPPORTED_PROTOCOL_OPERATIONS:
168
+ def select_protocol(
169
+ rse_settings: types.RSESettingsDict,
170
+ operation: constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL,
171
+ scheme: Optional[str] = None,
172
+ domain: str = 'wan'
173
+ ) -> types.RSEProtocolDict:
174
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
146
175
  raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
147
176
 
148
177
  if domain and domain not in utils.rse_supported_protocol_domains():
@@ -154,7 +183,16 @@ def select_protocol(rse_settings: types.RSESettingsDict, operation, scheme=None,
154
183
  return min(candidates, key=lambda k: k['domains'][domain][operation])
155
184
 
156
185
 
157
- def create_protocol(rse_settings: types.RSESettingsDict, operation, scheme=None, domain='wan', auth_token=None, protocol_attr=None, logger=logging.log, impl=None):
186
+ def create_protocol(
187
+ rse_settings: types.RSESettingsDict,
188
+ operation: str,
189
+ scheme: Optional[str] = None,
190
+ domain: str = 'wan',
191
+ auth_token: Optional[str] = None,
192
+ protocol_attr: Optional[types.RSEProtocolDict] = None,
193
+ logger: types.LoggerFunction = logging.log,
194
+ impl: Optional[str] = None
195
+ ) -> "RSEProtocol":
158
196
  """
159
197
  Instantiates the protocol defined for the given operation.
160
198
 
@@ -170,9 +208,11 @@ def create_protocol(rse_settings: types.RSESettingsDict, operation, scheme=None,
170
208
 
171
209
  # Verify feasibility of Protocol
172
210
  operation = operation.lower()
173
- if operation not in RSE_SUPPORTED_PROTOCOL_OPERATIONS:
211
+ if operation not in constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS:
174
212
  raise exception.RSEOperationNotSupported('Operation %s is not supported' % operation)
175
213
 
214
+ operation = cast("constants.RSE_ALL_SUPPORTED_PROTOCOL_OPERATIONS_LITERAL", operation)
215
+
176
216
  if domain and domain not in utils.rse_supported_protocol_domains():
177
217
  raise exception.RSEProtocolDomainNotSupported('Domain %s not supported' % domain)
178
218
 
@@ -204,7 +244,16 @@ def create_protocol(rse_settings: types.RSESettingsDict, operation, scheme=None,
204
244
  return protocol
205
245
 
206
246
 
207
- def lfns2pfns(rse_settings: types.RSESettingsDict, lfns, operation='write', scheme=None, domain='wan', auth_token=None, logger=logging.log, impl=None):
247
+ def lfns2pfns(
248
+ rse_settings: types.RSESettingsDict,
249
+ lfns: Union[list[types.LFNDict], types.LFNDict],
250
+ operation: str = 'write',
251
+ scheme: Optional[str] = None,
252
+ domain: str = 'wan',
253
+ auth_token: Optional[str] = None,
254
+ logger: types.LoggerFunction = logging.log,
255
+ impl: Optional[str] = None
256
+ ) -> dict[str, str]:
208
257
  """
209
258
  Convert the lfn to a pfn
210
259
 
@@ -222,7 +271,13 @@ def lfns2pfns(rse_settings: types.RSESettingsDict, lfns, operation='write', sche
222
271
  return create_protocol(rse_settings, operation, scheme, domain, auth_token=auth_token, logger=logger, impl=impl).lfns2pfns(lfns)
223
272
 
224
273
 
225
- def parse_pfns(rse_settings: types.RSESettingsDict, pfns, operation='read', domain='wan', auth_token=None):
274
+ def parse_pfns(
275
+ rse_settings: types.RSESettingsDict,
276
+ pfns: list[str],
277
+ operation: str = 'read',
278
+ domain: str = 'wan',
279
+ auth_token: Optional[str] = None
280
+ ) -> dict[str, dict[str, str]]:
226
281
  """
227
282
  Checks if a PFN is feasible for a given RSE. If so it splits the pfn in its various components.
228
283
 
@@ -244,7 +299,16 @@ def parse_pfns(rse_settings: types.RSESettingsDict, pfns, operation='read', doma
244
299
  return create_protocol(rse_settings, operation, urlparse(pfns[0]).scheme, domain, auth_token=auth_token).parse_pfns(pfns)
245
300
 
246
301
 
247
- def exists(rse_settings: types.RSESettingsDict, files, domain='wan', scheme=None, impl=None, auth_token=None, vo='def', logger=logging.log):
302
+ def exists(
303
+ rse_settings: types.RSESettingsDict,
304
+ files: Union[list[dict[str, str]], dict[str, str]],
305
+ domain: str = 'wan',
306
+ scheme: Optional[str] = None,
307
+ impl: Optional[str] = None,
308
+ auth_token: Optional[str] = None,
309
+ vo: str = 'def',
310
+ logger: types.LoggerFunction = logging.log
311
+ ) -> Union[bool, list[Union[bool, dict[dict[str, str], bool]]]]:
248
312
  """
249
313
  Checks if a file is present at the connected storage.
250
314
  Providing a list indicates the bulk mode.
@@ -263,26 +327,28 @@ def exists(rse_settings: types.RSESettingsDict, files, domain='wan', scheme=None
263
327
  :raises RSENotConnected: no connection to a specific storage has been established
264
328
  """
265
329
 
266
- ret = {}
267
- gs = True # gs represents the global status which indicates if every operation worked in bulk mode
268
-
269
330
  protocol = create_protocol(rse_settings, 'read', scheme=scheme, impl=impl, domain=domain, auth_token=auth_token, logger=logger)
270
331
  protocol.connect()
271
- try:
272
- protocol.exists(None)
273
- except NotImplementedError:
332
+
333
+ from rucio.rse.protocols.protocol import RSEProtocol # Placed it here to avoid possible circular imports
334
+ # Check if 'exists' is truly overridden on the read protocol
335
+ if not utils.is_method_overridden(protocol, RSEProtocol, 'exists'):
336
+ # If not overridden, optionally fall back to a write protocol
274
337
  protocol = create_protocol(rse_settings, 'write', scheme=scheme, domain=domain, auth_token=auth_token, logger=logger)
275
338
  protocol.connect()
276
- except:
277
- pass
278
339
 
279
- files = [files] if not type(files) is list else files
340
+ ret = {}
341
+ gs = True # gs represents the global status which indicates if every operation worked in bulk mode
342
+
343
+ if not isinstance(files, list):
344
+ files = [files]
280
345
  for f in files:
281
346
  exists = None
282
347
  if isinstance(f, STRING_TYPES):
283
348
  exists = protocol.exists(f)
284
349
  ret[f] = exists
285
350
  elif 'scope' in f: # a LFN is provided
351
+ f = cast("types.LFNDict", f)
286
352
  pfn = list(protocol.lfns2pfns(f).values())[0]
287
353
  if isinstance(pfn, exception.RucioException):
288
354
  raise pfn
@@ -300,12 +366,25 @@ def exists(rse_settings: types.RSESettingsDict, files, domain='wan', scheme=None
300
366
 
301
367
  protocol.close()
302
368
  if len(ret) == 1:
303
- for x in ret:
304
- return ret[x]
369
+ return next(iter(ret.values()))
305
370
  return [gs, ret]
306
371
 
307
372
 
308
- def upload(rse_settings: types.RSESettingsDict, lfns, domain='wan', source_dir=None, force_pfn=None, force_scheme=None, transfer_timeout=None, delete_existing=False, sign_service=None, auth_token=None, vo='def', logger=logging.log, impl=None):
373
+ def upload(
374
+ rse_settings: types.RSESettingsDict,
375
+ lfns: Union[list[types.LFNDict], types.LFNDict],
376
+ domain: str = 'wan',
377
+ source_dir: Optional[str] = None,
378
+ force_pfn: Optional[str] = None,
379
+ force_scheme: Optional[str] = None,
380
+ transfer_timeout: Optional[int] = None,
381
+ delete_existing: bool = False,
382
+ sign_service: Optional[str] = None,
383
+ auth_token: Optional[str] = None,
384
+ vo: str = 'def',
385
+ logger: types.LoggerFunction = logging.log,
386
+ impl: Optional[str] = None
387
+ ) -> dict[Union[int, str], Union[bool, str, dict[str, Union[Literal[True], Exception]]]]:
309
388
  """
310
389
  Uploads a file to the connected storage.
311
390
  Providing a list indicates the bulk mode.
@@ -343,7 +422,9 @@ def upload(rse_settings: types.RSESettingsDict, lfns, domain='wan', source_dir=N
343
422
  protocol.connect()
344
423
  protocol_delete = create_protocol(rse_settings, 'delete', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
345
424
  protocol_delete.connect()
346
- lfns = [lfns] if not type(lfns) is list else lfns
425
+
426
+ if not isinstance(lfns, list):
427
+ lfns = [lfns]
347
428
  for lfn in lfns:
348
429
  base_name = lfn.get('filename', lfn['name'])
349
430
  name = lfn.get('name', base_name)
@@ -398,7 +479,7 @@ def upload(rse_settings: types.RSESettingsDict, lfns, domain='wan', source_dir=N
398
479
 
399
480
  try: # Try uploading file
400
481
  logger(logging.DEBUG, 'Uploading to %s.rucio.upload', pfn)
401
- protocol.put(base_name, '%s.rucio.upload' % pfn, source_dir, transfer_timeout=transfer_timeout)
482
+ protocol.put(base_name, '%s.rucio.upload' % pfn, source_dir, transfer_timeout=transfer_timeout) # type: ignore (source_dir could be None)
402
483
  except Exception as e:
403
484
  gs = False
404
485
  ret['%s:%s' % (scope, name)] = e
@@ -496,15 +577,22 @@ def upload(rse_settings: types.RSESettingsDict, lfns, domain='wan', source_dir=N
496
577
  protocol.close()
497
578
  protocol_delete.close()
498
579
  if len(ret) == 1:
499
- for x in ret:
500
- if isinstance(ret[x], Exception):
501
- raise ret[x]
502
- else:
503
- return {0: ret[x], 1: ret, 'success': ret[x], 'pfn': pfn}
580
+ ret_value = next(iter(ret.values()))
581
+ if isinstance(ret_value, Exception):
582
+ raise ret_value
583
+ else:
584
+ return {0: ret_value, 1: ret, 'success': ret_value, 'pfn': pfn}
504
585
  return {0: gs, 1: ret, 'success': gs, 'pfn': pfn}
505
586
 
506
587
 
507
- def delete(rse_settings: types.RSESettingsDict, lfns, domain='wan', auth_token=None, logger=logging.log, impl=None):
588
+ def delete(
589
+ rse_settings: types.RSESettingsDict,
590
+ lfns: Union[list[types.LFNDict], types.LFNDict],
591
+ domain: str = 'wan',
592
+ auth_token: Optional[str] = None,
593
+ logger: types.LoggerFunction = logging.log,
594
+ impl: Optional[str] = None
595
+ ) -> Union[bool, list[Union[bool, dict[str, Union[Literal[True], Exception]]]]]:
508
596
  """
509
597
  Delete a file from the connected storage.
510
598
  Providing a list indicates the bulk mode.
@@ -527,7 +615,8 @@ def delete(rse_settings: types.RSESettingsDict, lfns, domain='wan', auth_token=N
527
615
  protocol = create_protocol(rse_settings, 'delete', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
528
616
  protocol.connect()
529
617
 
530
- lfns = [lfns] if not type(lfns) is list else lfns
618
+ if not isinstance(lfns, list):
619
+ lfns = [lfns]
531
620
  for lfn in lfns:
532
621
  pfn = list(protocol.lfns2pfns(lfn).values())[0]
533
622
  try:
@@ -539,15 +628,22 @@ def delete(rse_settings: types.RSESettingsDict, lfns, domain='wan', auth_token=N
539
628
 
540
629
  protocol.close()
541
630
  if len(ret) == 1:
542
- for x in ret:
543
- if isinstance(ret[x], Exception):
544
- raise ret[x]
545
- else:
546
- return ret[x]
631
+ ret_value = next(iter(ret.values()))
632
+ if isinstance(ret_value, Exception):
633
+ raise ret_value
634
+ else:
635
+ return ret_value
547
636
  return [gs, ret]
548
637
 
549
638
 
550
- def rename(rse_settings: types.RSESettingsDict, files, domain='wan', auth_token=None, logger=logging.log, impl=None):
639
+ def rename(
640
+ rse_settings: types.RSESettingsDict,
641
+ files: Union[list[dict[str, str]], dict[str, str]],
642
+ domain: str = 'wan',
643
+ auth_token: Optional[str] = None,
644
+ logger: types.LoggerFunction = logging.log,
645
+ impl: Optional[str] = None
646
+ ) -> Union[bool, list[Union[bool, dict[str, Union[Literal[True], Exception]]]]]:
551
647
  """
552
648
  Rename files stored on the connected storage.
553
649
  Providing a list indicates the bulk mode.
@@ -578,7 +674,8 @@ def rename(rse_settings: types.RSESettingsDict, files, domain='wan', auth_token=
578
674
  protocol = create_protocol(rse_settings, 'write', domain=domain, auth_token=auth_token, logger=logger, impl=impl)
579
675
  protocol.connect()
580
676
 
581
- files = [files] if not type(files) is list else files
677
+ if not isinstance(files, list):
678
+ files = [files]
582
679
  for f in files:
583
680
  pfn = None
584
681
  new_pfn = None
@@ -615,15 +712,22 @@ def rename(rse_settings: types.RSESettingsDict, files, domain='wan', auth_token=
615
712
 
616
713
  protocol.close()
617
714
  if len(ret) == 1:
618
- for x in ret:
619
- if isinstance(ret[x], Exception):
620
- raise ret[x]
621
- else:
622
- return ret[x]
715
+ ret_value = next(iter(ret.values()))
716
+ if isinstance(ret_value, Exception):
717
+ raise ret_value
718
+ else:
719
+ return ret_value
623
720
  return [gs, ret]
624
721
 
625
722
 
626
- def get_space_usage(rse_settings: types.RSESettingsDict, scheme=None, domain='wan', auth_token=None, logger=logging.log, impl=None):
723
+ def get_space_usage(
724
+ rse_settings: types.RSESettingsDict,
725
+ scheme: Optional[str] = None,
726
+ domain: str = 'wan',
727
+ auth_token: Optional[str] = None,
728
+ logger: types.LoggerFunction = logging.log,
729
+ impl: Optional[str] = None
730
+ ) -> list[Union[bool, Union[dict[str, int], Exception]]]:
627
731
  """
628
732
  Get RSE space usage information.
629
733
 
@@ -655,7 +759,14 @@ def get_space_usage(rse_settings: types.RSESettingsDict, scheme=None, domain='wa
655
759
  return [gs, ret]
656
760
 
657
761
 
658
- def find_matching_scheme(rse_settings_dest, rse_settings_src, operation_src, operation_dest, domain='wan', scheme=None):
762
+ def find_matching_scheme(
763
+ rse_settings_dest: types.RSESettingsDict,
764
+ rse_settings_src: types.RSESettingsDict,
765
+ operation_src: str,
766
+ operation_dest: str,
767
+ domain: str = 'wan',
768
+ scheme: Optional[Union[str, list[str]]] = None
769
+ ) -> tuple[str, str, int, int]:
659
770
  """
660
771
  Find the best matching scheme between two RSEs
661
772
 
@@ -724,7 +835,10 @@ def find_matching_scheme(rse_settings_dest, rse_settings_src, operation_src, ope
724
835
  raise exception.RSEProtocolNotSupported('No protocol for provided settings found : %s.' % str(rse_settings_dest))
725
836
 
726
837
 
727
- def _retry_protocol_stat(protocol, pfn):
838
+ def _retry_protocol_stat(
839
+ protocol: "RSEProtocol",
840
+ pfn: str
841
+ ) -> dict[str, Any]:
728
842
  """
729
843
  try to stat file, on fail try again 1s, 2s, 4s, 8s, 16s, 32s later. Fail is all fail
730
844
 
@@ -746,7 +860,10 @@ def _retry_protocol_stat(protocol, pfn):
746
860
  return protocol.stat(pfn)
747
861
 
748
862
 
749
- def __check_compatible_scheme(dest_scheme, src_scheme):
863
+ def __check_compatible_scheme(
864
+ dest_scheme: str,
865
+ src_scheme: str
866
+ ) -> bool:
750
867
  """
751
868
  Check if two schemes are compatible, such as srm and gsiftp
752
869