rucio-clients 35.7.0__py3-none-any.whl → 37.0.0rc2__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 (86) 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/stomp_utils.py +383 -119
  45. rucio/common/test_rucio_server.py +12 -6
  46. rucio/common/types.py +132 -52
  47. rucio/common/utils.py +93 -643
  48. rucio/rse/__init__.py +3 -3
  49. rucio/rse/protocols/bittorrent.py +11 -1
  50. rucio/rse/protocols/cache.py +0 -11
  51. rucio/rse/protocols/dummy.py +0 -11
  52. rucio/rse/protocols/gfal.py +14 -9
  53. rucio/rse/protocols/globus.py +1 -1
  54. rucio/rse/protocols/http_cache.py +1 -1
  55. rucio/rse/protocols/posix.py +2 -2
  56. rucio/rse/protocols/protocol.py +84 -317
  57. rucio/rse/protocols/rclone.py +2 -1
  58. rucio/rse/protocols/rfio.py +10 -1
  59. rucio/rse/protocols/ssh.py +2 -1
  60. rucio/rse/protocols/storm.py +2 -13
  61. rucio/rse/protocols/webdav.py +74 -30
  62. rucio/rse/protocols/xrootd.py +2 -1
  63. rucio/rse/rsemanager.py +170 -53
  64. rucio/rse/translation.py +260 -0
  65. rucio/vcsversion.py +4 -4
  66. rucio/version.py +7 -0
  67. {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rucio.cfg.atlas.client.template +3 -2
  68. {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rucio.cfg.template +3 -19
  69. {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/requirements.client.txt +11 -7
  70. rucio_clients-37.0.0rc2.data/scripts/rucio +133 -0
  71. rucio_clients-37.0.0rc2.data/scripts/rucio-admin +97 -0
  72. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/METADATA +18 -14
  73. rucio_clients-37.0.0rc2.dist-info/RECORD +104 -0
  74. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/licenses/AUTHORS.rst +3 -0
  75. rucio/common/schema/atlas.py +0 -413
  76. rucio/common/schema/belleii.py +0 -408
  77. rucio/common/schema/domatpc.py +0 -401
  78. rucio/common/schema/escape.py +0 -426
  79. rucio/common/schema/icecube.py +0 -406
  80. rucio/rse/protocols/gsiftp.py +0 -92
  81. rucio_clients-35.7.0.dist-info/RECORD +0 -88
  82. {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/etc/rse-accounts.cfg.template +0 -0
  83. {rucio_clients-35.7.0.data → rucio_clients-37.0.0rc2.data}/data/rucio_client/merge_rucio_configs.py +0 -0
  84. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/WHEEL +0 -0
  85. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  86. {rucio_clients-35.7.0.dist-info → rucio_clients-37.0.0rc2.dist-info}/top_level.txt +0 -0
rucio/cli/scope.py ADDED
@@ -0,0 +1,40 @@
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
+ import click
15
+
16
+ from rucio.cli.bin_legacy.rucio_admin import add_scope, list_scopes
17
+ from rucio.cli.utils import Arguments
18
+
19
+
20
+ @click.group()
21
+ def scope():
22
+ """Interact with scopes - a logical grouping of DIDs"""
23
+
24
+
25
+ @scope.command("add")
26
+ @click.argument("scope-name")
27
+ @click.option("-a", "--account", help="Associated account", required=True)
28
+ @click.pass_context
29
+ def add_(ctx, account, scope_name):
30
+ """Add a new scope with name [SCOPE-NAME]"""
31
+ args = Arguments({"scope": scope_name, "account": account})
32
+ add_scope(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
33
+
34
+
35
+ @scope.command("list")
36
+ @click.option("-a", "--account", help="Filter by associated account", required=False)
37
+ @click.pass_context
38
+ def list_(ctx, account):
39
+ """List existing scopes"""
40
+ list_scopes(Arguments({"account": account}), ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
@@ -0,0 +1,73 @@
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
+ import click
15
+
16
+ from rucio.cli.bin_legacy.rucio_admin import add_subscription, list_subscriptions, reevaluate_did_for_subscription, update_subscription
17
+ from rucio.cli.utils import Arguments
18
+
19
+
20
+ @click.group()
21
+ def subscription():
22
+ "The methods for automated and regular processing of some specific rules"
23
+
24
+
25
+ @subscription.command("show")
26
+ @click.option("-a", "--account", help="Account associated with the subscription")
27
+ @click.option("--long", default=False, is_flag=True, help="Show extended information about the subscription")
28
+ @click.argument("subscription-name")
29
+ @click.pass_context
30
+ def list_(ctx, subscription_name, account, long):
31
+ """Show the attributes of a subscription [SUBSCRIPTION-NAME]"""
32
+ args = Arguments({"subs_account": account, "name": subscription_name, "long": long})
33
+ list_subscriptions(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
34
+
35
+
36
+ @subscription.command("update")
37
+ @click.argument("subscription-name")
38
+ @click.option("--filter", "did_filter", help='Json serializable DID filter (eg \'{"scope": ["tests"], "project": ["data12_8TeV"]}\')', required=True)
39
+ @click.option("--rule", help='List of replication rules (eg \'[{"activity": "Functional Tests", "copies": 2, "rse_expression": "tier=2", "lifetime": 3600, "weight": "mou"}]\')', required=True)
40
+ @click.option("--comment", help="Comments on subscription")
41
+ @click.option("--lifetime", type=int, help="Subscription lifetime (in days)")
42
+ @click.option("--account", help="Account name")
43
+ @click.option("--priority", help="The priority of the subscription")
44
+ @click.pass_context
45
+ def update(ctx, subscription_name, did_filter, rule, comment, lifetime, account, priority):
46
+ """Update a subscription [SUBSCRIPTION-NAME] to have new properties"""
47
+ args = Arguments({"name": subscription_name, "filter": did_filter, "replication_rules": rule, "comments": comment, "lifetime": lifetime, "subs_account": account, "priority": priority})
48
+ update_subscription(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
49
+
50
+
51
+ @subscription.command("add")
52
+ @click.argument("subscription-name")
53
+ @click.option("--filter", "did_filter", help='Json serializable DID filter (eg \'{"scope": ["tests"], "project": ["data12_8TeV"]}\')', required=True)
54
+ @click.option("--rule", help='List of replication rules (eg \'[{"activity": "Functional Tests", "copies": 2, "rse_expression": "tier=2", "lifetime": 3600, "weight": "mou"}]\')', required=True)
55
+ @click.option("--comment", help="Comments on subscription")
56
+ @click.option("--lifetime", type=int, help="Subscription lifetime (in days)")
57
+ @click.option("--account", help="Account name")
58
+ @click.option("--priority", help="The priority of the subscription")
59
+ @click.pass_context
60
+ def add_(ctx, subscription_name, did_filter, rule, comment, lifetime, account, priority):
61
+ """Create a new subscription with the name [SUBSCRIPTION-NAME]"""
62
+ args = Arguments({"name": subscription_name, "filter": did_filter, "replication_rules": rule, "comments": comment, "lifetime": lifetime, "subs_account": account, "priority": priority})
63
+ add_subscription(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
64
+
65
+
66
+ @subscription.command("touch")
67
+ @click.argument("dids", nargs=-1)
68
+ @click.pass_context
69
+ def touch(ctx, dids):
70
+ """Reevaluate list of DIDs against all active subscriptions"""
71
+ # TODO make reeval accept dids as a list
72
+ dids = ",".join(dids)
73
+ reevaluate_did_for_subscription(Arguments({"dids": dids}), ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
rucio/cli/upload.py ADDED
@@ -0,0 +1,60 @@
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
+ import click
15
+
16
+ from rucio.cli.bin_legacy.rucio import upload
17
+ from rucio.cli.utils import Arguments
18
+ from rucio.common.config import config_get_float
19
+
20
+
21
+ @click.command("upload")
22
+ @click.argument("file-paths", nargs=-1)
23
+ @click.option("--rse", "--rse-name", help="Rucio Storage Element (RSE) name", required=True)
24
+ @click.option("--lifetime", type=int, help="Lifetime of the rule in seconds")
25
+ @click.option("--expiration-date", help="The date when the rule expires in UTC, format: <year>-<month>-<day>-<hour>:<minute>:<second>. E.g. 2022-10-20-20:00:00")
26
+ @click.option("--scope", help="Scope name.")
27
+ @click.option("--impl", type=click.Choice([]), help="Transfer protocol implementation to use (e.g: xrootd, gfal.NoRename, webdav, ssh.Rsync, rclone)")
28
+ # The --no-register option is hidden. This is pilot ONLY. Users should not use this. Will lead to unregistered data on storage!
29
+ @click.option("--no-register", is_flag=True, default=False, hidden=True)
30
+ @click.option("--register-after-upload", is_flag=True, default=False, help="Register the file _only_ after successful upload")
31
+ @click.option("--summary", is_flag=True, default=False, help="Create rucio_upload.json summary file")
32
+ @click.option("--guid", help="Manually specify the GUID for the file.")
33
+ @click.option("--protocol", help="Force the protocol to use")
34
+ @click.option("--pfn", help="Specify the exact PFN for the upload")
35
+ @click.option("--lfn", help="Specify the exact LFN for the upload")
36
+ @click.option("--transfer-timeout", type=float, default=config_get_float("upload", "transfer_timeout", False, 360), help="Transfer timeout (in seconds)")
37
+ @click.option("-r", "--recursive", is_flag=True, default=False, help="Convert recursively the folder structure into collections")
38
+ @click.pass_context
39
+ def upload_command(ctx, file_paths, rse, lifetime, expiration_date, scope, impl, no_register, register_after_upload, summary, guid, protocol, pfn, lfn, transfer_timeout, recursive):
40
+ """Upload file(s) to a Rucio RSE"""
41
+ args = Arguments(
42
+ {
43
+ "args": file_paths,
44
+ "rse": rse,
45
+ "lifetime": lifetime,
46
+ "expiration_date": expiration_date,
47
+ "scope": scope,
48
+ "impl": impl,
49
+ "no_register": no_register,
50
+ "register_after_upload": register_after_upload,
51
+ "protocol": protocol,
52
+ "summary": summary,
53
+ "guid": guid,
54
+ "pfn": pfn,
55
+ "name": lfn,
56
+ "transfer_timeout": transfer_timeout,
57
+ "recursive": recursive,
58
+ }
59
+ )
60
+ upload(args, ctx.obj.client, ctx.obj.logger, ctx.obj.console, ctx.obj.spinner)
rucio/cli/utils.py ADDED
@@ -0,0 +1,226 @@
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
+ import errno
15
+ import logging
16
+ import os
17
+ import signal
18
+ import subprocess
19
+ import sys
20
+ import traceback
21
+ from configparser import NoOptionError, NoSectionError
22
+ from functools import wraps
23
+
24
+ from rucio.client.client import Client
25
+ from rucio.common.config import config_get
26
+ from rucio.common.exception import (
27
+ AccessDenied,
28
+ CannotAuthenticate,
29
+ DataIdentifierAlreadyExists,
30
+ DataIdentifierNotFound,
31
+ Duplicate,
32
+ DuplicateContent,
33
+ InvalidObject,
34
+ InvalidRSEExpression,
35
+ MissingDependency,
36
+ RSENotFound,
37
+ RucioException,
38
+ RuleNotFound,
39
+ UnsupportedOperation,
40
+ )
41
+ from rucio.common.utils import setup_logger
42
+
43
+ SUCCESS = 0
44
+ FAILURE = 1
45
+
46
+
47
+ def exception_handler(function):
48
+ verbosity = ("-v" in sys.argv) or ("--verbose" in sys.argv)
49
+ logger = setup_logger(module_name=__name__, logger_name="user", verbose=verbosity)
50
+
51
+ @wraps(function)
52
+ def new_funct(*args, **kwargs):
53
+ try:
54
+ return function(*args, **kwargs)
55
+ except NotImplementedError as error:
56
+ logger.error(f"Cannot run that operation/command combination {error}")
57
+ return FAILURE
58
+ except InvalidObject as error:
59
+ logger.error(error)
60
+ return error.error_code
61
+ except DataIdentifierNotFound as error:
62
+ logger.error(error)
63
+ logger.debug("This means that the Data IDentifier you provided is not known by Rucio.")
64
+ return error.error_code
65
+ except AccessDenied as error:
66
+ logger.error(error)
67
+ logger.debug("This error is a permission issue. You cannot run this command with your account.")
68
+ return error.error_code
69
+ except DataIdentifierAlreadyExists as error:
70
+ logger.error(error)
71
+ logger.debug("This means that the Data IDentifier you try to add is already registered in Rucio.")
72
+ return error.error_code
73
+ except RSENotFound as error:
74
+ logger.error(error)
75
+ logger.debug("This means that the Rucio Storage Element you provided is not known by Rucio.")
76
+ return error.error_code
77
+ except InvalidRSEExpression as error:
78
+ logger.error(error)
79
+ logger.debug("This means the RSE expression you provided is not syntactically correct.")
80
+ return error.error_code
81
+ except DuplicateContent as error:
82
+ logger.error(error)
83
+ logger.debug("This means that the DID you want to attach is already in the target DID.")
84
+ return error.error_code
85
+ except Duplicate as error:
86
+ logger.error(error)
87
+ logger.debug("This means that you are trying to add something that already exists.")
88
+ return error.error_code
89
+ except TypeError as error:
90
+ logger.error(error)
91
+ logger.debug("This means the parameter you passed has a wrong type.")
92
+ return FAILURE
93
+ except RuleNotFound as error:
94
+ logger.error(error)
95
+ logger.debug("This means the rule you specified does not exist.")
96
+ return error.error_code
97
+ except UnsupportedOperation as error:
98
+ logger.error(error)
99
+ logger.debug("This means you cannot change the status of the DID.")
100
+ return error.error_code
101
+ except MissingDependency as error:
102
+ logger.error(error)
103
+ logger.debug("This means one dependency is missing.")
104
+ return error.error_code
105
+ except KeyError as error:
106
+ if "x-rucio-auth-token" in str(error):
107
+ used_account = None
108
+ try: # get the configured account from the configuration file
109
+ used_account = "%s (from rucio.cfg)" % config_get("client", "account")
110
+ except Exception:
111
+ pass
112
+ try: # are we overriden by the environment?
113
+ used_account = "%s (from RUCIO_ACCOUNT)" % os.environ["RUCIO_ACCOUNT"]
114
+ except Exception:
115
+ pass
116
+ logger.error("Specified account %s does not have an associated identity." % used_account)
117
+
118
+ else:
119
+ logger.debug(traceback.format_exc())
120
+ contact = config_get("policy", "support", raise_exception=False)
121
+ support = ("Please follow up with all relevant information at: " + contact) if contact else ""
122
+ logger.error("\nThe object is missing this property: %s\n" 'This should never happen. Please rerun the last command with the "-v" option to gather more information.\n' "%s" % (str(error), support))
123
+ return FAILURE
124
+ except RucioException as error:
125
+ logger.error(error)
126
+ return error.error_code
127
+ except Exception as error:
128
+ if isinstance(error, IOError) and getattr(error, "errno", None) == errno.EPIPE:
129
+ # Ignore Broken Pipe
130
+ # While in python3 we can directly catch 'BrokenPipeError', in python2 it doesn't exist.
131
+
132
+ # Python flushes standard streams on exit; redirect remaining output
133
+ # to devnull to avoid another BrokenPipeError at shutdown
134
+ devnull = os.open(os.devnull, os.O_WRONLY)
135
+ os.dup2(devnull, sys.stdout.fileno())
136
+ return SUCCESS
137
+ logger.debug(traceback.format_exc())
138
+ logger.error(error)
139
+ contact = config_get("policy", "support", raise_exception=False)
140
+ support = ("If it's a problem concerning your experiment or if you're unsure what to do, please follow up at: %s\n" % contact) if contact else ""
141
+ contact = config_get("policy", "support_rucio", default="https://github.com/rucio/rucio/issues")
142
+ support += "If you're sure there is a problem with Rucio itself, please follow up at: " + contact
143
+ logger.error("\nRucio exited with an unexpected/unknown error.\n" 'Please rerun the last command with the "-v" option to gather more information.\n' "%s" % support)
144
+ return FAILURE
145
+
146
+ return new_funct
147
+
148
+
149
+ def get_client(args, logger):
150
+ """
151
+ Returns a new client object.
152
+ """
153
+ if hasattr(args, "config") and (args.config is not None):
154
+ os.environ["RUCIO_CONFIG"] = args.config
155
+
156
+ if logger is None:
157
+ logger = setup_logger(module_name=__name__, logger_name="user", verbose=args.verbose)
158
+
159
+ if not args.auth_strategy:
160
+ if "RUCIO_AUTH_TYPE" in os.environ:
161
+ auth_type = os.environ["RUCIO_AUTH_TYPE"].lower()
162
+ else:
163
+ try:
164
+ auth_type = config_get("client", "auth_type").lower()
165
+ except (NoOptionError, NoSectionError):
166
+ logger.error("Cannot get AUTH_TYPE")
167
+ sys.exit(1)
168
+ else:
169
+ auth_type = args.auth_strategy.lower()
170
+
171
+ if auth_type in ["userpass", "saml"] and args.username is not None and args.password is not None:
172
+ creds = {"username": args.username, "password": args.password}
173
+ elif auth_type == "oidc":
174
+ if args.oidc_issuer:
175
+ args.oidc_issuer = args.oidc_issuer.lower()
176
+ creds = {
177
+ "oidc_auto": args.oidc_auto,
178
+ "oidc_scope": args.oidc_scope,
179
+ "oidc_audience": args.oidc_audience,
180
+ "oidc_polling": args.oidc_polling,
181
+ "oidc_refresh_lifetime": args.oidc_refresh_lifetime,
182
+ "oidc_issuer": args.oidc_issuer,
183
+ "oidc_username": args.oidc_username,
184
+ "oidc_password": args.oidc_password,
185
+ }
186
+ elif auth_type == "x509":
187
+ creds = {"client_cert": args.certificate, "client_key": args.client_key}
188
+ else:
189
+ creds = None
190
+
191
+ try:
192
+ client = Client(rucio_host=args.host, auth_host=args.auth_host, account=args.issuer, auth_type=auth_type, creds=creds, ca_cert=args.ca_certificate, timeout=args.timeout, user_agent=args.user_agent, vo=args.vo, logger=logger)
193
+ except CannotAuthenticate as error:
194
+ logger.error(error)
195
+ if "alert certificate expired" in str(error):
196
+ logger.error("The server certificate expired.")
197
+ elif auth_type.lower() == "x509_proxy":
198
+ logger.error("Please verify that your proxy is still valid and renew it if needed.")
199
+ sys.exit(1)
200
+ return client
201
+
202
+
203
+ def signal_handler(sig, frame, logger):
204
+ logger.warning("You pressed Ctrl+C! Exiting gracefully")
205
+ child_processes = subprocess.Popen("ps -o pid --ppid %s --noheaders" % os.getpid(), shell=True, stdout=subprocess.PIPE)
206
+ child_processes = child_processes.stdout.read() # type: ignore
207
+ for pid in child_processes.split("\n")[:-1]: # type: ignore
208
+ try:
209
+ os.kill(int(pid), signal.SIGTERM)
210
+ except Exception:
211
+ print("Cannot kill child process")
212
+ sys.exit(1)
213
+
214
+
215
+ def setup_gfal2_logger():
216
+ gfal2_logger = logging.getLogger("gfal2")
217
+ gfal2_logger.setLevel(logging.CRITICAL)
218
+ gfal2_logger.addHandler(logging.StreamHandler())
219
+
220
+
221
+ class Arguments(dict):
222
+ """dot.notation access to dictionary attributes"""
223
+
224
+ __getattr__ = dict.get
225
+ __setattr__ = dict.__setitem__
226
+ __delattr__ = dict.__delitem__
@@ -200,7 +200,6 @@ class AccountClient(BaseClient):
200
200
  :param account: The account name.
201
201
  :param identity: The identity key name. For example x509 DN, or a username.
202
202
  :param authtype: The type of the authentication (x509, gss, userpass).
203
- :param default: If True, the account should be used by default with the provided identity.
204
203
  """
205
204
 
206
205
  data = dumps({'identity': identity, 'authtype': authtype})
@@ -22,13 +22,11 @@ import os
22
22
  import secrets
23
23
  import sys
24
24
  import time
25
- from collections.abc import Generator
26
25
  from configparser import NoOptionError, NoSectionError
27
- from logging import Logger
28
26
  from os import environ, fdopen, geteuid, makedirs, path
29
27
  from shutil import move
30
28
  from tempfile import mkstemp
31
- from typing import Any, Optional
29
+ from typing import TYPE_CHECKING, Any, Optional
32
30
  from urllib.parse import urlparse
33
31
 
34
32
  import requests
@@ -40,10 +38,14 @@ from requests.status_codes import codes
40
38
  from rucio import version
41
39
  from rucio.common import exception
42
40
  from rucio.common.config import config_get, config_get_bool, config_get_int
43
- from rucio.common.exception import CannotAuthenticate, ClientProtocolNotSupported, ConfigNotFound, MissingClientParameter, MissingModuleException, NoAuthInformation, ServerConnectionException
41
+ from rucio.common.exception import CannotAuthenticate, ClientProtocolNotFound, ClientProtocolNotSupported, ConfigNotFound, MissingClientParameter, MissingModuleException, NoAuthInformation, ServerConnectionException
44
42
  from rucio.common.extra import import_extras
45
43
  from rucio.common.utils import build_url, get_tmp_dir, my_key_generator, parse_response, setup_logger, ssh_sign
46
44
 
45
+ if TYPE_CHECKING:
46
+ from collections.abc import Generator
47
+ from logging import Logger
48
+
47
49
  EXTRA_MODULES = import_extras(['requests_kerberos'])
48
50
 
49
51
  if EXTRA_MODULES['requests_kerberos']:
@@ -90,7 +92,7 @@ class BaseClient:
90
92
  timeout: Optional[int] = 600,
91
93
  user_agent: Optional[str] = 'rucio-clients',
92
94
  vo: Optional[str] = None,
93
- logger: Logger = LOG) -> None:
95
+ logger: 'Logger' = LOG) -> None:
94
96
  """
95
97
  Constructor of the BaseClient.
96
98
  :param rucio_host: The address of the rucio server, if None it is read from the config file.
@@ -155,11 +157,18 @@ class BaseClient:
155
157
  rucio_scheme = urlparse(self.host).scheme
156
158
  auth_scheme = urlparse(self.auth_host).scheme
157
159
 
158
- if rucio_scheme != 'http' and rucio_scheme != 'https':
159
- raise ClientProtocolNotSupported('\'%s\' not supported' % rucio_scheme)
160
+ rucio_scheme_allowed = ['http', 'https']
161
+ auth_scheme_allowed = ['http', 'https']
162
+
163
+ if not rucio_scheme:
164
+ raise ClientProtocolNotFound(host=self.host, protocols_allowed=rucio_scheme_allowed)
165
+ elif rucio_scheme not in rucio_scheme_allowed:
166
+ raise ClientProtocolNotSupported(host=self.host, protocol=rucio_scheme, protocols_allowed=rucio_scheme_allowed)
160
167
 
161
- if auth_scheme != 'http' and auth_scheme != 'https':
162
- raise ClientProtocolNotSupported('\'%s\' not supported' % auth_scheme)
168
+ if not auth_scheme:
169
+ raise ClientProtocolNotFound(host=self.auth_host, protocols_allowed=auth_scheme_allowed)
170
+ elif auth_scheme not in auth_scheme_allowed:
171
+ raise ClientProtocolNotSupported(host=self.auth_host, protocol=auth_scheme, protocols_allowed=auth_scheme_allowed)
163
172
 
164
173
  if (rucio_scheme == 'https' or auth_scheme == 'https') and ca_cert is None:
165
174
  self.logger.debug('HTTPS is required, but no ca_cert was passed. Trying to get it from X509_CERT_DIR.')
@@ -364,7 +373,7 @@ class BaseClient:
364
373
  else:
365
374
  return exception.RucioException, "%s: %s" % (exc_cls, exc_msg)
366
375
 
367
- def _load_json_data(self, response: requests.Response) -> Generator[Any, Any, Any]:
376
+ def _load_json_data(self, response: requests.Response) -> 'Generator[Any, Any, Any]':
368
377
  """
369
378
  Helper method to correctly load json data based on the content type of the http response.
370
379
 
@@ -517,7 +526,7 @@ class BaseClient:
517
526
  self.auth_token = result.headers['x-rucio-auth-token']
518
527
  return True
519
528
 
520
- def __refresh_token_OIDC(self) -> bool:
529
+ def __refresh_token_oidc(self) -> bool:
521
530
  """
522
531
  Checks if there is active refresh token and if so returns
523
532
  either active token with expiration timestamp or requests a new
@@ -571,7 +580,7 @@ class BaseClient:
571
580
  \nRucio Auth Server when attempting token refresh.")
572
581
  return False
573
582
 
574
- def __get_token_OIDC(self) -> bool:
583
+ def __get_token_oidc(self) -> bool:
575
584
  """
576
585
  First authenticates the user via a Identity Provider server
577
586
  (with user's username & password), by specifying oidc_scope,
@@ -600,14 +609,14 @@ class BaseClient:
600
609
  request_auth_url = build_url(self.auth_host, path='auth/oidc')
601
610
  # requesting authorization URL specific to the user & Rucio OIDC Client
602
611
  self.logger.debug("Initial auth URL request headers %s to files" % str(headers))
603
- OIDC_auth_res = self._send_request(request_auth_url, headers=headers, get_token=True)
604
- self.logger.debug("Response headers %s and text %s" % (str(OIDC_auth_res.headers), str(OIDC_auth_res.text)))
612
+ oidc_auth_res = self._send_request(request_auth_url, headers=headers, get_token=True)
613
+ self.logger.debug("Response headers %s and text %s" % (str(oidc_auth_res.headers), str(oidc_auth_res.text)))
605
614
  # with the obtained authorization URL we will contact the Identity Provider to get to the login page
606
- if 'X-Rucio-OIDC-Auth-URL' not in OIDC_auth_res.headers:
615
+ if 'X-Rucio-OIDC-Auth-URL' not in oidc_auth_res.headers:
607
616
  print("Rucio Client did not succeed to get AuthN/Z URL from the Rucio Auth Server. \
608
617
  \nThis could be due to wrongly requested/configured scope, audience or issuer.")
609
618
  return False
610
- auth_url = OIDC_auth_res.headers['X-Rucio-OIDC-Auth-URL']
619
+ auth_url = oidc_auth_res.headers['X-Rucio-OIDC-Auth-URL']
611
620
  if not self.creds['oidc_auto']:
612
621
  print("\nPlease use your internet browser, go to:")
613
622
  print("\n " + auth_url + " \n")
@@ -692,7 +701,7 @@ class BaseClient:
692
701
  with fdopen(file_d, "w") as f_exp_epoch:
693
702
  f_exp_epoch.write(str(self.token_exp_epoch))
694
703
  move(file_n, self.token_exp_epoch_file)
695
- self.__refresh_token_OIDC()
704
+ self.__refresh_token_oidc()
696
705
  return True
697
706
 
698
707
  def __get_token_x509(self) -> bool:
@@ -832,11 +841,11 @@ class BaseClient:
832
841
  url = build_url(self.auth_host, path='auth/saml')
833
842
 
834
843
  result = None
835
- SAML_auth_result = self._send_request(url, get_token=True)
836
- if SAML_auth_result.headers['X-Rucio-Auth-Token']:
837
- return SAML_auth_result.headers['X-Rucio-Auth-Token']
838
- SAML_auth_url = SAML_auth_result.headers['X-Rucio-SAML-Auth-URL']
839
- result = self._send_request(SAML_auth_url, type_='POST', data=userpass, verify=False)
844
+ saml_auth_result = self._send_request(url, get_token=True)
845
+ if saml_auth_result.headers['X-Rucio-Auth-Token']:
846
+ return saml_auth_result.headers['X-Rucio-Auth-Token']
847
+ saml_auth_url = saml_auth_result.headers['X-Rucio-SAML-Auth-URL']
848
+ result = self._send_request(saml_auth_url, type_='POST', data=userpass, verify=False)
840
849
  result = self._send_request(url, get_token=True)
841
850
 
842
851
  if not result:
@@ -868,7 +877,7 @@ class BaseClient:
868
877
  raise CannotAuthenticate('x509 authentication failed for account=%s with identity=%s' % (self.account,
869
878
  self.creds))
870
879
  elif self.auth_type == 'oidc':
871
- if not self.__get_token_OIDC():
880
+ if not self.__get_token_oidc():
872
881
  raise CannotAuthenticate('OIDC authentication failed for account=%s' % self.account)
873
882
 
874
883
  elif self.auth_type == 'gss':
@@ -912,7 +921,7 @@ class BaseClient:
912
921
  except Exception:
913
922
  raise
914
923
  if self.auth_oidc_refresh_active and self.auth_type == 'oidc':
915
- self.__refresh_token_OIDC()
924
+ self.__refresh_token_oidc()
916
925
  self.logger.debug('got token from file')
917
926
  return True
918
927
 
rucio/client/client.py CHANGED
@@ -57,7 +57,51 @@ class Client(AccountClient,
57
57
  DiracClient,
58
58
  LifetimeClient):
59
59
 
60
- """Main client class for accessing Rucio resources. Handles the authentication."""
60
+ """
61
+ Main client class for accessing Rucio resources. Handles the authentication.
62
+
63
+ Note:
64
+ Used to access all client methods. Each entity client *can* be used to access methods, but using the main client class is recommended for ease of use.
65
+
66
+ For using general methods -
67
+
68
+
69
+ ```
70
+ from rucio.client import Client
71
+
72
+ client = Client() # authenticate with config or environ settings
73
+ client.add_replication_rule(...)
74
+
75
+ client = Client(
76
+ rucio_host = "my_host",
77
+ auth_host = "my_auth_host",
78
+ account = "jdoe12345",
79
+ auth_type = "userpass",
80
+ creds = {
81
+ "username": "jdoe12345",
82
+ "password": "******",
83
+ }
84
+ ) # authenticate with kwargs
85
+ client.list_replicas(...)
86
+ ```
87
+
88
+ For using the upload and download clients -
89
+
90
+ ```
91
+ from rucio.client import Client
92
+ from rucio.client.uploadclient import UploadClient
93
+ from rucio.client.downloadclient import DownloadClient
94
+
95
+ client = Client(...) # Initialize a client using your preferred method
96
+
97
+ upload_client = UploadClient(client) # Pass forward the initialized client
98
+ upload_client.upload(items=...)
99
+
100
+ download_client = DownloadClient(client)
101
+ download_client.download_dids(items=...)
102
+ ```
103
+
104
+ """
61
105
 
62
106
  def __init__(self, **args):
63
107
  """
rucio/client/didclient.py CHANGED
@@ -538,14 +538,16 @@ class DIDClient(BaseClient):
538
538
  def get_metadata_bulk(
539
539
  self,
540
540
  dids: "Sequence[Mapping[str, Any]]",
541
- inherit: bool = False
541
+ inherit: bool = False,
542
+ plugin: str = "JSON",
542
543
  ) -> "Iterator[dict[str, Any]]":
543
544
  """
544
545
  Bulk get data identifier metadata
545
- :param inherit: A boolean. If set to true, the metadata of the parent are concatenated.
546
546
  :param dids: A list of dids.
547
+ :param inherit: A boolean. If set to true, the metadata of the parent are concatenated.
548
+ :param plugin: The metadata plugin to query, 'ALL' for all available plugins
547
549
  """
548
- data = {'dids': dids, 'inherit': inherit}
550
+ data = {'dids': dids, 'inherit': inherit, 'plugin': plugin}
549
551
  path = '/'.join([self.DIDS_BASEURL, 'bulkmeta'])
550
552
  url = build_url(choice(self.list_hosts), path=path)
551
553
  r = self._send_request(url, type_='POST', data=dumps(data))