rucio-clients 37.0.0rc1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (104) hide show
  1. rucio/__init__.py +17 -0
  2. rucio/alembicrevision.py +15 -0
  3. rucio/cli/__init__.py +14 -0
  4. rucio/cli/account.py +216 -0
  5. rucio/cli/bin_legacy/__init__.py +13 -0
  6. rucio/cli/bin_legacy/rucio.py +2825 -0
  7. rucio/cli/bin_legacy/rucio_admin.py +2500 -0
  8. rucio/cli/command.py +272 -0
  9. rucio/cli/config.py +72 -0
  10. rucio/cli/did.py +191 -0
  11. rucio/cli/download.py +128 -0
  12. rucio/cli/lifetime_exception.py +33 -0
  13. rucio/cli/replica.py +162 -0
  14. rucio/cli/rse.py +293 -0
  15. rucio/cli/rule.py +158 -0
  16. rucio/cli/scope.py +40 -0
  17. rucio/cli/subscription.py +73 -0
  18. rucio/cli/upload.py +60 -0
  19. rucio/cli/utils.py +226 -0
  20. rucio/client/__init__.py +15 -0
  21. rucio/client/accountclient.py +432 -0
  22. rucio/client/accountlimitclient.py +183 -0
  23. rucio/client/baseclient.py +983 -0
  24. rucio/client/client.py +120 -0
  25. rucio/client/configclient.py +126 -0
  26. rucio/client/credentialclient.py +59 -0
  27. rucio/client/didclient.py +868 -0
  28. rucio/client/diracclient.py +56 -0
  29. rucio/client/downloadclient.py +1783 -0
  30. rucio/client/exportclient.py +44 -0
  31. rucio/client/fileclient.py +50 -0
  32. rucio/client/importclient.py +42 -0
  33. rucio/client/lifetimeclient.py +90 -0
  34. rucio/client/lockclient.py +109 -0
  35. rucio/client/metaconventionsclient.py +140 -0
  36. rucio/client/pingclient.py +44 -0
  37. rucio/client/replicaclient.py +452 -0
  38. rucio/client/requestclient.py +125 -0
  39. rucio/client/richclient.py +317 -0
  40. rucio/client/rseclient.py +746 -0
  41. rucio/client/ruleclient.py +294 -0
  42. rucio/client/scopeclient.py +90 -0
  43. rucio/client/subscriptionclient.py +173 -0
  44. rucio/client/touchclient.py +82 -0
  45. rucio/client/uploadclient.py +969 -0
  46. rucio/common/__init__.py +13 -0
  47. rucio/common/bittorrent.py +234 -0
  48. rucio/common/cache.py +111 -0
  49. rucio/common/checksum.py +168 -0
  50. rucio/common/client.py +122 -0
  51. rucio/common/config.py +788 -0
  52. rucio/common/constants.py +217 -0
  53. rucio/common/constraints.py +17 -0
  54. rucio/common/didtype.py +237 -0
  55. rucio/common/exception.py +1208 -0
  56. rucio/common/extra.py +31 -0
  57. rucio/common/logging.py +420 -0
  58. rucio/common/pcache.py +1409 -0
  59. rucio/common/plugins.py +185 -0
  60. rucio/common/policy.py +93 -0
  61. rucio/common/schema/__init__.py +200 -0
  62. rucio/common/schema/generic.py +416 -0
  63. rucio/common/schema/generic_multi_vo.py +395 -0
  64. rucio/common/stomp_utils.py +423 -0
  65. rucio/common/stopwatch.py +55 -0
  66. rucio/common/test_rucio_server.py +154 -0
  67. rucio/common/types.py +483 -0
  68. rucio/common/utils.py +1688 -0
  69. rucio/rse/__init__.py +96 -0
  70. rucio/rse/protocols/__init__.py +13 -0
  71. rucio/rse/protocols/bittorrent.py +194 -0
  72. rucio/rse/protocols/cache.py +111 -0
  73. rucio/rse/protocols/dummy.py +100 -0
  74. rucio/rse/protocols/gfal.py +708 -0
  75. rucio/rse/protocols/globus.py +243 -0
  76. rucio/rse/protocols/http_cache.py +82 -0
  77. rucio/rse/protocols/mock.py +123 -0
  78. rucio/rse/protocols/ngarc.py +209 -0
  79. rucio/rse/protocols/posix.py +250 -0
  80. rucio/rse/protocols/protocol.py +361 -0
  81. rucio/rse/protocols/rclone.py +365 -0
  82. rucio/rse/protocols/rfio.py +145 -0
  83. rucio/rse/protocols/srm.py +338 -0
  84. rucio/rse/protocols/ssh.py +414 -0
  85. rucio/rse/protocols/storm.py +195 -0
  86. rucio/rse/protocols/webdav.py +594 -0
  87. rucio/rse/protocols/xrootd.py +302 -0
  88. rucio/rse/rsemanager.py +881 -0
  89. rucio/rse/translation.py +260 -0
  90. rucio/vcsversion.py +11 -0
  91. rucio/version.py +45 -0
  92. rucio_clients-37.0.0rc1.data/data/etc/rse-accounts.cfg.template +25 -0
  93. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.atlas.client.template +43 -0
  94. rucio_clients-37.0.0rc1.data/data/etc/rucio.cfg.template +241 -0
  95. rucio_clients-37.0.0rc1.data/data/requirements.client.txt +19 -0
  96. rucio_clients-37.0.0rc1.data/data/rucio_client/merge_rucio_configs.py +144 -0
  97. rucio_clients-37.0.0rc1.data/scripts/rucio +133 -0
  98. rucio_clients-37.0.0rc1.data/scripts/rucio-admin +97 -0
  99. rucio_clients-37.0.0rc1.dist-info/METADATA +54 -0
  100. rucio_clients-37.0.0rc1.dist-info/RECORD +104 -0
  101. rucio_clients-37.0.0rc1.dist-info/WHEEL +5 -0
  102. rucio_clients-37.0.0rc1.dist-info/licenses/AUTHORS.rst +100 -0
  103. rucio_clients-37.0.0rc1.dist-info/licenses/LICENSE +201 -0
  104. rucio_clients-37.0.0rc1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,452 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from datetime import datetime
