lsst-resources 29.2025.4200__tar.gz → 29.2025.4400__tar.gz

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 (53) hide show
  1. {lsst_resources-29.2025.4200/python/lsst_resources.egg-info → lsst_resources-29.2025.4400}/PKG-INFO +3 -3
  2. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/pyproject.toml +3 -3
  3. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/_davResourceHandle.py +7 -0
  4. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourcePath.py +17 -2
  5. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/davutils.py +163 -88
  6. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/file.py +1 -1
  7. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/http.py +2 -2
  8. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/schemeless.py +6 -3
  9. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/tests.py +14 -0
  10. lsst_resources-29.2025.4400/python/lsst/resources/version.py +2 -0
  11. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400/python/lsst_resources.egg-info}/PKG-INFO +3 -3
  12. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_schemeless.py +11 -0
  13. lsst_resources-29.2025.4200/python/lsst/resources/version.py +0 -2
  14. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/COPYRIGHT +0 -0
  15. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/LICENSE +0 -0
  16. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/MANIFEST.in +0 -0
  17. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/README.md +0 -0
  18. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/doc/lsst.resources/CHANGES.rst +0 -0
  19. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/doc/lsst.resources/dav.rst +0 -0
  20. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/doc/lsst.resources/index.rst +0 -0
  21. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/doc/lsst.resources/internal-api.rst +0 -0
  22. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/doc/lsst.resources/s3.rst +0 -0
  23. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/__init__.py +0 -0
  24. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/__init__.py +0 -0
  25. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/__init__.py +0 -0
  26. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/_baseResourceHandle.py +0 -0
  27. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/_fileResourceHandle.py +0 -0
  28. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/_httpResourceHandle.py +0 -0
  29. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/_resourceHandles/_s3ResourceHandle.py +0 -0
  30. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/dav.py +0 -0
  31. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/gs.py +0 -0
  32. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/location.py +0 -0
  33. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/mem.py +0 -0
  34. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/packageresource.py +0 -0
  35. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/py.typed +0 -0
  36. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/s3.py +0 -0
  37. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/s3utils.py +0 -0
  38. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst/resources/utils.py +0 -0
  39. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst_resources.egg-info/SOURCES.txt +0 -0
  40. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst_resources.egg-info/dependency_links.txt +0 -0
  41. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst_resources.egg-info/requires.txt +0 -0
  42. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst_resources.egg-info/top_level.txt +0 -0
  43. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/python/lsst_resources.egg-info/zip-safe +0 -0
  44. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/setup.cfg +0 -0
  45. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_dav.py +0 -0
  46. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_file.py +0 -0
  47. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_gs.py +0 -0
  48. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_http.py +0 -0
  49. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_location.py +0 -0
  50. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_mem.py +0 -0
  51. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_resource.py +0 -0
  52. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_s3.py +0 -0
  53. {lsst_resources-29.2025.4200 → lsst_resources-29.2025.4400}/tests/test_s3utils.py +0 -0
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.4200
3
+ Version: 29.2025.4400
4
4
  Summary: An abstraction layer for reading and writing from URI file resources.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
- License: BSD 3-Clause License
6
+ License-Expression: BSD-3-Clause
7
7
  Project-URL: Homepage, https://github.com/lsst/resources
8
8
  Keywords: lsst
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: BSD License
11
10
  Classifier: Operating System :: OS Independent
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.11.0
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: COPYRIGHT
@@ -5,18 +5,19 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "lsst-resources"
7
7
  description = "An abstraction layer for reading and writing from URI file resources."
8
- license = {text = "BSD 3-Clause License"}
8
+ license = "BSD-3-Clause"
9
+ license-files = ["COPYRIGHT", "LICENSE"]
9
10
  readme = "README.md"
10
11
  authors = [
11
12
  {name="Rubin Observatory Data Management", email="dm-admin@lists.lsst.org"},
12
13
  ]
13
14
  classifiers = [
14
15
  "Intended Audience :: Developers",
15
- "License :: OSI Approved :: BSD License",
16
16
  "Operating System :: OS Independent",
17
17
  "Programming Language :: Python :: 3",
18
18
  "Programming Language :: Python :: 3.11",
19
19
  "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
20
21
  ]
21
22
  keywords = ["lsst"]
