Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 

26import os 

27import os.path 

28import posixpath 

29import types 

30 

31from typing import ( 

32 Optional, 

33 Union, 

34) 

35 

36from ._butlerUri import ButlerURI 

37 

38 

39class Location: 

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

41 

42 Parameters 

43 ---------- 

44 datastoreRootUri : `ButlerURI` or `str` 

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

46 path : `str` 

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

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

49 else a POSIX separator. 

50 """ 

51 

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

53 

54 def __init__(self, datastoreRootUri: Union[ButlerURI, str], path: str): 

55 if isinstance(datastoreRootUri, str): 

56 datastoreRootUri = ButlerURI(datastoreRootUri, forceDirectory=True) 

57 elif not isinstance(datastoreRootUri, ButlerURI): 

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

59 

60 if not posixpath.isabs(datastoreRootUri.path): 

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

62 

63 self._datastoreRootUri = datastoreRootUri 

64 

65 pathModule: types.ModuleType 

66 if not self._datastoreRootUri.scheme: 

67 pathModule = os.path 

68 else: 

69 pathModule = posixpath 

70 

71 # mypy can not work out that these modules support isabs 

72 if pathModule.isabs(path): # type: ignore 

73 raise ValueError("Path within datastore must be relative not absolute") 

74 

75 self._path = path 

76 

77 # Internal cache of the full location as a ButlerURI 

78 self._uri: Optional[ButlerURI] = None 

79 

80 # Check that the resulting URI is inside the datastore 

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

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.geturl() 

91 path = self._path 

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

93 

94 @property 

95 def uri(self) -> ButlerURI: 

96 """URI corresponding to fully-specified location in datastore. 

97 """ 

98 if self._uri is None: 

99 self._uri = self._datastoreRootUri.join(self._path) 

100 return self._uri 

101 

102 @property 

103 def path(self) -> str: 

104 """Path corresponding to location. 

105 

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

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

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

109 with the local OS path separator. 

110 """ 

111 full = self.uri 

112 try: 

113 return full.ospath 

114 except AttributeError: 

115 return full.unquoted_path 

116 

117 @property 

118 def pathInStore(self) -> str: 

119 """Path corresponding to location relative to `Datastore` root. 

120 

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

122 """ 

123 return self._path 

124 

125 @property 

126 def netloc(self) -> str: 

127 """The URI network location.""" 

128 return self._datastoreRootUri.netloc 

129 

130 @property 

131 def relativeToPathRoot(self) -> str: 

132 """Returns the path component of the URI relative to the network 

133 location. 

134 

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

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

137 """ 

138 return self.uri.relativeToPathRoot 

139 

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

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

142 

143 All file extensions are replaced. 

144 

145 Parameters 

146 ---------- 

147 ext : `str` 

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

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

150 """ 

151 if ext is None: 

152 return 

153 

154 # Get the extension and remove it from the path if one is found 

155 # .fits.gz counts as one extension do not use os.path.splitext 

156 current = self.getExtension() 

157 path = self.pathInStore 

158 if current: 

159 path = path[:-len(current)] 

160 

161 # Ensure that we have a leading "." on file extension (and we do not 

162 # try to modify the empty string) 

163 if ext and not ext.startswith("."): 

164 ext = "." + ext 

165 

166 self._path = path + ext 

167 

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

169 self._uri = None 

170 

171 def getExtension(self) -> str: 

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

173 

174 Returns 

175 ------- 

176 ext : `str` 

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

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

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

180 a value of ``.fits.gz``. 

181 """ 

182 return self.uri.getExtension() 

183 

184 

185class LocationFactory: 

186 """Factory for `Location` instances. 

187 

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

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

190 or as a URI. 

191 

192 Parameters 

193 ---------- 

194 datastoreRoot : `str` 

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

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

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

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

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

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

201 """ 

202 

203 def __init__(self, datastoreRoot: Union[ButlerURI, str]): 

204 self._datastoreRootUri = ButlerURI(datastoreRoot, forceAbsolute=True, 

205 forceDirectory=True) 

206 

207 def __str__(self) -> str: 

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

209 

210 @property 

211 def netloc(self) -> str: 

212 """Returns the network location of root location of the `Datastore`.""" 

213 return self._datastoreRootUri.netloc 

214 

215 def fromPath(self, path: str) -> Location: 

216 """Factory function to create a `Location` from a POSIX path. 

217 

218 Parameters 

219 ---------- 

220 path : `str` 

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

222 

223 Returns 

224 ------- 

225 location : `Location` 

226 The equivalent `Location`. 

227 """ 

228 if os.path.isabs(path): 

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

230 return Location(self._datastoreRootUri, path)