16
+ from json import dumps, loads
17
+ from typing import Any, Optional
18
+ from urllib.parse import quote_plus
19
+
20
+ from requests.status_codes import codes
21
+
22
+ from rucio.client.baseclient import BaseClient, choice
23
+ from rucio.common.utils import build_url, chunks, render_json
24
+
25
+
26
+ class ReplicaClient(BaseClient):
27
+ """Replica client class for working with replicas"""
28
+
29
+ REPLICAS_BASEURL = 'replicas'
30
+ REPLICAS_CHUNK_SIZE = 1000
31
+
32
+ def quarantine_replicas(self, replicas, rse=None, rse_id=None):
33
+ """
34
+ Add quaratined replicas for RSE.
35
+
36
+ :param replicas: List of replica infos: {'scope': <scope> (optional), 'name': <name> (optional), 'path':<path> (required)}.
37
+ :param rse: RSE name.
38
+ :param rse_id: RSE id. Either RSE name or RSE id must be specified, but not both
39
+ """
40
+
41
+ if (rse is None) == (rse_id is None):
42
+ raise ValueError("Either RSE name or RSE id must be specified, but not both")
43
+
44
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'quarantine']))
45
+ headers = {}
46
+ for chunk in chunks(replicas, self.REPLICAS_CHUNK_SIZE):
47
+ data = {'rse': rse, 'rse_id': rse_id, 'replicas': chunk}
48
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
49
+ if r.status_code != codes.ok:
50
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
51
+ raise exc_cls(exc_msg)
52
+
53
+ def declare_bad_file_replicas(self, replicas, reason, force=False):
54
+ """
55
+ Declare a list of bad replicas.
56
+
57
+ :param replicas: Either a list of PFNs (string) or a list of dicts {'scope': <scope>, 'name': <name>, 'rse_id': <rse_id> or 'rse': <rse_name>}
58
+ :param reason: The reason of the loss.
59
+ :param force: boolean, tell the serrver to ignore existing replica status in the bad_replicas table. Default: False
60
+ :returns: Dictionary {"rse_name": ["did: error",...]} - list of strings for DIDs failed to declare, by RSE
61
+ """
62
+
63
+ out = {} # {rse: ["did: error text",...]}
64
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad']))
65
+ headers = {}
66
+ for chunk in chunks(replicas, self.REPLICAS_CHUNK_SIZE):
67
+ data = {'reason': reason, 'replicas': chunk, 'force': force}
68
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
69
+ if r.status_code not in (codes.created, codes.ok):
70
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
71
+ raise exc_cls(exc_msg)
72
+ chunk_result = loads(r.text)
73
+ if chunk_result:
74
+ for rse, lst in chunk_result.items():
75
+ out.setdefault(rse, []).extend(lst)
76
+ return out
77
+
78
+ def declare_bad_did_replicas(self, rse, dids, reason):
79
+ """
80
+ Declare a list of bad replicas.
81
+
82
+ :param rse: The RSE where the bad replicas reside
83
+ :param dids: The DIDs of the bad replicas
84
+ :param reason: The reason of the loss.
85
+ """
86
+ data = {'reason': reason, 'rse': rse, 'dids': dids}
87
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad/dids']))
88
+ headers = {}
89
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
90
+ if r.status_code == codes.created:
91
+ return loads(r.text)
92
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
93
+ raise exc_cls(exc_msg)
94
+
95
+ def declare_suspicious_file_replicas(self, pfns, reason):
96
+ """
97
+ Declare a list of bad replicas.
98
+
99
+ :param pfns: The list of PFNs.
100
+ :param reason: The reason of the loss.
101
+ """
102
+ data = {'reason': reason, 'pfns': pfns}
103
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'suspicious']))
104
+ headers = {}
105
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
106
+ if r.status_code == codes.created:
107
+ return loads(r.text)
108
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
109
+ raise exc_cls(exc_msg)
110
+
111
+ def get_did_from_pfns(self, pfns, rse=None):
112
+ """
113
+ Get the DIDs associated to a PFN on one given RSE
114
+
115
+ :param pfns: The list of PFNs.
116
+ :param rse: The RSE name.
117
+ :returns: A list of dictionaries {pfn: {'scope': scope, 'name': name}}
118
+ """
119
+ data = {'rse': rse, 'pfns': pfns}
120
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'dids']))
121
+ headers = {}
122
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
123
+ if r.status_code == codes.ok:
124
+ return self._load_json_data(r)
125
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
126
+ raise exc_cls(exc_msg)
127
+
128
+ def list_replicas(self, dids, schemes=None, ignore_availability=True,
129
+ all_states=False, metalink=False, rse_expression=None,
130
+ client_location=None, sort=None, domain=None,
131
+ signature_lifetime=None, nrandom=None,
132
+ resolve_archives=True, resolve_parents=False,
133
+ updated_after=None):
134
+ """
135
+ List file replicas for a list of data identifiers (DIDs).
136
+
137
+ :param dids: The list of data identifiers (DIDs) like :
138
+ [{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
139
+ :param schemes: A list of schemes to filter the replicas. (e.g. file, http, ...)
140
+ :param ignore_availability: Also include replicas from blocked RSEs into the list
141
+ :param metalink: ``False`` (default) retrieves as JSON,
142
+ ``True`` retrieves as metalink4+xml.
143
+ :param rse_expression: The RSE expression to restrict replicas on a set of RSEs.
144
+ :param client_location: Client location dictionary for PFN modification {'ip', 'fqdn', 'site', 'latitude', 'longitude'}
145
+ :param sort: Sort the replicas: ``geoip`` - based on src/dst IP topographical distance
146
+ :param domain: Define the domain. None is fallback to 'wan', otherwise 'wan, 'lan', or 'all'
147
+ :param signature_lifetime: If supported, in seconds, restrict the lifetime of the signed PFN.
148
+ :param nrandom: pick N random replicas. If the initial number of replicas is smaller than N, returns all replicas.
149
+ :param resolve_archives: When set to True, find archives which contain the replicas.
150
+ :param resolve_parents: When set to True, find all parent datasets which contain the replicas.
151
+ :param updated_after: epoch timestamp or datetime object (UTC time), only return replicas updated after this time
152
+
153
+ :returns: A list of dictionaries with replica information.
154
+
155
+ """
156
+ data = {'dids': dids,
157
+ 'domain': domain}
158
+
159
+ if schemes:
160
+ data['schemes'] = schemes
161
+ if ignore_availability is not None:
162
+ data['ignore_availability'] = ignore_availability
163
+ data['all_states'] = all_states
164
+
165
+ if rse_expression:
166
+ data['rse_expression'] = rse_expression
167
+
168
+ if client_location:
169
+ data['client_location'] = client_location
170
+
171
+ if sort:
172
+ data['sort'] = sort
173
+
174
+ if updated_after:
175
+ if isinstance(updated_after, datetime):
176
+ # encode in UTC string with format '%Y-%m-%dT%H:%M:%S' e.g. '2020-03-02T12:01:38'
177
+ data['updated_after'] = updated_after.strftime('%Y-%m-%dT%H:%M:%S')
178
+ else:
179
+ data['updated_after'] = updated_after
180
+
181
+ if signature_lifetime:
182
+ data['signature_lifetime'] = signature_lifetime
183
+
184
+ if nrandom:
185
+ data['nrandom'] = nrandom
186
+
187
+ data['resolve_archives'] = resolve_archives
188
+
189
+ data['resolve_parents'] = resolve_parents
190
+
191
+ url = build_url(choice(self.list_hosts),
192
+ path='/'.join([self.REPLICAS_BASEURL, 'list']))
193
+
194
+ headers = {}
195
+ if metalink:
196
+ headers['Accept'] = 'application/metalink4+xml'
197
+
198
+ # pass json dict in querystring
199
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data), stream=True)
200
+ if r.status_code == codes.ok:
201
+ if not metalink:
202
+ return self._load_json_data(r)
203
+ return r.text
204
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
205
+ raise exc_cls(exc_msg)
206
+
207
+ def list_suspicious_replicas(self, rse_expression=None, younger_than=None, nattempts=None):
208
+ """
209
+ List file replicas tagged as suspicious.
210
+
211
+ :param rse_expression: The RSE expression to restrict replicas on a set of RSEs.
212
+ :param younger_than: Datetime object to select the replicas which were declared since younger_than date. Default value = 10 days ago.
213
+ :param nattempts: The minimum number of replica appearances in the bad_replica DB table from younger_than date. Default value = 0.
214
+ :param state: State of the replica, either 'BAD' or 'SUSPICIOUS'. No value returns replicas with either state.
215
+
216
+ """
217
+ params = {}
218
+ if rse_expression:
219
+ params['rse_expression'] = rse_expression
220
+
221
+ if younger_than:
222
+ params['younger_than'] = younger_than
223
+
224
+ if nattempts:
225
+ params['nattempts'] = nattempts
226
+
227
+ url = build_url(choice(self.list_hosts),
228
+ path='/'.join([self.REPLICAS_BASEURL, 'suspicious']))
229
+ r = self._send_request(url, type_='GET', params=params)
230
+ if r.status_code == codes.ok:
231
+ return self._load_json_data(r)
232
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
233
+ raise exc_cls(exc_msg)
234
+
235
+ def add_replica(
236
+ self,
237
+ rse: str,
238
+ scope: str,
239
+ name: str,
240
+ bytes_: int,
241
+ adler32: str,
242
+ pfn: Optional[str] = None,
243
+ md5: Optional[str] = None,
244
+ meta: Optional[dict[str, Any]] = None
245
+ ) -> bool:
246
+ """
247
+ Add file replicas to a RSE.
248
+
249
+ :param rse: the RSE name.
250
+ :param scope: The scope of the file.
251
+ :param name: The name of the file.
252
+ :param bytes_: The size in bytes.
253
+ :param adler32: adler32 checksum.
254
+ :param pfn: PFN of the file for non deterministic RSE.
255
+ :param md5: md5 checksum.
256
+ :param meta: Metadata attributes.
257
+
258
+ :return: True if files were created successfully.
259
+
260
+ """
261
+ meta = meta or {}
262
+ dict_ = {'scope': scope, 'name': name, 'bytes': bytes_, 'meta': meta, 'adler32': adler32}
263
+ if md5:
264
+ dict_['md5'] = md5
265
+ if pfn:
266
+ dict_['pfn'] = pfn
267
+ return self.add_replicas(rse=rse, files=[dict_])
268
+
269
+ def add_replicas(self, rse, files, ignore_availability=True):
270
+ """
271
+ Bulk add file replicas to a RSE.
272
+
273
+ :param rse: the RSE name.
274
+ :param files: The list of files. This is a list of DIDs like :
275
+ [{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
276
+ :param ignore_availability: Ignore the RSE blocklsit.
277
+
278
+ :return: True if files were created successfully.
279
+
280
+ """
281
+ url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
282
+ data = {'rse': rse, 'files': files, 'ignore_availability': ignore_availability}
283
+ r = self._send_request(url, type_='POST', data=render_json(**data))
284
+ if r.status_code == codes.created:
285
+ return True
286
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
287
+ raise exc_cls(exc_msg)
288
+
289
+ def delete_replicas(self, rse, files, ignore_availability=True):
290
+ """
291
+ Bulk delete file replicas from a RSE.
292
+
293
+ :param rse: the RSE name.
294
+ :param files: The list of files. This is a list of DIDs like :
295
+ [{'scope': <scope1>, 'name': <name1>}, {'scope': <scope2>, 'name': <name2>}, ...]
296
+ :param ignore_availability: Ignore the RSE blocklist.
297
+
298
+ :return: True if files have been deleted successfully.
299
+
300
+ """
301
+ url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
302
+ data = {'rse': rse, 'files': files, 'ignore_availability': ignore_availability}
303
+ r = self._send_request(url, type_='DEL', data=render_json(**data))
304
+ if r.status_code == codes.ok:
305
+ return True
306
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
307
+ raise exc_cls(exc_msg)
308
+
309
+ def update_replicas_states(self, rse, files):
310
+ """
311
+ Bulk update the file replicas states from a RSE.
312
+
313
+ :param rse: the RSE name.
314
+ :param files: The list of files. This is a list of DIDs like :
315
+ [{'scope': <scope1>, 'name': <name1>, 'state': <state1>}, {'scope': <scope2>, 'name': <name2>, 'state': <state2>}, ...],
316
+ where a state value can be either of:
317
+ 'A' (AVAILABLE)
318
+ 'U' (UNAVAILABLE)
319
+ 'C' (COPYING)
320
+ 'B' (BEING_DELETED)
321
+ 'D' (BAD)
322
+ 'T' (TEMPORARY_UNAVAILABLE)
323
+ :return: True if replica states have been updated successfully, otherwise an exception is raised.
324
+
325
+ """
326
+ url = build_url(choice(self.list_hosts), path=self.REPLICAS_BASEURL)
327
+ data = {'rse': rse, 'files': files}
328
+ r = self._send_request(url, type_='PUT', data=render_json(**data))
329
+ if r.status_code == codes.ok:
330
+ return True
331
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
332
+ raise exc_cls(exc_msg)
333
+
334
+ def list_dataset_replicas(self, scope, name, deep=False):
335
+ """
336
+ List dataset replicas for a did (scope:name).
337
+
338
+ :param scope: The scope of the dataset.
339
+ :param name: The name of the dataset.
340
+ :param deep: Lookup at the file level.
341
+
342
+ :returns: A list of dict dataset replicas.
343
+
344
+ """
345
+ payload = {}
346
+ if deep:
347
+ payload = {'deep': True}
348
+
349
+ url = build_url(self.host,
350
+ path='/'.join([self.REPLICAS_BASEURL, quote_plus(scope), quote_plus(name), 'datasets']),
351
+ params=payload)
352
+ r = self._send_request(url, type_='GET')
353
+ if r.status_code == codes.ok:
354
+ return self._load_json_data(r)
355
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
356
+ raise exc_cls(exc_msg)
357
+
358
+ def list_dataset_replicas_bulk(self, dids):
359
+ """
360
+ List dataset replicas for a did (scope:name).
361
+
362
+ :param dids: The list of DIDs of the datasets.
363
+
364
+ :returns: A list of dict dataset replicas.
365
+ """
366
+ payload = {'dids': list(dids)}
367
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'datasets_bulk']))
368
+ r = self._send_request(url, type_='POST', data=dumps(payload))
369
+ if r.status_code == codes.ok:
370
+ return self._load_json_data(r)
371
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
372
+ raise exc_cls(exc_msg)
373
+
374
+ def list_dataset_replicas_vp(self, scope, name, deep=False):
375
+ """
376
+ List dataset replicas for a DID (scope:name) using the
377
+ Virtual Placement service.
378
+
379
+ NOTICE: This is an RnD function and might change or go away at any time.
380
+
381
+ :param scope: The scope of the dataset.
382
+ :param name: The name of the dataset.
383
+ :param deep: Lookup at the file level.
384
+
385
+ :returns: If VP exists a list of dicts of sites
386
+ """
387
+ payload = {}
388
+ if deep:
389
+ payload = {'deep': True}
390
+
391
+ url = build_url(self.host,
392
+ path='/'.join([self.REPLICAS_BASEURL, quote_plus(scope), quote_plus(name), 'datasets_vp']),
393
+ params=payload)
394
+ r = self._send_request(url, type_='GET')
395
+ if r.status_code == codes.ok:
396
+ return self._load_json_data(r)
397
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
398
+ raise exc_cls(exc_msg)
399
+
400
+ def list_datasets_per_rse(self, rse, filters=None, limit=None):
401
+ """
402
+ List datasets at a RSE.
403
+
404
+ :param rse: the rse name.
405
+ :param filters: dictionary of attributes by which the results should be filtered.
406
+ :param limit: limit number.
407
+
408
+ :returns: A list of dict dataset replicas.
409
+
410
+ """
411
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'rse', rse]))
412
+ r = self._send_request(url, type_='GET')
413
+ if r.status_code == codes.ok:
414
+ return self._load_json_data(r)
415
+
416
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
417
+ raise exc_cls(exc_msg)
418
+
419
+ def add_bad_pfns(self, pfns, reason, state, expires_at):
420
+ """
421
+ Declare a list of bad replicas.
422
+
423
+ :param pfns: The list of PFNs.
424
+ :param reason: The reason of the loss.
425
+ :param state: The state of the replica. Either BAD, SUSPICIOUS, TEMPORARY_UNAVAILABLE
426
+ :param expires_at: Specify a timeout for the TEMPORARY_UNAVAILABLE replicas. None for BAD files.
427
+
428
+ :return: True if PFNs were created successfully.
429
+
430
+ """
431
+ data = {'reason': reason, 'pfns': pfns, 'state': state, 'expires_at': expires_at}
432
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'bad/pfns']))
433
+ headers = {}
434
+ r = self._send_request(url, headers=headers, type_='POST', data=dumps(data))
435
+ if r.status_code == codes.created:
436
+ return True
437
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
438
+ raise exc_cls(exc_msg)
439
+
440
+ def set_tombstone(self, replicas):
441
+ """
442
+ Set a tombstone on a list of replicas.
443
+
444
+ :param replicas: list of replicas.
445
+ """
446
+ url = build_url(self.host, path='/'.join([self.REPLICAS_BASEURL, 'tombstone']))
447
+ data = {'replicas': replicas}
448
+ r = self._send_request(url, type_='POST', data=render_json(**data))
449
+ if r.status_code == codes.created:
450
+ return True
451
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
452
+ raise exc_cls(exc_msg)
@@ -0,0 +1,125 @@
1
+ # Copyright European Organization for Nuclear Research (CERN) since 2012
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from typing import TYPE_CHECKING, Any, Optional
16
+ from urllib.parse import quote_plus
17
+
18
+ from requests.status_codes import codes
19
+
20
+ from rucio.client.baseclient import BaseClient, choice
21
+ from rucio.common.utils import build_url
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Iterator, Sequence
25
+
26
+
27
+ class RequestClient(BaseClient):
28
+
29
+ REQUEST_BASEURL = 'requests'
30
+
31
+ def list_requests(
32
+ self,
33
+ src_rse: str,
34
+ dst_rse: str,
35
+ request_states: 'Sequence[str]'
36
+ ) -> 'Iterator[dict[str, Any]]':
37
+ """Return latest request details
38
+
39
+ :return: request information
40
+ """
41
+ path = '/'.join([self.REQUEST_BASEURL, 'list']) + '?' + '&'.join(['src_rse={}'.format(src_rse), 'dst_rse={}'.format(
42
+ dst_rse), 'request_states={}'.format(request_states)])
43
+ url = build_url(choice(self.list_hosts), path=path)
44
+ r = self._send_request(url, type_='GET')
45
+
46
+ if r.status_code == codes.ok:
47
+ return self._load_json_data(r)
48
+ else:
49
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
50
+ raise exc_cls(exc_msg)
51
+
52
+ def list_requests_history(
53
+ self,
54
+ src_rse: str,
55
+ dst_rse: str,
56
+ request_states: 'Sequence[str]',
57
+ offset: int = 0,
58
+ limit: int = 100
59
+ ) -> 'Iterator[dict[str, Any]]':
60
+ """Return historical request details
61
+
62
+ :return: request information
63
+ """
64
+ path = '/'.join([self.REQUEST_BASEURL, 'history', 'list']) + '?' + '&'.join(['src_rse={}'.format(src_rse), 'dst_rse={}'.format(
65
+ dst_rse), 'request_states={}'.format(request_states), 'offset={}'.format(offset), 'limit={}'.format(limit)])
66
+ url = build_url(choice(self.list_hosts), path=path)
67
+ r = self._send_request(url, type_='GET')
68
+
69
+ if r.status_code == codes.ok:
70
+ return self._load_json_data(r)
71
+ else:
72
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
73
+ raise exc_cls(exc_msg)
74
+
75
+ def list_request_by_did(
76
+ self,
77
+ name: str,
78
+ rse: str,
79
+ scope: Optional[str] = None
80
+ ) -> 'Iterator[dict[str, Any]]':
81
+ """Return latest request details for a DID
82
+
83
+ :param name: DID
84
+ :param rse: Destination RSE name
85
+ :param scope: rucio scope, defaults to None
86
+ :raises exc_cls: from BaseClient._get_exception
87
+ :return: request information
88
+ """
89
+
90
+ if scope is not None:
91
+ path = '/'.join([self.REQUEST_BASEURL, quote_plus(scope), quote_plus(name), rse])
92
+ url = build_url(choice(self.list_hosts), path=path)
93
+ r = self._send_request(url, type_='GET')
94
+
95
+ if r.status_code == codes.ok:
96
+ return next(self._load_json_data(r))
97
+ else:
98
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
99
+ raise exc_cls(exc_msg)
100
+
101
+ def list_request_history_by_did(
102
+ self,
103
+ name: str,
104
+ rse: str,
105
+ scope: Optional[str] = None
106
+ ) -> 'Iterator[dict[str, Any]]':
107
+ """Return latest request details for a DID
108
+
109
+ :param name: DID
110
+ :param rse: Destination RSE name
111
+ :param scope: rucio scope, defaults to None
112
+ :raises exc_cls: from BaseClient._get_exception
113
+ :return: request information
114
+ """
115
+
116
+ if scope is not None:
117
+ path = '/'.join([self.REQUEST_BASEURL, 'history', quote_plus(scope), quote_plus(name), rse])
118
+ url = build_url(choice(self.list_hosts), path=path)
119
+ r = self._send_request(url, type_='GET')
120
+
121
+ if r.status_code == codes.ok:
122
+ return next(self._load_json_data(r))
123
+ else:
124
+ exc_cls, exc_msg = self._get_exception(headers=r.headers, status_code=r.status_code, data=r.content)
125
+ raise exc_cls(exc_msg)