22
23
  dependencies = [
@@ -53,7 +54,6 @@ where = ["python"]
53
54
 
54
55
  [tool.setuptools]
55
56
  zip-safe = true
56
- license-files = ["COPYRIGHT", "LICENSE"]
57
57
 
58
58
  [tool.setuptools.package-data]
59
59
  "lsst.resources" = ["py.typed"]
@@ -36,6 +36,8 @@ class DavReadResourceHandle(BaseResourceHandle[bytes]):
36
36
  Logger to used when writing messages.
37
37
  uri : `lsst.resources.dav.DavResourcePath`
38
38
  URI of remote resource.
39
+ stat : `DavFileMetadata`
40
+ Information about this resource.
39
41
  newline : `str` or `None`, optional
40
42
  When doing multiline operations, break the stream on given character.
41
43
  Defaults to newline. If a file is opened in binary mode, this argument
@@ -181,6 +183,11 @@ class DavReadResourceHandle(BaseResourceHandle[bytes]):
181
183
  def readinto(self, output: bytearray) -> int:
182
184
  """Read up to `len(output)` bytes into `output` and return the number
183
185
  of bytes read.
186
+
187
+ Parameters
188
+ ----------
189
+ output : `bytearray`
190
+ Byte array to write output into.
184
191
  """
185
192
  if self._eof or len(output) == 0:
186
193
  return 0
@@ -22,6 +22,7 @@ import logging
22
22
  import os
23
23
  import posixpath
24
24
  import re
25
+ import sys
25
26
  import urllib.parse
26
27
  from collections import defaultdict
27
28
  from pathlib import Path, PurePath, PurePosixPath
@@ -870,7 +871,7 @@ class ResourcePath: # numpydoc ignore=PR02
870
871
  params=path_uri.params,
871
872
  )
872
873
 
873
- def relative_to(self, other: ResourcePath) -> str | None:
874
+ def relative_to(self, other: ResourcePath, walk_up: bool = False) -> str | None:
874
875
  """Return the relative path from this URI to the other URI.
875
876
 
876
877
  Parameters
@@ -878,6 +879,9 @@ class ResourcePath: # numpydoc ignore=PR02
878
879
  other : `ResourcePath`
879
880
  URI to use to calculate the relative path. Must be a parent
880
881
  of this URI.
882
+ walk_up : `bool`, optional
883
+ Control whether "``..``" can be used to resolve a relative path.
884
+ Default is `False`. Can not be `True` on Python version 3.11.
881
885
 
882
886
  Returns
883
887
  -------
@@ -896,11 +900,22 @@ class ResourcePath: # numpydoc ignore=PR02
896
900
  if not {self.netloc, other.netloc}.issubset(local_netlocs):
897
901
  return None
898
902
 
903
+ # Rather than trying to guess a failure reason from the TypeError
904
+ # explicitly check for python 3.11. Doing this will simplify the
905
+ # rediscovery of a useless python version check when we set a new
906
+ # minimum version.
907
+ kwargs = {}
908
+ if walk_up:
909
+ if sys.version_info < (3, 12, 0):
910
+ raise TypeError("walk_up parameter can not be true in python 3.11 and older")
911
+
912
+ kwargs["walk_up"] = True
913
+
899
914
  enclosed_path = self._pathLib(self.relativeToPathRoot)
900
915
  parent_path = other.relativeToPathRoot
901
916
  subpath: str | None
902
917
  try:
903
- subpath = str(enclosed_path.relative_to(parent_path))
918
+ subpath = str(enclosed_path.relative_to(parent_path, **kwargs))
904
919
  except ValueError:
905
920
  subpath = None
906
921
  else:
@@ -58,12 +58,12 @@ def normalize_path(path: str | None) -> str:
58
58
  Parameters
59
59
  ----------
60
60
  path : `str`, optional
61
- Path to normalize, e.g. '/path/to/..///normalize/'
61
+ Path to normalize (e.g., '/path/to/..///normalize/').
62
62
 
63
63
  Returns
64
64
  -------
65
65
  url : `str`
66
- Normalized URL, e.g. '/path/normalize'
66
+ Normalized URL (e.g., '/path/normalize').
67
67
  """
68
68
  return "/" if not path else "/" + posixpath.normpath(path).lstrip("/")
69
69
 
