Coverage for python/lsst/resources/location.py: 32%
81 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:29 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-25 09:29 +0000
1# This file is part of lsst-resources.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# Use of this source code is governed by a 3-clause BSD-style
10# license that can be found in the LICENSE file.
12from __future__ import annotations
14__all__ = ("Location", "LocationFactory")
16from ._resourcePath import ResourcePath
19class Location:
20 """Identifies a location within the `Datastore`.
22 Parameters
23 ----------
24 datastoreRootUri : `ResourcePath` or `str` or `None`
25 Base URI for this datastore, must include an absolute path.
26 If `None` the `path` must correspond to an absolute URI.
27 path : `ResourcePath` or `str`
28 Relative path within datastore. Assumed to be using the local
29 path separator if a ``file`` scheme is being used for the URI,
30 else a POSIX separator. Can be a full URI if the root URI is `None`.
31 Can also be a schemeless URI if it refers to a relative path.
32 """
34 __slots__ = ("_datastoreRootUri", "_path", "_uri")
36 def __init__(self, datastoreRootUri: None | ResourcePath | str, path: ResourcePath | str):
37 # Be careful not to force a relative local path to absolute path
38 path_uri = ResourcePath(path, forceAbsolute=False)
40 if isinstance(datastoreRootUri, str):
41 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True)
42 elif datastoreRootUri is None:
43 if not path_uri.isabs():
44 raise ValueError(f"No datastore root URI given but path '{path}' was not absolute URI.")
45 elif not isinstance(datastoreRootUri, ResourcePath):
46 raise ValueError("Datastore root must be a ResourcePath instance")
48 if datastoreRootUri is not None and not datastoreRootUri.isabs():
49 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).")
51 self._datastoreRootUri = datastoreRootUri
53 # if the root URI is not None the path must not be absolute since
54 # it is required to be within the root.
55 if datastoreRootUri is not None:
56 if path_uri.isabs():
57 raise ValueError(f"Path within datastore must be relative not absolute, got {path_uri}")
59 self._path = path_uri
61 # Internal cache of the full location as a ResourcePath
62 self._uri: ResourcePath | None = None
64 # Check that the resulting URI is inside the datastore
65 # This can go wrong if we were given ../dir as path
66 if self._datastoreRootUri is not None:
67 pathInStore = self.uri.relative_to(self._datastoreRootUri)
68 if pathInStore is None:
69 raise ValueError(f"Unexpectedly {path} jumps out of {self._datastoreRootUri}")
71 def __str__(self) -> str:
72 return str(self.uri)
74 def __repr__(self) -> str:
75 uri = self._datastoreRootUri
76 path = self._path
77 return f"{self.__class__.__name__}({uri!r}, {path.path!r})"
79 def __eq__(self, other: object) -> bool:
80 if not isinstance(other, Location):
81 return NotImplemented
82 # Compare the combined URI rather than how it is apportioned
83 return self.uri == other.uri
85 @property
86 def uri(self) -> ResourcePath:
87 """Return URI corresponding to fully-specified datastore location."""
88 if self._uri is None:
89 root = self._datastoreRootUri
90 if root is None:
91 uri = self._path
92 else:
93 uri = root.join(self._path)
94 self._uri = uri
95 return self._uri
97 @property
98 def path(self) -> str:
99 """Return path corresponding to location.
101 This path includes the root of the `Datastore`, but does not include
102 non-path components of the root URI. Paths will not include URI
103 quoting. If a file URI scheme is being used the path will be returned
104 with the local OS path separator.
105 """
106 full = self.uri
107 try:
108 return full.ospath
109 except AttributeError:
110 return full.unquoted_path
112 @property
113 def pathInStore(self) -> ResourcePath:
114 """Return path corresponding to location relative to `Datastore` root.
116 Uses the same path separator as supplied to the object constructor.
117 Can be an absolute URI if that is how the location was configured.
118 """
119 return self._path
121 @property
122 def netloc(self) -> str:
123 """Return the URI network location."""
124 return self.uri.netloc
126 @property
127 def relativeToPathRoot(self) -> str:
128 """Return the path component relative to the network location.
130 Effectively, this is the path property with POSIX separator stripped
131 from the left hand side of the path. Will be unquoted.
132 """
133 return self.uri.relativeToPathRoot
135 def updateExtension(self, ext: str | None) -> None:
136 """Update the file extension associated with this `Location`.
138 All file extensions are replaced.
140 Parameters
141 ----------
142 ext : `str`
143 New extension. If an empty string is given any extension will
144 be removed. If `None` is given there will be no change.
145 """
146 if ext is None:
147 return
149 self._path = self._path.updatedExtension(ext)
151 # Clear the URI cache so it can be recreated with the new path
152 self._uri = None
154 def getExtension(self) -> str:
155 """Return the file extension(s) associated with this location.
157 Returns
158 -------
159 ext : `str`
160 The file extension (including the ``.``). Can be empty string
161 if there is no file extension. Will return all file extensions
162 as a single extension such that ``file.fits.gz`` will return
163 a value of ``.fits.gz``.
164 """
165 return self.uri.getExtension()
168class LocationFactory:
169 """Factory for `Location` instances.
171 The factory is constructed from the root location of the datastore.
172 This location can be a path on the file system (absolute or relative)
173 or as a URI.
175 Parameters
176 ----------
177 datastoreRoot : `str`
178 Root location of the `Datastore` either as a path in the local
179 filesystem or as a URI. File scheme URIs can be used. If a local
180 filesystem path is used without URI scheme, it will be converted
181 to an absolute path and any home directory indicators expanded.
182 If a file scheme is used with a relative path, the path will
183 be treated as a posixpath but then converted to an absolute path.
184 """
186 def __init__(self, datastoreRoot: ResourcePath | str):
187 self._datastoreRootUri = ResourcePath(datastoreRoot, forceAbsolute=True, forceDirectory=True)
189 def __str__(self) -> str:
190 return f"{self.__class__.__name__}@{self._datastoreRootUri}"
192 @property
193 def netloc(self) -> str:
194 """Return the network location of root location of the `Datastore`."""
195 return self._datastoreRootUri.netloc
197 def fromPath(self, path: str | ResourcePath) -> Location:
198 """Create a `Location` from a POSIX path.
200 Parameters
201 ----------
202 path : `str` or `ResourcePath`
203 A standard POSIX path, relative to the `Datastore` root.
204 If it is a `ResourcePath` it must not be absolute.
206 Returns
207 -------
208 location : `Location`
209 The equivalent `Location`.
210 """
211 path = ResourcePath(path, forceAbsolute=False)
212 if path.isabs():
213 raise ValueError("LocationFactory path must be relative to datastore, not absolute.")
214 return Location(self._datastoreRootUri, path)