Coverage for python/lsst/daf/butler/core/location.py: 25%

82 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-12 02:19 -0800

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 program is free software: you can redistribute it and/or modify 

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22from __future__ import annotations 

23 

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

25 

26from typing import Optional, Union 

27 

28from lsst.resources import ResourcePath, ResourcePathExpression 

29 

30 

31class Location: 

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

33 

34 Parameters 

35 ---------- 

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

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

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

39 path : `lsst.resources.ResourcePathExpression` 

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

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

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

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

44 """ 

45 

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

47 

48 def __init__(self, datastoreRootUri: Union[None, ResourcePathExpression], path: ResourcePathExpression): 

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

50 path_uri = ResourcePath(path, forceAbsolute=False) 

51 

52 if isinstance(datastoreRootUri, str): 

53 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

54 elif datastoreRootUri is None: 

55 if not path_uri.isabs(): 

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

57 elif not isinstance(datastoreRootUri, ResourcePath): 

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

59 

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

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

62 

63 self._datastoreRootUri = datastoreRootUri 

64 

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

66 # it is required to be within the root. 

67 if datastoreRootUri is not None: 

68 if path_uri.isabs(): 

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

70 

71 self._path = path_uri 

72 

73 # Internal cache of the full location as a ResourcePath 

74 self._uri: Optional[ResourcePath] = None 

75 

76 # Check that the resulting URI is inside the datastore 

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

78 if self._datastoreRootUri is not None: 

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

80 if pathInStore is None: 

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

82 

83 def __str__(self) -> str: 

84 return str(self.uri) 

85 

86 def __repr__(self) -> str: 

87 uri = self._datastoreRootUri 

88 path = self._path 

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

90 

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

92 if not isinstance(other, Location): 

93 return NotImplemented 

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

95 return self.uri == other.uri 

96 

97 @property 

98 def uri(self) -> ResourcePath: 

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

100 if self._uri is None: 

101 root = self._datastoreRootUri 

102 if root is None: 

103 uri = self._path 

104 else: 

105 uri = root.join(self._path) 

106 self._uri = uri 

107 return self._uri 

108 

109 @property 

110 def path(self) -> str: 

111 """Return path corresponding to location. 

112 

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

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

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

116 with the local OS path separator. 

117 """ 

118 full = self.uri 

119 try: 

120 return full.ospath 

121 except AttributeError: 

122 return full.unquoted_path 

123 

124 @property 

125 def pathInStore(self) -> ResourcePath: 

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

127 

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

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

130 """ 

131 return self._path 

132 

133 @property 

134 def netloc(self) -> str: 

135 """Return the URI network location.""" 

136 return self.uri.netloc 

137 

138 @property 

139 def relativeToPathRoot(self) -> str: 

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

141 

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

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

144 """ 

145 return self.uri.relativeToPathRoot 

146 

147 def updateExtension(self, ext: Optional[str]) -> None: 

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

149 

150 All file extensions are replaced. 

151 

152 Parameters 

153 ---------- 

154 ext : `str` 

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

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

157 """ 

158 if ext is None: 

159 return 

160 

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

162 

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

164 self._uri = None 

165 

166 def getExtension(self) -> str: 

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

168 

169 Returns 

170 ------- 

171 ext : `str` 

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

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

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

175 a value of ``.fits.gz``. 

176 """ 

177 return self.uri.getExtension() 

178 

179 

180class LocationFactory: 

181 """Factory for `Location` instances. 

182 

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

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

185 or as a URI. 

186 

187 Parameters 

188 ---------- 

189 datastoreRoot : `str` 

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

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

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

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

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

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

196 """ 

197 

198 def __init__(self, datastoreRoot: ResourcePathExpression): 

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

200 

201 def __str__(self) -> str: 

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

203 

204 @property 

205 def netloc(self) -> str: 

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

207 return self._datastoreRootUri.netloc 

208 

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

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

211 

212 Parameters 

213 ---------- 

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

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

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

217 

218 Returns 

219 ------- 

220 location : `Location` 

221 The equivalent `Location`. 

222 """ 

223 path = ResourcePath(path, forceAbsolute=False) 

224 if path.isabs(): 

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

226 return Location(self._datastoreRootUri, path)