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

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/>.
22from __future__ import annotations
24__all__ = ("Location", "LocationFactory")
26import os
27import os.path
29from typing import (
30 Optional,
31 Union,
32)
34from ._butlerUri import ButlerURI
37class Location:
38 """Identifies a location within the `Datastore`.
40 Parameters
41 ----------
42 datastoreRootUri : `ButlerURI` or `str` or `None`
43 Base URI for this datastore, must include an absolute path.
44 If `None` the `path` must correspond to an absolute URI.
45 path : `ButlerURI` or `str`
46 Relative path within datastore. Assumed to be using the local
47 path separator if a ``file`` scheme is being used for the URI,
48 else a POSIX separator. Can be a full URI if the root URI is `None`.
49 Can also be a schemeless URI if it refers to a relative path.
50 """
52 __slots__ = ("_datastoreRootUri", "_path", "_uri")
54 def __init__(self, datastoreRootUri: Union[None, ButlerURI, str],
55 path: Union[ButlerURI, str]):
56 # Be careful not to force a relative local path to absolute path
57 path_uri = ButlerURI(path, forceAbsolute=False)
59 if isinstance(datastoreRootUri, str):
60 datastoreRootUri = ButlerURI(datastoreRootUri, forceDirectory=True)
61 elif datastoreRootUri is None:
62 if not path_uri.isabs():
63 raise ValueError(f"No datastore root URI given but path '{path}' was not absolute URI.")
64 elif not isinstance(datastoreRootUri, ButlerURI):
65 raise ValueError("Datastore root must be a ButlerURI instance")
67 if datastoreRootUri is not None and not datastoreRootUri.isabs():
68 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).")
70 self._datastoreRootUri = datastoreRootUri
72 # if the root URI is not None the path must not be absolute since
73 # it is required to be within the root.
74 if datastoreRootUri is not None:
75 if path_uri.isabs():
76 raise ValueError(f"Path within datastore must be relative not absolute, got {path_uri}")
78 self._path = path_uri
80 # Internal cache of the full location as a ButlerURI
81 self._uri: Optional[ButlerURI] = None
83 # Check that the resulting URI is inside the datastore
84 # This can go wrong if we were given ../dir as path
85 if self._datastoreRootUri is not None:
86 pathInStore = self.uri.relative_to(self._datastoreRootUri)
87 if pathInStore is None:
88 raise ValueError(f"Unexpectedly {path} jumps out of {self._datastoreRootUri}")
90 def __str__(self) -> str:
91 return str(self.uri)
93 def __repr__(self) -> str:
94 uri = self._datastoreRootUri
95 path = self._path
96 return f"{self.__class__.__name__}({uri!r}, {path.path!r})"
98 def __eq__(self, other: object) -> bool:
99 if not isinstance(other, Location):
100 return NotImplemented
101 # Compare the combined URI rather than how it is apportioned
102 return self.uri == other.uri
104 @property
105 def uri(self) -> ButlerURI:
106 """Return URI corresponding to fully-specified datastore location."""
107 if self._uri is None:
108 root = self._datastoreRootUri
109 if root is None:
110 uri = self._path
111 else:
112 uri = root.join(self._path)
113 self._uri = uri
114 return self._uri
116 @property
117 def path(self) -> str:
118 """Return path corresponding to location.
120 This path includes the root of the `Datastore`, but does not include
121 non-path components of the root URI. Paths will not include URI
122 quoting. If a file URI scheme is being used the path will be returned
123 with the local OS path separator.
124 """
125 full = self.uri
126 try:
127 return full.ospath
128 except AttributeError:
129 return full.unquoted_path
131 @property
132 def pathInStore(self) -> ButlerURI:
133 """Return path corresponding to location relative to `Datastore` root.
135 Uses the same path separator as supplied to the object constructor.
136 Can be an absolute URI if that is how the location was configured.
137 """
138 return self._path
140 @property
141 def netloc(self) -> str:
142 """Return the URI network location."""
143 return self.uri.netloc
145 @property
146 def relativeToPathRoot(self) -> str:
147 """Return the path component relative to the network location.
149 Effectively, this is the path property with POSIX separator stripped
150 from the left hand side of the path. Will be unquoted.
151 """
152 return self.uri.relativeToPathRoot
154 def updateExtension(self, ext: Optional[str]) -> None:
155 """Update the file extension associated with this `Location`.
157 All file extensions are replaced.
159 Parameters
160 ----------
161 ext : `str`
162 New extension. If an empty string is given any extension will
163 be removed. If `None` is given there will be no change.
164 """
165 if ext is None:
166 return
168 self._path = self._path.updatedExtension(ext)
170 # Clear the URI cache so it can be recreated with the new path
171 self._uri = None
173 def getExtension(self) -> str:
174 """Return the file extension(s) associated with this location.
176 Returns
177 -------
178 ext : `str`
179 The file extension (including the ``.``). Can be empty string
180 if there is no file extension. Will return all file extensions
181 as a single extension such that ``file.fits.gz`` will return
182 a value of ``.fits.gz``.
183 """
184 return self.uri.getExtension()
187class LocationFactory:
188 """Factory for `Location` instances.
190 The factory is constructed from the root location of the datastore.
191 This location can be a path on the file system (absolute or relative)
192 or as a URI.
194 Parameters
195 ----------
196 datastoreRoot : `str`
197 Root location of the `Datastore` either as a path in the local
198 filesystem or as a URI. File scheme URIs can be used. If a local
199 filesystem path is used without URI scheme, it will be converted
200 to an absolute path and any home directory indicators expanded.
201 If a file scheme is used with a relative path, the path will
202 be treated as a posixpath but then converted to an absolute path.
203 """
205 def __init__(self, datastoreRoot: Union[ButlerURI, str]):
206 self._datastoreRootUri = ButlerURI(datastoreRoot, forceAbsolute=True,
207 forceDirectory=True)
209 def __str__(self) -> str:
210 return f"{self.__class__.__name__}@{self._datastoreRootUri}"
212 @property
213 def netloc(self) -> str:
214 """Return the network location of root location of the `Datastore`."""
215 return self._datastoreRootUri.netloc
217 def fromPath(self, path: str) -> Location:
218 """Create a `Location` from a POSIX path.
220 Parameters
221 ----------
222 path : `str`
223 A standard POSIX path, relative to the `Datastore` root.
225 Returns
226 -------
227 location : `Location`
228 The equivalent `Location`.
229 """
230 if os.path.isabs(path):
231 raise ValueError("LocationFactory path must be relative to datastore, not absolute.")
232 return Location(self._datastoreRootUri, path)