@@ -75,20 +75,18 @@ def normalize_url(url: str, preserve_scheme: bool = False, preserve_path: bool =
75
75
  Parameters
76
76
  ----------
77
77
  url : `str`
78
- URL to normalize, e.g. 'davs://example.org:1234///path/to//../dir/'
79
-
78
+ URL to normalize (e.g., 'davs://example.org:1234///path/to//../dir/').
80
79
  preserve_scheme : `bool`
81
80
  If True the scheme of `url` will be preserved. Otherwise the scheme
82
81
  of the returned normalized URL will be 'http' or 'https'.
83
-
84
- preserve_path : `bool`
85
- if True, the path of `url` will be preserved in the returned
82
+ preserve_path : `bool`
83
+ If True, the path of `url` will be preserved in the returned
86
84
  normalized URL, otherwise, the returned URL will have '/' as path.
87
85
 
88
86
  Returns
89
87
  -------
90
88
  url : `str`
91
- Normalized URL, e.g. 'https://example.org:1234/path/dir'
89
+ Normalized URL (e.g. 'https://example.org:1234/path/dir').
92
90
  """
93
91
  parsed = parse_url(url)
94
92
  if parsed.scheme is None:
@@ -324,7 +322,7 @@ class DavConfigPool:
324
322
 
325
323
  Parameters
326
324
  ----------
327
- filenames : `list[str]`
325
+ filename : `list` [ `str` ]
328
326
  List of environment variables or file names to load the configuration
329
327
  from. The first file found in the list will be read and the
330
328
  configuration settings for all webDAV endpoints will be extracted
@@ -416,7 +414,12 @@ class DavConfigPool:
416
414
 
417
415
  def get_config_for_url(self, url: str) -> DavConfig:
418
416
  """Return the configuration to use a webDAV client when interacting
419
- whith the server which hosts the resource at `url`.
417
+ with the server which hosts the resource at `url`.
418
+
419
+ Parameters
420
+ ----------
421
+ url : `str`
422
+ URL for which to obtain a configuration.
420
423
  """
421
424
  # Select the configuration for the endpoint of the provided URL.
422
425
  normalized_url: str = normalize_url(url, preserve_path=False)
@@ -446,7 +449,7 @@ def make_retry(config: DavConfig) -> Retry:
446
449
  Returns
447
450
  -------
448
451
  retry : `urllib3.util.Retry`
449
- retry object to he used when creating a ``urllib3.PoolManager``
452
+ Retry object to he used when creating a ``urllib3.PoolManager``.
450
453
  """
451
454
  backoff_min: float = config.retry_backoff_min
452
455
  backoff_max: float = config.retry_backoff_max
@@ -534,6 +537,13 @@ class DavClientPool:
534
537
  """Return a client for interacting with the endpoint where `url`
535
538
  is hosted.
536
539
 
540
+ Parameters
541
+ ----------
542
+ url : `str`
543
+ URL for which to obtain a client.
544
+
545
+ Notes
546
+ -----
537
547
  The returned client is thread-safe. If a client for that endpoint
538
548
  already exists it is reused, otherwise a new client is created
539
549
  with the appropriate configuration for interacting with the storage
@@ -600,10 +610,13 @@ class DavClient:
600
610
  Parameters
601
611
  ----------
602
612
  url : `str`
603
- Root URL of the storage endpoint (e.g. "https://host.example.org:1234/")
604
-
613
+ Root URL of the storage endpoint (e.g.
614
+ "https://host.example.org:1234/").
605
615
  config : `DavConfig`
606
616
  Configuration to initialize this client.
617
+ accepts_ranges : `bool` | `None`
618
+ Indicate whether the remote server accepts the ``Range`` header in GET
619
+ requests.
607
620
  """
608
621
 
609
622
  def __init__(self, url: str, config: DavConfig, accepts_ranges: bool | None = None) -> None:
@@ -721,9 +734,14 @@ class DavClient:
721
734
 
722
735
  def get_server_details(self, url: str) -> dict[str, str]:
723
736
  """
724
- Retrieve the details of the server and check it advertises compliance
737
+ Retrieve the details of the server and check it advertises compliance
725
738
  to class 1 of webDAV protocol.
726
739
 
740
+ Parameters
741
+ ----------
742
+ url : `str`
743
+ URL to check.
744
+
727
745
  Returns
728
746
  -------
729
747
  details: `dic[str, str]`
@@ -807,17 +825,17 @@ class DavClient:
807
825
  Target URL.
808
826
  headers : `dict[str, str]`, optional
809
827
  Headers to sent with the request.
810
- body: `bytes` or `str` or `None`, optional
828
+ body : `bytes` or `str` or `None`, optional
811
829
  Request body.
812
- pool_manager: `PoolManager`, optional
830
+ pool_manager : `PoolManager`, optional
813
831
  Pool manager to use to send the request. By default, the requests
814
832
  are sent to the frontend servers.
815
- preload_content: `bool`, optional
833
+ preload_content : `bool`, optional
816
834
  If True, the response body is downloaded and can be retrieved
817
835
  via the returned response `.data` property. If False, the
818
836
  caller needs to call `.read()` on the returned response object to
819
837
  download the body, either entirely in one call or by chunks.
820
- redirect: `bool`, optional
838
+ redirect : `bool`, optional
821
839
  If True, automatically handle redirects. If False, the returned
822
840
  response may contain a redirection to another location.
823
841
 
@@ -871,7 +889,7 @@ class DavClient:
871
889
  Target URL.
872
890
  headers : `dict[str, str]`, optional
873
891
  Headers to sent with the request.
874
- preload_content: `bool`, optional
892
+ preload_content : `bool`, optional
875
893
  If True, the response body is downloaded and can be retrieved
876
894
  via the returned response `.data` property. If False, the
877
895
  caller needs to call the `.read()` on the returned response
@@ -934,7 +952,7 @@ class DavClient:
934
952
  ----------
935
953
  url : `str`
936
954
  Target URL.
937
- data: `BinaryIO` or `bytes`
955
+ data : `BinaryIO` or `bytes`
938
956
  Request body.
939
957
  """
940
958
  # Send a PUT request with empty body and handle redirection. This
@@ -1000,8 +1018,7 @@ class DavClient:
1000
1018
  ----------
1001
1019
  url : `str`
1002
1020
  Target URL.
1003
-
1004
- raise_if_not_found: `bool``
1021
+ headers : `bool``
1005
1022
  If the target URL is not found, raise an exception. Otherwise
1006
1023
  just return the response.
1007
1024
  """
@@ -1063,9 +1080,9 @@ class DavClient:
1063
1080
  Returns
1064
1081
  -------
1065
1082
  result: `DavResourceMetadata``
1066
- Details of the resources at `url`. If no resource was found at
1067
- that URL no exception is raised. Instead the returned details allow
1068
- for detecting that the resource does not exist.
1083
+ Details of the resources at `url`. If no resource was found at
1084
+ that URL no exception is raised. Instead the returned details allow
1085
+ for detecting that the resource does not exist.
1069
1086
  """
1070
1087
  resp = self._propfind(url)
1071
1088
  match resp.status:
@@ -1095,43 +1112,48 @@ class DavClient:
1095
1112
  Returns
1096
1113
  -------
1097
1114
  result: `dict``
1098
-
1099
1115
  For an existing file, the returned value has the form:
1100
1116
 
1101
- {
1102
- "name": name,
1103
- "size": 1234,
1104
- "type": "file",
1105
- "last_modified":
1106
- datetime.datetime(2025, 4, 10, 15, 12, 51, 227854),
1107
- "checksums": {
1108
- "adler32": "0fc5f83f",
1109
- "md5": "1f57339acdec099c6c0a41f8e3d5fcd0",
1117
+ .. code-block:: json
1118
+
1119
+ {
1120
+ "name": name,
1121
+ "size": 1234,
1122
+ "type": "file",
1123
+ "last_modified":
1124
+ datetime.datetime(2025, 4, 10, 15, 12, 51, 227854),
1125
+ "checksums": {
1126
+ "adler32": "0fc5f83f",
1127
+ "md5": "1f57339acdec099c6c0a41f8e3d5fcd0",
1128
+ }
1110
1129
  }
1111
- }
1112
1130
 
1113
1131
  For an existing directory, the returned value has the form:
1114
1132
 
1115
- {
1116
- "name": name,
1117
- "size": 0,
1118
- "type": "directory",
1119
- "last_modified":
1120
- datetime.datetime(2025, 4, 10, 15, 12, 51, 227854),
1121
- "checksums": {},
1122
- }
1133
+ .. code-block:: json
1134
+
1135
+ {
1136
+ "name": name,
1137
+ "size": 0,
1138
+ "type": "directory",
1139
+ "last_modified":
1140
+ datetime.datetime(2025, 4, 10, 15, 12, 51, 227854),
1141
+ "checksums": {},
1142
+ }
1123
1143
 
1124
- For an inexisting file or directory, the returned value has the
1144
+ For a non-existing file or directory, the returned value has the
1125
1145
  form:
1126
1146
 
1127
- {
1128
- "name": name,
1129
- "size": None,
1130
- "type": None,
1131
- "last_modified":
1147
+ .. code-block:: json
1148
+
1149
+ {
1150
+ "name": name,
1151
+ "size": None,
1152
+ "type": None,
1153
+ "last_modified":
1132
1154
  datetime.datetime(1, 1, 1, 0, 0),
1133
- "checksums": {},
1134
- }
1155
+ "checksums": {},
1156
+ }
1135
1157
 
1136
1158
  Notes
1137
1159
  -----
@@ -1234,12 +1256,11 @@ class DavClient:
1234
1256
  ----------
1235
1257
  url : `str`
1236
1258
  Target URL.
1237
-
1238
- start: `int`
1259
+ start : `int`
1239
1260
  Starting byte offset of the range to download.
1240
- end: `int`
1261
+ end : `int`
1241
1262
  Ending byte offset of the range to download.
1242
- headers: `dict[str,str]`, optional
1263
+ headers : `dict[str,str]`, optional
1243
1264
  Specific headers to sent with the GET request.
1244
1265
 
1245
1266
  Returns
@@ -1268,13 +1289,13 @@ class DavClient:
1268
1289
  ----------
1269
1290
  url : `str`
1270
1291
  Target URL.
1271
-
1272
- filename: `str`
1292
+ filename : `str`
1273
1293
  Local file to write the content to. If the file already exists,
1274
1294
  it will be rewritten.
1275
-
1276
- chunk_size: `int`
1295
+ chunk_size : `int`
1277
1296
  Size of the chunks to write to `filename`.
1297
+ close_connection : `bool`
1298
+ Whether to close the connection after download.
1278
1299
 
1279
1300
  Returns
1280
1301
  -------
@@ -1330,8 +1351,7 @@ class DavClient:
1330
1351
  ----------
1331
1352
  url : `str`
1332
1353
  Target URL.
1333
-
1334
- data: `bytes`
1354
+ data : `bytes`
1335
1355
  Sequence of bytes to upload.
1336
1356
 
1337
1357
  Notes
@@ -1350,7 +1370,7 @@ class DavClient:
1350
1370
  Parameters
1351
1371
  ----------
1352
1372
  url : `str`
1353
- Target URL
1373
+ Target URL.
1354
1374
 
1355
1375
  Returns
1356
1376
  -------
@@ -1412,6 +1432,11 @@ class DavClient:
1412
1432
  def accepts_ranges(self, url: str) -> bool:
1413
1433
  """Return `True` if the server supports a 'Range' header in
1414
1434
  GET requests against `url`.
1435
+
1436
+ Parameters
1437
+ ----------
1438
+ url : `str`
1439
+ Target URL.
1415
1440
  """
1416
1441
  # If we have already determined that the server accepts "Range" for
1417
1442
  # another URL, we assume that it implements that feature for any
@@ -1486,6 +1511,8 @@ class DavClient:
1486
1511
 
1487
1512
  Parameters
1488
1513
  ----------
1514
+ url : `str`
1515
+ Target URL.
1489
1516
  expiration_time_seconds : `int`
1490
1517
  Number of seconds until the generated URL is no longer valid.
1491
1518
 
@@ -1502,6 +1529,8 @@ class DavClient:
1502
1529
 
1503
1530
  Parameters
1504
1531
  ----------
1532
+ url : `str`
1533
+ Target URL.
1505
1534
  expiration_time_seconds : `int`
1506
1535
  Number of seconds until the generated URL is no longer valid.
1507
1536
 
@@ -1530,10 +1559,13 @@ class DavClientURLSigner(DavClient):
1530
1559
  Parameters
1531
1560
  ----------
1532
1561
  url : `str`
1533
- Root URL of the storage endpoint (e.g. "https://host.example.org:1234/")
1534
-
1562
+ Root URL of the storage endpoint
1563
+ (e.g. "https://host.example.org:1234/").
1535
1564
  config : `DavConfig`
1536
1565
  Configuration to initialize this client.
1566
+ accepts_ranges : `bool` | `None`
1567
+ Indicate whether the remote server accepts the ``Range`` header in GET
1568
+ requests.
1537
1569
  """
1538
1570
 
1539
1571
  def __init__(self, url: str, config: DavConfig, accepts_ranges: bool | None = None) -> None:
@@ -1772,10 +1804,13 @@ class DavClientDCache(DavClientURLSigner):
1772
1804
  Parameters
1773
1805
  ----------
1774
1806
  url : `str`
1775
- Root URL of the storage endpoint (e.g. "https://host.example.org:1234/")
1776
-
1807
+ Root URL of the storage endpoint
1808
+ (e.g. "https://host.example.org:1234/").
1777
1809
  config : `DavConfig`
1778
1810
  Configuration to initialize this client.
1811
+ accepts_ranges : `bool` | `None`
1812
+ Indicate whether the remote server accepts the ``Range`` header in GET
1813
+ requests.
1779
1814
  """
1780
1815
 
1781
1816
  def __init__(self, url: str, config: DavConfig, accepts_ranges: bool | None = None) -> None:
@@ -1817,7 +1852,7 @@ class DavClientDCache(DavClientURLSigner):
1817
1852
  Target URL.
1818
1853
  headers : `dict[str, str]`, optional
1819
1854
  Headers to sent with the request.
1820
- preload_content: `bool`, optional
1855
+ preload_content : `bool`, optional
1821
1856
  If True, the response body is downloaded and can be retrieved
1822
1857
  via the returned response `.data` property. If False, the
1823
1858
  caller needs to call the `.read()` on the returned response
@@ -1900,7 +1935,7 @@ class DavClientDCache(DavClientURLSigner):
1900
1935
  ----------
1901
1936
  url : `str`
1902
1937
  Target URL.
1903
- data: `BinaryIO` or `bytes`
1938
+ data : `BinaryIO` or `bytes`
1904
1939
  Request body.
1905
1940
  """
1906
1941
  # Send a PUT request with empty body to the dCache frontend server to
@@ -1999,10 +2034,13 @@ class DavClientXrootD(DavClientURLSigner):
1999
2034
  Parameters
2000
2035
  ----------
2001
2036
  url : `str`
2002
- Root URL of the storage endpoint (e.g. "https://host.example.org:1234/")
2003
-
2037
+ Root URL of the storage endpoint
2038
+ (e.g. "https://host.example.org:1234/").
2004
2039
  config : `DavConfig`
2005
2040
  Configuration to initialize this client.
2041
+ accepts_ranges : `bool` | `None`
2042
+ Indicate whether the remote server accepts the ``Range`` header in GET
2043
+ requests.
2006
2044
  """
2007
2045
 
2008
2046
  def __init__(self, url: str, config: DavConfig, accepts_ranges: bool | None = None) -> None:
@@ -2019,7 +2057,7 @@ class DavClientXrootD(DavClientURLSigner):
2019
2057
  Target URL.
2020
2058
  headers : `dict[str, str]`, optional
2021
2059
  Headers to sent with the request.
2022
- preload_content: `bool`, optional
2060
+ preload_content : `bool`, optional
2023
2061
  If True, the response body is downloaded and can be retrieved
2024
2062
  via the returned response `.data` property. If False, the
2025
2063
  caller needs to call the `.read()` on the returned response
@@ -2087,7 +2125,7 @@ class DavClientXrootD(DavClientURLSigner):
2087
2125
  ----------
2088
2126
  url : `str`
2089
2127
  Target URL.
2090
- data: `BinaryIO` or `bytes`
2128
+ data : `BinaryIO` or `bytes`
2091
2129
  Request body.
2092
2130
  """
2093
2131
  # Send a PUT request with empty body to the XRootD frontend server to
@@ -2196,7 +2234,27 @@ class DavClientXrootD(DavClientURLSigner):
2196
2234
 
2197
2235
 
2198
2236
  class DavFileMetadata:
2199
- """Container for attributes of interest of a webDAV file or directory."""
2237
+ """Container for attributes of interest of a webDAV file or directory.
2238
+
2239
+ Parameters
2240
+ ----------
2241
+ base_url : `str`
2242
+ Base URL.
2243
+ href : `str`, optional
2244
+ Path component that can be added to the base URL.
2245
+ name : `str`, optional
2246
+ Name.
2247
+ exists : `bool`, optional
2248
+ Whether file or directory exist.
2249
+ size : `int`, optional
2250
+ Size of file.
2251
+ is_dir : `bool`, optional
2252
+ Whether the URL points to a directory or file.
2253
+ last_modified : `bool`, optional
2254
+ Last modified date.
2255
+ checksums : `dict` [ `str`, `str` ] | `None`, optional
2256
+ Checksums.
2257
+ """
2200
2258
 
2201
2259
  def __init__(
2202
2260
  self,
@@ -2220,7 +2278,15 @@ class DavFileMetadata:
2220
2278
 
2221
2279
  @staticmethod
2222
2280
  def from_property(base_url: str, property: DavProperty) -> DavFileMetadata:
2223
- """Create an instance from the values in `property`."""
2281
+ """Create an instance from the values in `property`.
2282
+
2283
+ Parameters
2284
+ ----------
2285
+ base_url : `str`
2286
+ Base URL.
2287
+ property : `DavProperty`
2288
+ Properties to associate with URL.
2289
+ """
2224
2290
  return DavFileMetadata(
2225
2291
  base_url=base_url,
2226
2292
  href=property.href,
@@ -2416,13 +2482,7 @@ class DavProperty:
2416
2482
 
2417
2483
 
2418
2484
  class DavPropfindParser:
2419
- """Helper class to parse the response body of a PROPFIND request.
2420
-
2421
- Parameters
2422
- ----------
2423
- body : `bytes`
2424
- The XML-encoded response body to PROPFIND.
2425
- """
2485
+ """Helper class to parse the response body of a PROPFIND request."""
2426
2486
 
2427
2487
  def __init__(self) -> None:
2428
2488
  return
@@ -2434,11 +2494,12 @@ class DavPropfindParser:
2434
2494
  Parameters
2435
2495
  ----------
2436
2496
  body : `bytes`
2437
- XML-encoded response body to a PROPFIND request
2497
+ XML-encoded response body to a PROPFIND request.
2438
2498
 
2439
2499
  Returns
2440
2500
  -------
2441
- responses : `List[DavProperty]`
2501
+ responses : `list` [ `DavProperty` ]
2502
+ Parsed content of the response.
2442
2503
 
2443
2504
  Notes
2444
2505
  -----
@@ -2550,7 +2611,13 @@ class TokenAuthorizer:
2550
2611
  return owner_accessible and not group_accessible and not other_accessible
2551
2612
 
2552
2613
  def set_authorization(self, headers: dict[str, str]) -> None:
2553
- """Add the 'Authorization' header to `headers`."""
2614
+ """Add the 'Authorization' header to `headers`.
2615
+
2616
+ Parameters
2617
+ ----------
2618
+ headers : `dict` [ `str`, `str` ]
2619
+ Dict to augment with authorization information.
2620
+ """
2554
2621
  if self._token is None:
2555
2622
  return
2556
2623
 
@@ -2565,8 +2632,8 @@ def expand_vars(path: str | None) -> str | None:
2565
2632
  Parameters
2566
2633
  ----------
2567
2634
  path : `str` or `None`
2568
- Abolute or relative path which may include an environment variable
2569
- e.g. '$HOME/path/to/my/file'
2635
+ Abolute or relative path which may include an environment variable
2636
+ (e.g. '$HOME/path/to/my/file').
2570
2637
 
2571
2638
  Returns
2572
2639
  -------
@@ -2577,7 +2644,15 @@ def expand_vars(path: str | None) -> str | None:
2577
2644
 
2578
2645
 
2579
2646
  def dump_response(method: str, resp: HTTPResponse) -> None:
2580
- """Dump response for debugging purposes."""
2647
+ """Dump response for debugging purposes.
2648
+
2649
+ Parameters
2650
+ ----------
2651
+ method : `str`
2652
+ Method name to include in log output.
2653
+ resp : `HTTPResponse`
2654
+ Response to dump.
2655
+ """
2581
2656
  log.debug("%s %s", method, resp.geturl())
2582
2657
  for header, value in resp.headers.items():
2583
2658
  log.debug(" %s: %s", header, value)
@@ -108,7 +108,7 @@ class FileResourcePath(ResourcePath):
108
108
 
109
109
  def write(self, data: bytes, overwrite: bool = True) -> None:
110
110
  dir = os.path.dirname(self.ospath)
111
- if not os.path.exists(dir):
111
+ if dir and not os.path.exists(dir):
112
112
  _create_directories(dir)
113
113
  mode = "wb" if overwrite else "xb"
114
114
  with open(self.ospath, mode) as f:
@@ -774,8 +774,8 @@ class HttpResourcePath(ResourcePath):
774
774
  generated internally by `HttpResourcePath` (e.g. authentication
775
775
  headers).
776
776
 
777
- Return
778
- ------
777
+ Returns
778
+ -------
779
779
  instance : `ResourcePath`
780
780
  Newly-created `HttpResourcePath` instance.
781
781
 
@@ -105,13 +105,16 @@ class SchemelessResourcePath(FileResourcePath):
105
105
  return stat.S_ISDIR(status.st_mode)
106
106
  return self.dirLike
107
107
 
108
- def relative_to(self, other: ResourcePath) -> str | None:
108
+ def relative_to(self, other: ResourcePath, walk_up: bool = False) -> str | None:
109
109
  """Return the relative path from this URI to the other URI.
110
110
 
111
111
  Parameters
112
112
  ----------
113
113
  other : `ResourcePath`
114
114
  URI to use to calculate the relative path.
115
+ walk_up : `bool`, optional
116
+ Control whether "``..``" can be used to resolve a relative path.
117
+ Default is `False`. Can not be `True` on Python version 3.11.
115
118
 
116
119
  Returns
117
120
  -------
@@ -146,8 +149,8 @@ class SchemelessResourcePath(FileResourcePath):
146
149
  raise RuntimeError(f"Unexpected combination of {child}.relative_to({other}).")
147
150
 
148
151
  if child is None:
149
- return super().relative_to(other)
150
- return child.relative_to(other)
152
+ return super().relative_to(other, walk_up=walk_up)
153
+ return child.relative_to(other, walk_up=walk_up)
151
154
 
152
155
  @classmethod
153
156
  def _fixupPathUri(
@@ -17,6 +17,7 @@ import os
17
17
  import pathlib
18
18
  import random
19
19
  import string
20
+ import sys
20
21
  import tempfile
21
22
  import unittest
22
23
  import urllib.parse
@@ -375,6 +376,19 @@ class GenericTestCase(_GenericTestCase):
375
376
  parent = ResourcePath("d/e.txt", forceAbsolute=False)
376
377
  self.assertIsNone(child.relative_to(parent), f"{child}.relative_to({parent})")
377
378
 
379
+ # Allow .. in response.
380
+ child = ResourcePath(self._make_uri("a/b/c/d.txt"), forceAbsolute=False)
381
+ parent = ResourcePath(self._make_uri("a/b/d/e/"), forceAbsolute=False)
382
+ self.assertIsNone(child.relative_to(parent), f"{child}.relative_to({parent})")
383
+
384
+ if sys.version_info >= (3, 12, 0):
385
+ # Fails on python 3.11.
386
+ self.assertEqual(
387
+ child.relative_to(parent, walk_up=True),
388
+ "../../c/d.txt",
389
+ f"{child}.relative_to({parent}, walk_up=True)",
390
+ )
391
+
378
392
  def test_parents(self) -> None:
379
393
  """Test of splitting and parent walking."""
380
394
  parent = ResourcePath(self._make_uri("somedir"), forceDirectory=True)
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "29.2025.4400"
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lsst-resources
3
- Version: 29.2025.4200
3
+ Version: 29.2025.4400
4
4
  Summary: An abstraction layer for reading and writing from URI file resources.
5
5
  Author-email: Rubin Observatory Data Management <dm-admin@lists.lsst.org>
6
- License: BSD 3-Clause License
6
+ License-Expression: BSD-3-Clause
7
7
  Project-URL: Homepage, https://github.com/lsst/resources
8
8
  Keywords: lsst
9
9
  Classifier: Intended Audience :: Developers
10
- Classifier: License :: OSI Approved :: BSD License
11
10
  Classifier: Operating System :: OS Independent
12
11
  Classifier: Programming Language :: Python :: 3
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
15
  Requires-Python: >=3.11.0
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: COPYRIGHT
@@ -98,6 +98,17 @@ class SchemelessTestCase(unittest.TestCase):
98
98
  self.assertFalse(f.isdir())
99
99
  self.assertIsNone(f.dirLike)
100
100
 
101
+ def test_cwd_write(self):
102
+ f = None
103
+ try:
104
+ f = ResourcePath("cwd.txt", forceAbsolute=False)
105
+ f.write(b"abc")
106
+ written = f.read()
107
+ self.assertEqual(written, b"abc")
108
+ finally:
109
+ if f:
110
+ f.remove()
111
+
101
112
 
102
113
  if __name__ == "__main__":
103
114
  unittest.main()
@@ -1,2 +0,0 @@
1
- __all__ = ["__version__"]
2
- __version__ = "29.2025.4200"