Coverage for python/lsst/daf/butler/core/location.py: 28%
82 statements
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 12:04 -0700
« prev ^ index » next coverage.py v6.4.4, created at 2022-08-19 12:04 -0700
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/>.
22from __future__ import annotations
24__all__ = ("Location", "LocationFactory")
26from typing import Optional, Union
28from lsst.resources import ResourcePath, ResourcePathExpression
31class Location:
32 """Identifies a location within the `Datastore`.
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 """
46 __slots__ = ("_datastoreRootUri", "_path", "_uri")
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)
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")
60 if datastoreRootUri is not None and not datastoreRootUri.isabs():
61 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).")
63 self._datastoreRootUri = datastoreRootUri
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}")
71 self._path = path_uri
73 # Internal cache of the full location as a ResourcePath
74 self._uri: Optional[ResourcePath] = None
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}")
83 def __str__(self) -> str:
84 return str(self.uri)
86 def __repr__(self) -> str:
87 uri = self._datastoreRootUri
88 path = self._path
89 return f"{self.__class__.__name__}({uri!r}, {path.path!r})"
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
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
109 @property
110 def path(self) -> str:
111 """Return path corresponding to location.
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
124 @property
125 def pathInStore(self) -> ResourcePath:
126 """Return path corresponding to location relative to `Datastore` root.
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
133 @property
134 def netloc(self) -> str:
135 """Return the URI network location."""
136 return self.uri.netloc
138 @property
139 def relativeToPathRoot(self) -> str:
140 """Return the path component relative to the network location.
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
147 def updateExtension(self, ext: Optional[str]) -> None:
148 """Update the file extension associated with this `Location`.
150 All file extensions are replaced.
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
161 self._path = self._path.updatedExtension(ext)
163 # Clear the URI cache so it can be recreated with the new path
164 self._uri = None
166 def getExtension(self) -> str:
167 """Return the file extension(s) associated with this location.
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()
180class LocationFactory:
181 """Factory for `Location` instances.
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.
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 """
198 def __init__(self, datastoreRoot: ResourcePathExpression):
199 self._datastoreRootUri = ResourcePath(datastoreRoot, forceAbsolute=True, forceDirectory=True)
201 def __str__(self) -> str:
202 return f"{self.__class__.__name__}@{self._datastoreRootUri}"
204 @property
205 def netloc(self) -> str:
206 """Return the network location of root location of the `Datastore`."""
207 return self._datastoreRootUri.netloc
209 def fromPath(self, path: ResourcePathExpression) -> Location:
210 """Create a `Location` from a POSIX path.
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.
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)