xrdclient 0.1.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.
Files changed (119) hide show
  1. xrdclient/__init__.py +249 -0
  2. xrdclient/_compat.py +86 -0
  3. xrdclient/_log.py +96 -0
  4. xrdclient/aio.py +1040 -0
  5. xrdclient/auth/__init__.py +399 -0
  6. xrdclient/auth/base.py +107 -0
  7. xrdclient/auth/gsi.py +678 -0
  8. xrdclient/auth/kerberos/__init__.py +22 -0
  9. xrdclient/auth/kerberos/asn1.py +524 -0
  10. xrdclient/auth/kerberos/caches.py +99 -0
  11. xrdclient/auth/kerberos/ccache.py +303 -0
  12. xrdclient/auth/kerberos/kcm.py +254 -0
  13. xrdclient/auth/kerberos/kdc.py +152 -0
  14. xrdclient/auth/kerberos/keyring.py +334 -0
  15. xrdclient/auth/kerberos/model.py +119 -0
  16. xrdclient/auth/kerberos/profile.py +256 -0
  17. xrdclient/auth/kerberos/tgs.py +234 -0
  18. xrdclient/auth/krb5.py +272 -0
  19. xrdclient/auth/prompt.py +196 -0
  20. xrdclient/auth/simple.py +51 -0
  21. xrdclient/auth/sss.py +251 -0
  22. xrdclient/auth/ztn.py +206 -0
  23. xrdclient/cli/__init__.py +214 -0
  24. xrdclient/cli/cp.py +598 -0
  25. xrdclient/cli/fs.py +721 -0
  26. xrdclient/client/__init__.py +9 -0
  27. xrdclient/client/_fattr.py +70 -0
  28. xrdclient/client/_zip.py +203 -0
  29. xrdclient/client/bulk.py +494 -0
  30. xrdclient/client/file.py +1461 -0
  31. xrdclient/client/filesystem.py +1252 -0
  32. xrdclient/compat/__init__.py +59 -0
  33. xrdclient/compat/client/__init__.py +86 -0
  34. xrdclient/compat/client/_args.py +67 -0
  35. xrdclient/compat/client/_channels.py +54 -0
  36. xrdclient/compat/client/_convert.py +275 -0
  37. xrdclient/compat/client/_dispatch.py +217 -0
  38. xrdclient/compat/client/_status.py +171 -0
  39. xrdclient/compat/client/_version.py +7 -0
  40. xrdclient/compat/client/copyprocess.py +489 -0
  41. xrdclient/compat/client/env.py +455 -0
  42. xrdclient/compat/client/file.py +557 -0
  43. xrdclient/compat/client/filesystem.py +599 -0
  44. xrdclient/compat/client/finalize.py +30 -0
  45. xrdclient/compat/client/flags.py +174 -0
  46. xrdclient/compat/client/glob_funcs.py +89 -0
  47. xrdclient/compat/client/responses.py +458 -0
  48. xrdclient/compat/client/tape.py +393 -0
  49. xrdclient/compat/client/url.py +114 -0
  50. xrdclient/compat/client/utils.py +81 -0
  51. xrdclient/compat/client/xattr.py +53 -0
  52. xrdclient/config.py +538 -0
  53. xrdclient/copy/__init__.py +28 -0
  54. xrdclient/copy/engine.py +1300 -0
  55. xrdclient/copy/limits.py +149 -0
  56. xrdclient/copy/replicas.py +270 -0
  57. xrdclient/copy/tpc.py +290 -0
  58. xrdclient/crypto/__init__.py +124 -0
  59. xrdclient/crypto/aes.py +238 -0
  60. xrdclient/crypto/blowfish.py +188 -0
  61. xrdclient/crypto/checksum.py +122 -0
  62. xrdclient/crypto/crc32c.py +136 -0
  63. xrdclient/crypto/crc64.py +58 -0
  64. xrdclient/crypto/delegation.py +281 -0
  65. xrdclient/crypto/der.py +234 -0
  66. xrdclient/crypto/rfc3961.py +340 -0
  67. xrdclient/crypto/rsa.py +412 -0
  68. xrdclient/crypto/sigver.py +191 -0
  69. xrdclient/crypto/trust.py +139 -0
  70. xrdclient/crypto/x509.py +431 -0
  71. xrdclient/doctor.py +416 -0
  72. xrdclient/easy.py +391 -0
  73. xrdclient/errors.py +366 -0
  74. xrdclient/flags.py +453 -0
  75. xrdclient/fsspec_impl.py +499 -0
  76. xrdclient/http/__init__.py +35 -0
  77. xrdclient/http/client.py +654 -0
  78. xrdclient/http/dav.py +744 -0
  79. xrdclient/http/file.py +493 -0
  80. xrdclient/http/tape.py +256 -0
  81. xrdclient/http/tpc.py +341 -0
  82. xrdclient/io/__init__.py +207 -0
  83. xrdclient/io/raw.py +222 -0
  84. xrdclient/path.py +303 -0
  85. xrdclient/proto/__init__.py +25 -0
  86. xrdclient/proto/buffer.py +174 -0
  87. xrdclient/proto/constants.py +320 -0
  88. xrdclient/proto/frames.py +164 -0
  89. xrdclient/proto/machine.py +945 -0
  90. xrdclient/proto/requests.py +1011 -0
  91. xrdclient/proto/responses.py +675 -0
  92. xrdclient/py.typed +0 -0
  93. xrdclient/s3/__init__.py +34 -0
  94. xrdclient/s3/fs.py +837 -0
  95. xrdclient/s3/sigv4.py +219 -0
  96. xrdclient/session/__init__.py +9 -0
  97. xrdclient/session/bulk.py +639 -0
  98. xrdclient/session/deadline.py +93 -0
  99. xrdclient/session/pool.py +345 -0
  100. xrdclient/session/router.py +732 -0
  101. xrdclient/session/sync.py +800 -0
  102. xrdclient/testing/__init__.py +35 -0
  103. xrdclient/testing/__main__.py +13 -0
  104. xrdclient/testing/faults.py +363 -0
  105. xrdclient/testing/http.py +724 -0
  106. xrdclient/testing/s3.py +522 -0
  107. xrdclient/testing/server.py +1482 -0
  108. xrdclient/transport/__init__.py +27 -0
  109. xrdclient/transport/base.py +84 -0
  110. xrdclient/transport/memory.py +79 -0
  111. xrdclient/transport/sync.py +95 -0
  112. xrdclient/types.py +425 -0
  113. xrdclient/url.py +363 -0
  114. xrdclient-0.1.0.dist-info/METADATA +387 -0
  115. xrdclient-0.1.0.dist-info/RECORD +119 -0
  116. xrdclient-0.1.0.dist-info/WHEEL +4 -0
  117. xrdclient-0.1.0.dist-info/entry_points.txt +11 -0
  118. xrdclient-0.1.0.dist-info/licenses/COPYING +674 -0
  119. xrdclient-0.1.0.dist-info/licenses/LICENSE +165 -0
