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

81 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-15 09:13 +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 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 lsst.resources import ResourcePath, ResourcePathExpression 

27 

28 

29class Location: 

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

31 

32 Parameters 

33 ---------- 

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

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

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

37 path : `lsst.resources.ResourcePathExpression` 

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

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

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

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

42 """ 

43 

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

45 

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

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

48 path_uri = ResourcePath(path, forceAbsolute=False) 

49 

50 if isinstance(datastoreRootUri, str): 

51 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

52 elif datastoreRootUri is None: 

53 if not path_uri.isabs(): 

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

55 elif not isinstance(datastoreRootUri, ResourcePath): 

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

57 

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

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

60 

61 self._datastoreRootUri = datastoreRootUri 

62 

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

64 # it is required to be within the root. 

65 if datastoreRootUri is not None: 

66 if path_uri.isabs(): 

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

68 

69 self._path = path_uri 

70 

71 # Internal cache of the full location as a ResourcePath 

72 self._uri: ResourcePath | None = None 

73 

74 # Check that the resulting URI is inside the datastore 

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

76 if self._datastoreRootUri is not None: 

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

78 if pathInStore is None: 

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

80 

81 def __str__(self) -> str: 

82 return str(self.uri) 

83 

84 def __repr__(self) -> str: 

85 uri = self._datastoreRootUri 

86 path = self._path 

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

88 

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

90 if not isinstance(other, Location): 

91 return NotImplemented 

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

93 return self.uri == other.uri 

94 

95 @property 

96 def uri(self) -> ResourcePath: 

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

98 if self._uri is None: 

99 root = self._datastoreRootUri 

100 if root is None: 

101 uri = self._path 

102 else: 

103 uri = root.join(self._path) 

104 self._uri = uri 

105 return self._uri 

106 

107 @property 

108 def path(self) -> str: 

109 """Return path corresponding to location. 

110 

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

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

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

114 with the local OS path separator. 

115 """ 

116 full = self.uri 

117 try: 

118 return full.ospath 

119 except AttributeError: 

120 return full.unquoted_path 

121 

122 @property 

123 def pathInStore(self) -> ResourcePath: 

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

125 

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

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

128 """ 

129 return self._path 

130 

131 @property 

132 def netloc(self) -> str: 

133 """Return the URI network location.""" 

134 return self.uri.netloc 

135 

136 @property 

137 def relativeToPathRoot(self) -> str: 

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

139 

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

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

142 """ 

143 return self.uri.relativeToPathRoot 

144 

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

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

147 

148 All file extensions are replaced. 

149 

150 Parameters 

151 ---------- 

152 ext : `str` 

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

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

155 """ 

156 if ext is None: 

157 return 

158 

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

160 

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

162 self._uri = None 

163 

164 def getExtension(self) -> str: 

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

166 

167 Returns 

168 ------- 

169 ext : `str` 

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

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

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

173 a value of ``.fits.gz``. 

174 """ 

175 return self.uri.getExtension() 

176 

177 

178class LocationFactory: 

179 """Factory for `Location` instances. 

180 

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

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

183 or as a URI. 

184 

185 Parameters 

186 ---------- 

187 datastoreRoot : `str` 

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

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

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

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

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

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

194 """ 

195 

196 def __init__(self, datastoreRoot: ResourcePathExpression): 

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

198 

199 def __str__(self) -> str: 

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

201 

202 @property 

203 def netloc(self) -> str: 

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

205 return self._datastoreRootUri.netloc 

206 

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

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

209 

210 Parameters 

211 ---------- 

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

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

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

215 

216 Returns 

217 ------- 

218 location : `Location` 

219 The equivalent `Location`. 

220 """ 

221 path = ResourcePath(path, forceAbsolute=False) 

222 if path.isabs(): 

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

224 return Location(self._datastoreRootUri, path)