Coverage for python/lsst/daf/butler/core/location.py: 33%
80 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-12 09:20 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-08-12 09:20 +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/>.
22from __future__ import annotations
24__all__ = ("Location", "LocationFactory")
26from lsst.resources import ResourcePath, ResourcePathExpression
29class Location:
30 """Identifies a location within the `Datastore`.
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 """
44 __slots__ = ("_datastoreRootUri", "_path", "_uri")
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)
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")
58 if datastoreRootUri is not None and not datastoreRootUri.isabs():
59 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).")
61 self._datastoreRootUri = datastoreRootUri
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 and path_uri.isabs():
66 raise ValueError(f"Path within datastore must be relative not absolute, got {path_uri}")
68 self._path = path_uri
70 # Internal cache of the full location as a ResourcePath
71 self._uri: ResourcePath | None = None
73 # Check that the resulting URI is inside the datastore
74 # This can go wrong if we were given ../dir as path
75 if self._datastoreRootUri is not None:
76 pathInStore = self.uri.relative_to(self._datastoreRootUri)
77 if pathInStore is None:
78 raise ValueError(f"Unexpectedly {path} jumps out of {self._datastoreRootUri}")
80 def __str__(self) -> str:
81 return str(self.uri)
83 def __repr__(self) -> str:
84 uri = self._datastoreRootUri
85 path = self._path
86 return f"{self.__class__.__name__}({uri!r}, {path.path!r})"
88 def __eq__(self, other: object) -> bool:
89 if not isinstance(other, Location):
90 return NotImplemented
91 # Compare the combined URI rather than how it is apportioned
92 return self.uri == other.uri
94 @property
95 def uri(self) -> ResourcePath:
96 """Return URI corresponding to fully-specified datastore location."""
97 if self._uri is None:
98 root = self._datastoreRootUri
99 if root is None:
100 uri = self._path
101 else:
102 uri = root.join(self._path)
103 self._uri = uri
104 return self._uri
106 @property
107 def path(self) -> str:
108 """Return path corresponding to location.
110 This path includes the root of the `Datastore`, but does not include
111 non-path components of the root URI. Paths will not include URI
112 quoting. If a file URI scheme is being used the path will be returned
113 with the local OS path separator.
114 """
115 full = self.uri
116 try:
117 return full.ospath
118 except AttributeError:
119 return full.unquoted_path
121 @property
122 def pathInStore(self) -> ResourcePath:
123 """Return path corresponding to location relative to `Datastore` root.
125 Uses the same path separator as supplied to the object constructor.
126 Can be an absolute URI if that is how the location was configured.
127 """
128 return self._path
130 @property
131 def netloc(self) -> str:
132 """Return the URI network location."""
133 return self.uri.netloc
135 @property
136 def relativeToPathRoot(self) -> str:
137 """Return the path component relative to the network location.
139 Effectively, this is the path property with POSIX separator stripped
140 from the left hand side of the path. Will be unquoted.
141 """
142 return self.uri.relativeToPathRoot
144 def updateExtension(self, ext: str | None) -> None:
145 """Update the file extension associated with this `Location`.
147 All file extensions are replaced.
149 Parameters
150 ----------
151 ext : `str`
152 New extension. If an empty string is given any extension will
153 be removed. If `None` is given there will be no change.
154 """
155 if ext is None:
156 return
158 self._path = self._path.updatedExtension(ext)
160 # Clear the URI cache so it can be recreated with the new path
161 self._uri = None
163 def getExtension(self) -> str:
164 """Return the file extension(s) associated with this location.
166 Returns
167 -------
168 ext : `str`
169 The file extension (including the ``.``). Can be empty string
170 if there is no file extension. Will return all file extensions
171 as a single extension such that ``file.fits.gz`` will return
172 a value of ``.fits.gz``.
173 """
174 return self.uri.getExtension()
177class LocationFactory:
178 """Factory for `Location` instances.
180 The factory is constructed from the root location of the datastore.
181 This location can be a path on the file system (absolute or relative)
182 or as a URI.
184 Parameters
185 ----------
186 datastoreRoot : `str`
187 Root location of the `Datastore` either as a path in the local
188 filesystem or as a URI. File scheme URIs can be used. If a local
189 filesystem path is used without URI scheme, it will be converted
190 to an absolute path and any home directory indicators expanded.
191 If a file scheme is used with a relative path, the path will
192 be treated as a posixpath but then converted to an absolute path.
193 """
195 def __init__(self, datastoreRoot: ResourcePathExpression):
196 self._datastoreRootUri = ResourcePath(datastoreRoot, forceAbsolute=True, forceDirectory=True)
198 def __str__(self) -> str:
199 return f"{self.__class__.__name__}@{self._datastoreRootUri}"
201 @property
202 def netloc(self) -> str:
203 """Return the network location of root location of the `Datastore`."""
204 return self._datastoreRootUri.netloc
206 def fromPath(self, path: ResourcePathExpression) -> Location:
207 """Create a `Location` from a POSIX path.
209 Parameters
210 ----------
211 path : `str` or `lsst.resources.ResourcePath`
212 A standard POSIX path, relative to the `Datastore` root.
213 If it is a `lsst.resources.ResourcePath` it must not be absolute.
215 Returns
216 -------
217 location : `Location`
218 The equivalent `Location`.
219 """
220 path = ResourcePath(path, forceAbsolute=False)
221 if path.isabs():
222 raise ValueError("LocationFactory path must be relative to datastore, not absolute.")
223 return Location(self._datastoreRootUri, path)