Coverage for python/lsst/daf/butler/_location.py: 33%

80 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-06 10:53 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("Location", "LocationFactory") 

31 

32from lsst.resources import ResourcePath, ResourcePathExpression 

33 

34 

35class Location: 

36 """Identifies a location within the `Datastore`. 

37 

38 Parameters 

39 ---------- 

40 datastoreRootUri : `lsst.resources.ResourcePathExpression` or `None` 

41 Base URI for this datastore, must include an absolute path. 

42 If `None` the `path` must correspond to an absolute URI. 

43 path : `lsst.resources.ResourcePathExpression` 

44 Relative path within datastore. Assumed to be using the local 

45 path separator if a ``file`` scheme is being used for the URI, 

46 else a POSIX separator. Can be a full URI if the root URI is `None`. 

47 Can also be a schemeless URI if it refers to a relative path. 

48 """ 

49 

50 __slots__ = ("_datastoreRootUri", "_path", "_uri") 

51 

52 def __init__(self, datastoreRootUri: None | ResourcePathExpression, path: ResourcePathExpression): 

53 # Be careful not to force a relative local path to absolute path 

54 path_uri = ResourcePath(path, forceAbsolute=False) 

55 

56 if isinstance(datastoreRootUri, str): 

57 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

58 elif datastoreRootUri is None: 

59 if not path_uri.isabs(): 

60 raise ValueError(f"No datastore root URI given but path '{path}' was not absolute URI.") 

61 elif not isinstance(datastoreRootUri, ResourcePath): 

62 raise ValueError("Datastore root must be a ResourcePath instance") 

63 

64 if datastoreRootUri is not None and not datastoreRootUri.isabs(): 

65 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).") 

66 

67 self._datastoreRootUri = datastoreRootUri 

68 

69 # if the root URI is not None the path must not be absolute since 

70 # it is required to be within the root. 

71 if datastoreRootUri is not None and path_uri.isabs(): 

72 raise ValueError(f"Path within datastore must be relative not absolute, got {path_uri}") 

73 

74 self._path = path_uri 

75 

76 # Internal cache of the full location as a ResourcePath 

77 self._uri: ResourcePath | None = None 

78 

79 # Check that the resulting URI is inside the datastore 

80 # This can go wrong if we were given ../dir as path 

81 if self._datastoreRootUri is not None: 

82 pathInStore = self.uri.relative_to(self._datastoreRootUri) 

83 if pathInStore is None: 

84 raise ValueError(f"Unexpectedly {path} jumps out of {self._datastoreRootUri}") 

85 

86 def __str__(self) -> str: 

87 return str(self.uri) 

88 

89 def __repr__(self) -> str: 

90 uri = self._datastoreRootUri 

91 path = self._path 

92 return f"{self.__class__.__name__}({uri!r}, {path.path!r})" 

93 

94 def __eq__(self, other: object) -> bool: 

95 if not isinstance(other, Location): 

96 return NotImplemented 

97 # Compare the combined URI rather than how it is apportioned 

98 return self.uri == other.uri 

99 

100 @property 

101 def uri(self) -> ResourcePath: 

102 """Return URI corresponding to fully-specified datastore location.""" 

103 if self._uri is None: 

104 root = self._datastoreRootUri 

105 if root is None: 

106 uri = self._path 

107 else: 

108 uri = root.join(self._path) 

109 self._uri = uri 

110 return self._uri 

111 

112 @property 

113 def path(self) -> str: 

114 """Return path corresponding to location. 

115 

116 This path includes the root of the `Datastore`, but does not include 

117 non-path components of the root URI. Paths will not include URI 

118 quoting. If a file URI scheme is being used the path will be returned 

119 with the local OS path separator. 

120 """ 

121 full = self.uri 

122 try: 

123 return full.ospath 

124 except AttributeError: 

125 return full.unquoted_path 

126 

127 @property 

128 def pathInStore(self) -> ResourcePath: 

129 """Return path corresponding to location relative to `Datastore` root. 

130 

131 Uses the same path separator as supplied to the object constructor. 

132 Can be an absolute URI if that is how the location was configured. 

133 """ 

134 return self._path 

135 

136 @property 

137 def netloc(self) -> str: 

138 """Return the URI network location.""" 

139 return self.uri.netloc 

140 

141 @property 

142 def relativeToPathRoot(self) -> str: 

143 """Return the path component relative to the network location. 

144 

145 Effectively, this is the path property with POSIX separator stripped 

146 from the left hand side of the path. Will be unquoted. 

147 """ 

148 return self.uri.relativeToPathRoot 

149 

150 def updateExtension(self, ext: str | None) -> None: 

151 """Update the file extension associated with this `Location`. 

152 

153 All file extensions are replaced. 

154 

155 Parameters 

156 ---------- 

157 ext : `str` 

158 New extension. If an empty string is given any extension will 

159 be removed. If `None` is given there will be no change. 

160 """ 

161 if ext is None: 

162 return 

163 

164 self._path = self._path.updatedExtension(ext) 

165 

166 # Clear the URI cache so it can be recreated with the new path 

167 self._uri = None 

168 

169 def getExtension(self) -> str: 

170 """Return the file extension(s) associated with this location. 

171 

172 Returns 

173 ------- 

174 ext : `str` 

175 The file extension (including the ``.``). Can be empty string 

176 if there is no file extension. Will return all file extensions 

177 as a single extension such that ``file.fits.gz`` will return 

178 a value of ``.fits.gz``. 

179 """ 

180 return self.uri.getExtension() 

181 

182 

183class LocationFactory: 

184 """Factory for `Location` instances. 

185 

186 The factory is constructed from the root location of the datastore. 

187 This location can be a path on the file system (absolute or relative) 

188 or as a URI. 

189 

190 Parameters 

191 ---------- 

192 datastoreRoot : `str` 

193 Root location of the `Datastore` either as a path in the local 

194 filesystem or as a URI. File scheme URIs can be used. If a local 

195 filesystem path is used without URI scheme, it will be converted 

196 to an absolute path and any home directory indicators expanded. 

197 If a file scheme is used with a relative path, the path will 

198 be treated as a posixpath but then converted to an absolute path. 

199 """ 

200 

201 def __init__(self, datastoreRoot: ResourcePathExpression): 

202 self._datastoreRootUri = ResourcePath(datastoreRoot, forceAbsolute=True, forceDirectory=True) 

203 

204 def __str__(self) -> str: 

205 return f"{self.__class__.__name__}@{self._datastoreRootUri}" 

206 

207 @property 

208 def netloc(self) -> str: 

209 """Return the network location of root location of the `Datastore`.""" 

210 return self._datastoreRootUri.netloc 

211 

212 def fromPath(self, path: ResourcePathExpression) -> Location: 

213 """Create a `Location` from a POSIX path. 

214 

215 Parameters 

216 ---------- 

217 path : `str` or `lsst.resources.ResourcePath` 

218 A standard POSIX path, relative to the `Datastore` root. 

219 If it is a `lsst.resources.ResourcePath` it must not be absolute. 

220 

221 Returns 

222 ------- 

223 location : `Location` 

224 The equivalent `Location`. 

225 """ 

226 path = ResourcePath(path, forceAbsolute=False) 

227 if path.isabs(): 

228 raise ValueError("LocationFactory path must be relative to datastore, not absolute.") 

229 return Location(self._datastoreRootUri, path)