xrdclient/__init__.py ADDED
@@ -0,0 +1,249 @@
1
+ """A pure-Python XRootD client.
2
+
3
+ Speaks the native ``root://`` binary protocol and HTTP/WebDAV, with no
4
+ compiled extension and no XRootD installation required.
5
+
6
+ >>> import xrdclient
7
+ >>> with xrdclient.open("root://eos.example.org//store/data.root") as f:
8
+ ... header = f.read(1024)
9
+
10
+ The five levels of the API, from most to least convenient:
11
+
12
+ :mod:`xrdclient.easy`
13
+ One-line verbs on a URL - ``xrdclient.ls``, ``xrdclient.size``, ``xrdclient.read_text`` -
14
+ for when there is one question to ask and no reason to build anything.
15
+ ``xrdclient.open`` / :class:`~xrdclient.path.XRootDPath`
16
+ File objects and ``pathlib`` semantics.
17
+ :class:`~xrdclient.client.FileSystem` / :class:`~xrdclient.client.File`
18
+ Explicit per-operation control, sync or async.
19
+ :class:`~xrdclient.session.Session`
20
+ A single authenticated connection.
21
+ :mod:`xrdclient.proto`
22
+ The sans-io protocol machinery.
23
+
24
+ Three packages build on this one and are installed separately:
25
+ `xrdroot <https://github.com/rob-c/xrdroot>`_ reads and writes the ROOT file
26
+ format over any URL here, `xrdml <https://github.com/rob-c/xrdml>`_ hands a
27
+ file of rows to PyTorch a minibatch at a time, and
28
+ `xrddatasets <https://github.com/rob-c/xrddatasets>`_ converts open data into
29
+ ROOT files and publishes the catalogue they are served from.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from typing import TYPE_CHECKING
35
+
36
+ from .client import Checkpoint, File, FileSystem
37
+ from .config import Config, configure, current, find_config_file, override
38
+ from .copy import CopyResult, SyncMode, copy, copy_tree, third_party
39
+ from .easy import (
40
+ checksum,
41
+ exists,
42
+ glob,
43
+ is_online,
44
+ ls,
45
+ mkdir,
46
+ move,
47
+ read_bytes,
48
+ read_text,
49
+ remove,
50
+ size,
51
+ stage,
52
+ stat,
53
+ write_bytes,
54
+ write_text,
55
+ )
56
+ from .errors import (
57
+ AttrNotFoundError,
58
+ AuthenticationError,
59
+ BusyError,
60
+ ChecksumMismatchError,
61
+ ConnectionError,
62
+ CredentialError,
63
+ InvalidArgumentError,
64
+ NoMechanismError,
65
+ NoSpaceError,
66
+ NotFoundError,
67
+ PageIntegrityError,
68
+ ProtocolError,
69
+ QuotaError,
70
+ ReadOnlyError,
71
+ RedirectLimitError,
72
+ ServerError,
73
+ ServerTimeoutError,
74
+ TimeoutError,
75
+ TLSRequiredError,
76
+ TokenExpiredError,
77
+ TooLargeError,
78
+ TransientError,
79
+ UnsupportedError,
80
+ WaitLimitError,
81
+ XRootDError,
82
+ )
83
+ from .flags import (
84
+ Access,
85
+ DirListFlags,
86
+ LocateFlags,
87
+ MkDirFlags,
88
+ OpenFlags,
89
+ PrepareFlags,
90
+ QueryCode,
91
+ StatInfoFlags,
92
+ )
93
+ from .io import open_url as open
94
+ from .path import XRootDPath
95
+ from .path import XRootDPath as Path # ``xrdclient.Path`` reads the way pathlib does
96
+ from .session.deadline import OperationExpiredError, deadline
97
+ from .types import (
98
+ CheckpointInfo,
99
+ ChecksumInfo,
100
+ CloneRange,
101
+ DirEntry,
102
+ LocationInfo,
103
+ PageResult,
104
+ PrepareStatus,
105
+ ProtocolInfo,
106
+ ReadRange,
107
+ SpaceInfo,
108
+ StatInfo,
109
+ VFSInfo,
110
+ WriteChunk,
111
+ human_bytes,
112
+ )
113
+ from .url import XRootDURL, parse
114
+
115
+ if TYPE_CHECKING: # bound at runtime by ``__getattr__`` below, named here so
116
+ # that a type checker and the documentation can both see what they are
117
+ from .doctor import Check, Report, diagnose
118
+ del TYPE_CHECKING # imported for the block above, not part of the package
119
+
120
+ __version__ = "0.1.0"
121
+
122
+
123
+ #: Names that live in a submodule nobody should pay to import. Each is bound
124
+ #: on first use by :func:`__getattr__`; the value is the module it comes from.
125
+ _LAZY = {"Check": "doctor", "Report": "doctor", "diagnose": "doctor"}
126
+
127
+
128
+ def __getattr__(name: str) -> object:
129
+ """Expose ``xrdclient.aio`` and the diagnostics lazily.
130
+
131
+ Each is one attribute access away and neither costs anything until it is
132
+ asked for: nothing under ``xrd`` imports :mod:`asyncio` until someone
133
+ reaches for ``xrdclient.aio``, or the environment checks until someone runs
134
+ :func:`~xrdclient.doctor.diagnose`. A transfer is the common case and pays for
135
+ neither.
136
+ """
137
+ import importlib
138
+
139
+ if name in ("aio", "doctor"):
140
+ # Not ``from . import aio``: the import system resolves that by asking
141
+ # this very function for the attribute, and the recursion is infinite.
142
+ return importlib.import_module(f"{__name__}.{name}")
143
+ if name in _LAZY:
144
+ value = getattr(importlib.import_module(f"{__name__}.{_LAZY[name]}"), name)
145
+ globals()[name] = value # bound once; later lookups skip this function
146
+ return value
147
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
148
+
149
+
150
+ __all__ = [
151
+ "__version__",
152
+ # the facades that are imported on first use
153
+ "aio",
154
+ # configuration
155
+ "Config",
156
+ "configure",
157
+ "current",
158
+ "override",
159
+ "find_config_file",
160
+ # a time limit on whatever runs inside a block
161
+ "deadline",
162
+ # copying
163
+ "copy",
164
+ "copy_tree",
165
+ "third_party",
166
+ "CopyResult",
167
+ "SyncMode",
168
+ # diagnosing
169
+ "diagnose",
170
+ "Check",
171
+ "Report",
172
+ # files and paths
173
+ "open",
174
+ "XRootDPath",
175
+ "Path",
176
+ "FileSystem",
177
+ "File",
178
+ "Checkpoint",
179
+ # one-line verbs, for when a URL is all you have
180
+ "ls",
181
+ "glob",
182
+ "stat",
183
+ "exists",
184
+ "size",
185
+ "checksum",
186
+ "read_bytes",
187
+ "read_text",
188
+ "write_bytes",
189
+ "write_text",
190
+ "mkdir",
191
+ "remove",
192
+ "move",
193
+ "stage",
194
+ "is_online",
195
+ "human_bytes",
196
+ # urls
197
+ "XRootDURL",
198
+ "parse",
199
+ # values
200
+ "CheckpointInfo",
201
+ "ChecksumInfo",
202
+ "CloneRange",
203
+ "DirEntry",
204
+ "LocationInfo",
205
+ "PageResult",
206
+ "ProtocolInfo",
207
+ "ReadRange",
208
+ "StatInfo",
209
+ "SpaceInfo",
210
+ "PrepareStatus",
211
+ "VFSInfo",
212
+ "WriteChunk",
213
+ # flags
214
+ "Access",
215
+ "DirListFlags",
216
+ "LocateFlags",
217
+ "MkDirFlags",
218
+ "OpenFlags",
219
+ "PrepareFlags",
220
+ "QueryCode",
221
+ "StatInfoFlags",
222
+ # errors
223
+ "XRootDError",
224
+ "ProtocolError",
225
+ "ConnectionError",
226
+ "TimeoutError",
227
+ "OperationExpiredError",
228
+ "TransientError",
229
+ "AuthenticationError",
230
+ "NoMechanismError",
231
+ "CredentialError",
232
+ "TokenExpiredError",
233
+ "RedirectLimitError",
234
+ "WaitLimitError",
235
+ "ChecksumMismatchError",
236
+ "PageIntegrityError",
237
+ "ServerError",
238
+ "NotFoundError",
239
+ "NoSpaceError",
240
+ "UnsupportedError",
241
+ "ReadOnlyError",
242
+ "QuotaError",
243
+ "AttrNotFoundError",
244
+ "BusyError",
245
+ "InvalidArgumentError",
246
+ "ServerTimeoutError",
247
+ "TLSRequiredError",
248
+ "TooLargeError",
249
+ ]
xrdclient/_compat.py ADDED
@@ -0,0 +1,86 @@
1
+ """What the oldest Python this library runs on has not got.
2
+
3
+ That is 3.9, because it is what RHEL 9 and AlmaLinux 9 ship, and a physicist
4
+ on a login node at a site that runs one of those cannot choose otherwise. The
5
+ handful of things 3.10 added that this library would use anyway are written
6
+ out here rather than scattered through it, and where the interpreter has the
7
+ real thing, the real thing is what runs.
8
+
9
+ Nothing else in this package may name a version. If a module needs something
10
+ newer than the floor, it belongs here, with the fallback beside it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import itertools
16
+ import socket
17
+ import sys
18
+ from collections.abc import Iterable, Iterator
19
+ from typing import Any, TypeVar, overload
20
+
21
+ __all__ = ["SLOTS", "TIMEOUTS", "flag_members", "zip_strict"]
22
+
23
+ T = TypeVar("T")
24
+ U = TypeVar("U")
25
+
26
+ #: ``slots=True`` for a dataclass, where the interpreter takes it. Slots make
27
+ #: a record smaller and its attributes quicker to reach; 3.9 has no such
28
+ #: argument, and the only thing missing there is that saving.
29
+ SLOTS: dict[str, bool] = {"slots": True} if sys.version_info >= (3, 10) else {}
30
+
31
+ #: What a socket raises when it runs out of patience. 3.10 made
32
+ #: :exc:`socket.timeout` another name for :exc:`TimeoutError`; before that they
33
+ #: were two exceptions and only the first was ever raised, so every ``except``
34
+ #: that means "the far end went quiet" names both.
35
+ TIMEOUTS: tuple[type[BaseException], ...] = (socket.timeout, TimeoutError)
36
+
37
+ #: Whether :func:`zip` can do the length check itself, in C.
38
+ _NATIVE_STRICT_ZIP = sys.version_info >= (3, 10)
39
+
40
+
41
+ @overload
42
+ def zip_strict(a: Iterable[T], b: Iterable[U], /) -> Iterator[tuple[T, U]]: ...
43
+
44
+
45
+ @overload
46
+ def zip_strict(*iterables: Iterable[Any]) -> Iterator[tuple[Any, ...]]: ...
47
+
48
+
49
+ def zip_strict(*iterables: Iterable[Any]) -> Iterator[tuple[Any, ...]]:
50
+ """:func:`zip`, refusing iterables that turn out to be different lengths.
51
+
52
+ ``zip(..., strict=True)`` where there is one, and the same check written
53
+ out where there is not. The check is the whole point: everything paired
54
+ this way in this library is two halves of one thing - a flag per path, a
55
+ byte per byte of the block before it - and a bug that makes them different
56
+ lengths is invisible if the shorter one simply ends the loop.
57
+ """
58
+ if _NATIVE_STRICT_ZIP:
59
+ return zip(*iterables, strict=True)
60
+ return _paired(*iterables)
61
+
62
+
63
+ def flag_members(flag: Any) -> list[Any]:
64
+ """The single bits a :class:`~enum.Flag` is made of, in declaration order.
65
+
66
+ 3.11 taught a flag member to iterate into its own parts. Before that a
67
+ flag was not iterable at all, so the parts are picked out of the class
68
+ here: the same members, in the same order, whatever the interpreter. A bit
69
+ nobody named is dropped, as 3.11 drops it, and a member standing for more
70
+ than one bit is not a part of anything.
71
+ """
72
+ bits = int(flag)
73
+ return [
74
+ member
75
+ for member in type(flag)
76
+ if member.value and not member.value & (member.value - 1) and member.value & bits
77
+ ]
78
+
79
+
80
+ def _paired(*iterables: Iterable[Any]) -> Iterator[tuple[Any, ...]]:
81
+ """3.9's :func:`zip`, with the length check 3.10 does for itself."""
82
+ ended = object()
83
+ for values in itertools.zip_longest(*iterables, fillvalue=ended):
84
+ if any(value is ended for value in values):
85
+ raise ValueError("zip_strict() was given iterables of different lengths")
86
+ yield values
xrdclient/_log.py ADDED
@@ -0,0 +1,96 @@
1
+ """Logging helpers.
2
+
3
+ Everything logs under the ``xrdclient.`` hierarchy through a filter that redacts
4
+ credential material, so enabling DEBUG never leaks a token into a log file.
5
+
6
+ The same filter can mute parts of the hierarchy at some levels and not
7
+ others (:func:`mute`) - what XrdCl's per-level topic masks do, and what the
8
+ compatibility layer's ``SetLogMask`` is built on.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ import re
15
+ from collections.abc import Iterable
16
+
17
+ __all__ = ["get_logger", "mute", "redact"]
18
+
19
+ _PATTERNS = (
20
+ re.compile(r"(authz=)[^&\s'\"]+", re.I),
21
+ re.compile(r"(Bearer\s+)[A-Za-z0-9._~+/-]+=*", re.I),
22
+ re.compile(r"(eyJ[A-Za-z0-9_-]{4,})\.[A-Za-z0-9._-]+"),
23
+ re.compile(r"((?:token|password|secret|keytab|cred)['\"]?\s*[=:]\s*)\S+", re.I),
24
+ )
25
+
26
+
27
+ def redact(text: str) -> str:
28
+ """Replace credential material in ``text`` with ``<redacted>``."""
29
+ for pat in _PATTERNS:
30
+ text = pat.sub(r"\1<redacted>", text)
31
+ return text
32
+
33
+
34
+ class _RedactingFilter(logging.Filter):
35
+ """Interpolate first, then redact the result.
36
+
37
+ Redacting the format string and the arguments separately misses anything
38
+ that only looks like a credential once they are joined - ``"%s=%s"`` with
39
+ ``("token", secret)`` sails straight through - and it can corrupt the
40
+ format string itself, since ``"keytab: %s"`` is exactly the shape of a
41
+ secret assignment. Formatting up front has neither problem, and handlers
42
+ call :meth:`~logging.LogRecord.getMessage` anyway.
43
+ """
44
+
45
+ def filter(self, record: logging.LogRecord) -> bool:
46
+ if _muted and _is_muted(record):
47
+ return False
48
+ try:
49
+ message = record.getMessage()
50
+ except (TypeError, ValueError):
51
+ return True # a broken format string is logging's problem, not ours
52
+ cleaned = redact(message)
53
+ if cleaned != message or record.args:
54
+ record.msg = cleaned
55
+ record.args = ()
56
+ return True
57
+
58
+
59
+ #: ``(lowest level, level above the highest) -> logger prefixes`` whose
60
+ #: records in that band of levels are dropped. Replaced, never mutated, so
61
+ #: the filter reads it without a lock.
62
+ _muted: dict[tuple[int, int], tuple[str, ...]] = {}
63
+
64
+
65
+ def mute(low: int, high: int, loggers: Iterable[str]) -> None:
66
+ """Drop records from ``loggers`` with a level from ``low`` up to ``high``.
67
+
68
+ Each name covers its logger and every logger below it, and replaces what
69
+ was muted in that band before; no names un-mutes it.
70
+ """
71
+ global _muted
72
+ prefixes = tuple(f"{name}." for name in loggers)
73
+ changed = {band: names for band, names in _muted.items() if band != (low, high)}
74
+ if prefixes:
75
+ changed[(low, high)] = prefixes
76
+ _muted = changed
77
+
78
+
79
+ def _is_muted(record: logging.LogRecord) -> bool:
80
+ name = f"{record.name}."
81
+ return any(
82
+ low <= record.levelno < high and name.startswith(prefixes)
83
+ for (low, high), prefixes in _muted.items()
84
+ )
85
+
86
+
87
+ _filter = _RedactingFilter()
88
+ _root = logging.getLogger("xrdclient")
89
+ _root.addFilter(_filter)
90
+
91
+
92
+ def get_logger(name: str) -> logging.Logger:
93
+ """A logger under ``xrdclient.`` with redaction applied."""
94
+ log = logging.getLogger(name if name.startswith("xrdclient") else f"xrdclient.{name}")
95
+ log.addFilter(_filter)
96
+ return log