lsst-utils 25.2023.600__py3-none-any.whl → 29.2025.4800__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsst/utils/__init__.py +0 -3
- lsst/utils/_packaging.py +2 -0
- lsst/utils/argparsing.py +79 -0
- lsst/utils/classes.py +27 -9
- lsst/utils/db_auth.py +339 -0
- lsst/utils/deprecated.py +10 -7
- lsst/utils/doImport.py +8 -9
- lsst/utils/inheritDoc.py +34 -6
- lsst/utils/introspection.py +285 -19
- lsst/utils/iteration.py +193 -7
- lsst/utils/logging.py +155 -105
- lsst/utils/packages.py +324 -82
- lsst/utils/plotting/__init__.py +15 -0
- lsst/utils/plotting/figures.py +159 -0
- lsst/utils/plotting/limits.py +155 -0
- lsst/utils/plotting/publication_plots.py +184 -0
- lsst/utils/plotting/rubin.mplstyle +46 -0
- lsst/utils/tests.py +231 -102
- lsst/utils/threads.py +9 -3
- lsst/utils/timer.py +207 -110
- lsst/utils/usage.py +6 -6
- lsst/utils/version.py +1 -1
- lsst/utils/wrappers.py +74 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/METADATA +19 -15
- lsst_utils-29.2025.4800.dist-info/RECORD +32 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/WHEEL +1 -1
- lsst/utils/_forwarded.py +0 -28
- lsst/utils/backtrace/__init__.py +0 -33
- lsst/utils/ellipsis.py +0 -54
- lsst/utils/get_caller_name.py +0 -45
- lsst_utils-25.2023.600.dist-info/RECORD +0 -29
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info/licenses}/LICENSE +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/top_level.txt +0 -0
- {lsst_utils-25.2023.600.dist-info → lsst_utils-29.2025.4800.dist-info}/zip-safe +0 -0
lsst/utils/__init__.py
CHANGED
|
@@ -10,12 +10,9 @@
|
|
|
10
10
|
# license that can be found in the LICENSE file.
|
|
11
11
|
"""General LSST Utilities."""
|
|
12
12
|
|
|
13
|
-
from ._forwarded import *
|
|
14
13
|
from ._packaging import *
|
|
15
|
-
from .backtrace import *
|
|
16
14
|
from .deprecated import *
|
|
17
15
|
from .doImport import *
|
|
18
|
-
from .get_caller_name import *
|
|
19
16
|
from .inheritDoc import *
|
|
20
17
|
from .version import *
|
|
21
18
|
from .wrappers import *
|
lsst/utils/_packaging.py
CHANGED
lsst/utils/argparsing.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# This file is part of utils.
|
|
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.
|
|
11
|
+
#
|
|
12
|
+
|
|
13
|
+
"""Utilities to help with argument parsing in command line interfaces."""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
__all__ = ["AppendDict"]
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import copy
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AppendDict(argparse.Action):
|
|
26
|
+
"""An action analogous to the built-in 'append' that appends to a `dict`
|
|
27
|
+
instead of a `list`.
|
|
28
|
+
|
|
29
|
+
Inputs are assumed to be strings in the form "key=value"; any input that
|
|
30
|
+
does not contain exactly one "=" character is invalid. If the default value
|
|
31
|
+
is non-empty, the default key-value pairs may be overwritten by values from
|
|
32
|
+
the command line.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
option_strings: str | list[str],
|
|
38
|
+
dest: str,
|
|
39
|
+
nargs: int | str | None = None,
|
|
40
|
+
const: Any | None = None,
|
|
41
|
+
default: Any | None = None,
|
|
42
|
+
type: type | None = None,
|
|
43
|
+
choices: Any | None = None,
|
|
44
|
+
required: bool = False,
|
|
45
|
+
help: str | None = None,
|
|
46
|
+
metavar: str | None = None,
|
|
47
|
+
):
|
|
48
|
+
if default is None:
|
|
49
|
+
default = {}
|
|
50
|
+
if not isinstance(default, Mapping):
|
|
51
|
+
argname = option_strings if option_strings else metavar if metavar else dest
|
|
52
|
+
raise TypeError(f"Default for {argname} must be a mapping or None, got {default!r}.")
|
|
53
|
+
super().__init__(option_strings, dest, nargs, const, default, type, choices, required, help, metavar)
|
|
54
|
+
|
|
55
|
+
def __call__(
|
|
56
|
+
self, parser: argparse.ArgumentParser, namespace: Any, values: Any, option_string: str | None = None
|
|
57
|
+
) -> None:
|
|
58
|
+
# argparse doesn't make defensive copies, so namespace.dest may be
|
|
59
|
+
# the same object as self.default. Do the copy ourselves and avoid
|
|
60
|
+
# modifying the object previously in namespace.dest.
|
|
61
|
+
mapping = copy.copy(getattr(namespace, self.dest))
|
|
62
|
+
|
|
63
|
+
# Sometimes values is a copy of default instead of an input???
|
|
64
|
+
if isinstance(values, Mapping):
|
|
65
|
+
mapping.update(values)
|
|
66
|
+
else:
|
|
67
|
+
# values may be either a string or list of strings, depending on
|
|
68
|
+
# nargs. Unsafe to test for Sequence, because a scalar string
|
|
69
|
+
# passes.
|
|
70
|
+
if not isinstance(values, list):
|
|
71
|
+
values = [values]
|
|
72
|
+
for value in values:
|
|
73
|
+
vars = value.split("=")
|
|
74
|
+
if len(vars) != 2:
|
|
75
|
+
raise ValueError(f"Argument {value!r} does not match format 'key=value'.")
|
|
76
|
+
mapping[vars[0]] = vars[1]
|
|
77
|
+
|
|
78
|
+
# Other half of the defensive copy.
|
|
79
|
+
setattr(namespace, self.dest, mapping)
|
lsst/utils/classes.py
CHANGED
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
# license that can be found in the LICENSE file.
|
|
11
11
|
#
|
|
12
12
|
|
|
13
|
-
"""Utilities to help with class creation.
|
|
14
|
-
"""
|
|
13
|
+
"""Utilities to help with class creation."""
|
|
15
14
|
|
|
16
15
|
from __future__ import annotations
|
|
17
16
|
|
|
18
17
|
__all__ = ["Singleton", "cached_getter", "immutable"]
|
|
19
18
|
|
|
20
19
|
import functools
|
|
21
|
-
from
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from threading import RLock
|
|
22
|
+
from typing import Any, ClassVar, TypeVar
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class Singleton(type):
|
|
@@ -32,18 +33,25 @@ class Singleton(type):
|
|
|
32
33
|
adjust state of the singleton.
|
|
33
34
|
"""
|
|
34
35
|
|
|
35
|
-
_instances:
|
|
36
|
+
_instances: ClassVar[dict[type, Any]] = {}
|
|
37
|
+
# This lock isn't ideal because it is shared between all classes using this
|
|
38
|
+
# metaclass, but current use cases don't do long-running I/O during
|
|
39
|
+
# initialization so the performance impact should be low. It must be an
|
|
40
|
+
# RLock instead of a regular Lock because one singleton class might try to
|
|
41
|
+
# instantiate another as part of its initialization.
|
|
42
|
+
__lock: ClassVar[RLock] = RLock()
|
|
36
43
|
|
|
37
44
|
# Signature is intentionally not substitutable for type.__call__ (no *args,
|
|
38
45
|
# **kwargs) to require classes that use this metaclass to have no
|
|
39
46
|
# constructor arguments.
|
|
40
47
|
def __call__(cls) -> Any:
|
|
41
|
-
|
|
42
|
-
cls
|
|
43
|
-
|
|
48
|
+
with cls.__lock:
|
|
49
|
+
if cls not in cls._instances:
|
|
50
|
+
cls._instances[cls] = super().__call__()
|
|
51
|
+
return cls._instances[cls]
|
|
44
52
|
|
|
45
53
|
|
|
46
|
-
_T = TypeVar("_T", bound=
|
|
54
|
+
_T = TypeVar("_T", bound=Any)
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
def immutable(cls: _T) -> _T:
|
|
@@ -80,7 +88,7 @@ def immutable(cls: _T) -> _T:
|
|
|
80
88
|
|
|
81
89
|
# mypy says the variable here has signature (str, Any) i.e. no "self";
|
|
82
90
|
# I think it's just confused by descriptor stuff.
|
|
83
|
-
cls.__setattr__ = __setattr__
|
|
91
|
+
cls.__setattr__ = __setattr__
|
|
84
92
|
|
|
85
93
|
def __getstate__(self: _T) -> dict: # noqa: N807
|
|
86
94
|
# Disable default state-setting when unpickled.
|
|
@@ -112,6 +120,16 @@ def cached_getter(func: Callable[[_S], _R]) -> Callable[[_S], _R]:
|
|
|
112
120
|
Only works on methods that take only ``self``
|
|
113
121
|
as an argument, and returns the cached result on subsequent calls.
|
|
114
122
|
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
func : `~collections.abc.Callable`
|
|
126
|
+
Method from which the result should be cached.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
`~collections.abc.Callable`
|
|
131
|
+
Decorated method.
|
|
132
|
+
|
|
115
133
|
Notes
|
|
116
134
|
-----
|
|
117
135
|
This is intended primarily as a stopgap for Python 3.8's more sophisticated
|
lsst/utils/db_auth.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# This file is part of utils.
|
|
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
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import stat
|
|
28
|
+
import urllib.parse
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
__all__ = ["DbAuth", "DbAuthError", "DbAuthPermissionsError"]
|
|
33
|
+
|
|
34
|
+
_DEFAULT_PATH = "~/.lsst/db-auth.yaml"
|
|
35
|
+
_DEFAULT_ENVVAR = "LSST_DB_AUTH"
|
|
36
|
+
_DEFAULT_CREDS_ENVVAR = "LSST_DB_AUTH_CREDENTIALS"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DbAuthError(RuntimeError):
|
|
40
|
+
"""Exception raised when a problem has occurred retrieving database
|
|
41
|
+
authentication information.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DbAuthNotFoundError(DbAuthError):
|
|
48
|
+
"""Credentials file does not exist or no match was found in it."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class DbAuthPermissionsError(DbAuthError):
|
|
52
|
+
"""Credentials file has incorrect permissions."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DbAuth:
|
|
56
|
+
"""Retrieves authentication information for database connections.
|
|
57
|
+
|
|
58
|
+
The authorization configuration is taken from the ``authList`` parameter
|
|
59
|
+
or a (group- and world-inaccessible) YAML file located at a path specified
|
|
60
|
+
by the given environment variable or at a default path location.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
path : `str` or `None`, optional
|
|
65
|
+
Path to configuration file, default path is ``~/.lsst/db-auth.yaml``.
|
|
66
|
+
The default is used if `None`.
|
|
67
|
+
envVar : `str` or `None`, optional
|
|
68
|
+
Name of environment variable pointing to configuration file, default is
|
|
69
|
+
``LSST_DB_AUTH``. This is used if the environment variable is available
|
|
70
|
+
even if the path is specified. The default is used if `None`.
|
|
71
|
+
authList : `list` [`dict`] or `None`, optional
|
|
72
|
+
Authentication configuration. Used directly if given without referring
|
|
73
|
+
to the environment.
|
|
74
|
+
credsEnvVar : `str` or `None`, optional
|
|
75
|
+
Name of environment variable containing the authentication
|
|
76
|
+
configuration in JSON format. Default is ``LSST_DB_AUTH_CREDENTIALS``.
|
|
77
|
+
If the environment variable is defined, this takes priority over
|
|
78
|
+
reading credentials from file. The default is used if `None`.
|
|
79
|
+
|
|
80
|
+
Notes
|
|
81
|
+
-----
|
|
82
|
+
Defaults will be used if no option is provided. If provided ``authList``
|
|
83
|
+
will be used directly. The JSON credentials environment variable will
|
|
84
|
+
be used if defined, in preference to reading from a file even if overrides
|
|
85
|
+
are given for ``path`` and ``envVar``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
path: str | None = _DEFAULT_PATH,
|
|
91
|
+
envVar: str | None = _DEFAULT_ENVVAR,
|
|
92
|
+
authList: list[dict[str, str]] | None = None,
|
|
93
|
+
credsEnvVar: str | None = _DEFAULT_CREDS_ENVVAR,
|
|
94
|
+
):
|
|
95
|
+
if authList is not None:
|
|
96
|
+
self._db_auth_path = "<auth-list>"
|
|
97
|
+
self.authList = authList
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Credentials in JSON takes priority.
|
|
101
|
+
if jsonCreds := os.getenv(credsEnvVar or _DEFAULT_CREDS_ENVVAR):
|
|
102
|
+
try:
|
|
103
|
+
self.authList = json.loads(jsonCreds)
|
|
104
|
+
except json.JSONDecodeError as exc:
|
|
105
|
+
raise DbAuthError(
|
|
106
|
+
f"Unable to load DbAuth configuration using JSON environment variable {credsEnvVar}"
|
|
107
|
+
) from exc
|
|
108
|
+
self._db_auth_path = "<json-env-var>"
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
secretPath = os.getenv(envVar or _DEFAULT_ENVVAR, path or _DEFAULT_PATH)
|
|
112
|
+
secretPath = os.path.expanduser(secretPath)
|
|
113
|
+
if not os.path.isfile(secretPath):
|
|
114
|
+
raise DbAuthNotFoundError(f"No DbAuth configuration file: {secretPath}")
|
|
115
|
+
mode = os.stat(secretPath).st_mode
|
|
116
|
+
if mode & (stat.S_IRWXG | stat.S_IRWXO) != 0:
|
|
117
|
+
raise DbAuthPermissionsError(
|
|
118
|
+
f"DbAuth configuration file {secretPath} has incorrect permissions: {mode:o}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
with open(secretPath) as secretFile:
|
|
123
|
+
self.authList = yaml.safe_load(secretFile)
|
|
124
|
+
except Exception as exc:
|
|
125
|
+
raise DbAuthError(f"Unable to load DbAuth configuration file: {secretPath}.") from exc
|
|
126
|
+
self._db_auth_path = secretPath
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def db_auth_path(self) -> str:
|
|
130
|
+
"""The path to the secrets file used to load credentials (`str`)."""
|
|
131
|
+
return self._db_auth_path
|
|
132
|
+
|
|
133
|
+
# dialectname, host, and database are tagged as Optional only because other
|
|
134
|
+
# routines delegate to this one in order to raise a consistent exception
|
|
135
|
+
# for that condition.
|
|
136
|
+
def getAuth(
|
|
137
|
+
self,
|
|
138
|
+
dialectname: str | None,
|
|
139
|
+
username: str | None,
|
|
140
|
+
host: str | None,
|
|
141
|
+
port: int | str | None,
|
|
142
|
+
database: str | None,
|
|
143
|
+
) -> tuple[str | None, str]:
|
|
144
|
+
"""Retrieve a username and password for a database connection.
|
|
145
|
+
|
|
146
|
+
This function matches elements from the database connection URL with
|
|
147
|
+
glob-like URL patterns in a list of configuration dictionaries.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
dialectname : `str`
|
|
152
|
+
Database dialect, for example sqlite, mysql, postgresql, oracle,
|
|
153
|
+
or mssql.
|
|
154
|
+
username : `str` or None
|
|
155
|
+
Username from connection URL if present.
|
|
156
|
+
host : `str`
|
|
157
|
+
Host name from connection URL if present.
|
|
158
|
+
port : `str` or `int` or None
|
|
159
|
+
Port from connection URL if present.
|
|
160
|
+
database : `str`
|
|
161
|
+
Database name from connection URL.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
username: `str`
|
|
166
|
+
Username to use for database connection; same as parameter if
|
|
167
|
+
present.
|
|
168
|
+
password: `str`
|
|
169
|
+
Password to use for database connection.
|
|
170
|
+
|
|
171
|
+
Raises
|
|
172
|
+
------
|
|
173
|
+
DbAuthError
|
|
174
|
+
Raised if the input is missing elements, an authorization
|
|
175
|
+
dictionary is missing elements, the authorization file is
|
|
176
|
+
misconfigured, or no matching authorization is found.
|
|
177
|
+
|
|
178
|
+
Notes
|
|
179
|
+
-----
|
|
180
|
+
The list of authorization configuration dictionaries is tested in
|
|
181
|
+
order, with the first matching dictionary used. Each dictionary must
|
|
182
|
+
contain a ``url`` item with a pattern to match against the database
|
|
183
|
+
connection URL and a ``password`` item. If no username is provided in
|
|
184
|
+
the database connection URL, the dictionary must also contain a
|
|
185
|
+
``username`` item.
|
|
186
|
+
|
|
187
|
+
The ``url`` item must begin with a dialect and is not allowed to
|
|
188
|
+
specify dialect+driver.
|
|
189
|
+
|
|
190
|
+
Glob-style patterns (using "``*``" and "``?``" as wildcards) can be
|
|
191
|
+
used to match the host and database name portions of the connection
|
|
192
|
+
URL. For the username, port, and database name portions, omitting them
|
|
193
|
+
from the pattern matches against any value in the connection URL.
|
|
194
|
+
|
|
195
|
+
Examples
|
|
196
|
+
--------
|
|
197
|
+
The connection URL
|
|
198
|
+
``postgresql://user@host.example.com:5432/my_database`` matches against
|
|
199
|
+
the identical string as a pattern. Other patterns that would match
|
|
200
|
+
include:
|
|
201
|
+
|
|
202
|
+
* ``postgresql://*``
|
|
203
|
+
* ``postgresql://*.example.com``
|
|
204
|
+
* ``postgresql://*.example.com/my_*``
|
|
205
|
+
* ``postgresql://host.example.com/my_database``
|
|
206
|
+
* ``postgresql://host.example.com:5432/my_database``
|
|
207
|
+
* ``postgresql://user@host.example.com/my_database``
|
|
208
|
+
|
|
209
|
+
Note that the connection URL
|
|
210
|
+
``postgresql://host.example.com/my_database`` would not match against
|
|
211
|
+
the pattern ``postgresql://host.example.com:5432``, even if the default
|
|
212
|
+
port for the connection is 5432.
|
|
213
|
+
"""
|
|
214
|
+
# Check inputs, squashing MyPy warnings that they're unnecessary
|
|
215
|
+
# (since they're only unnecessary if everyone else runs MyPy).
|
|
216
|
+
if dialectname is None or dialectname == "":
|
|
217
|
+
raise DbAuthError("Missing dialectname parameter")
|
|
218
|
+
if host is None or host == "":
|
|
219
|
+
raise DbAuthError("Missing host parameter")
|
|
220
|
+
if database is None or database == "":
|
|
221
|
+
raise DbAuthError("Missing database parameter")
|
|
222
|
+
|
|
223
|
+
for authDict in self.authList:
|
|
224
|
+
# Check for mandatory entries
|
|
225
|
+
if "url" not in authDict:
|
|
226
|
+
raise DbAuthError("Missing URL in DbAuth configuration")
|
|
227
|
+
|
|
228
|
+
# Parse pseudo-URL from db-auth.yaml
|
|
229
|
+
components = urllib.parse.urlparse(authDict["url"])
|
|
230
|
+
|
|
231
|
+
# Check for same database backend type/dialect
|
|
232
|
+
if components.scheme == "":
|
|
233
|
+
raise DbAuthError("Missing database dialect in URL: " + authDict["url"])
|
|
234
|
+
|
|
235
|
+
if "+" in components.scheme:
|
|
236
|
+
raise DbAuthError(
|
|
237
|
+
"Authorization dictionary URLs should only specify "
|
|
238
|
+
f"dialects, got: {components.scheme}. instead."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# dialect and driver are allowed in db string, since functionality
|
|
242
|
+
# could change. Connecting to a DB using different driver does not
|
|
243
|
+
# change dbname/user/pass and other auth info so we ignore it.
|
|
244
|
+
# https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
|
|
245
|
+
dialect = dialectname.split("+")[0]
|
|
246
|
+
if dialect != components.scheme:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Check for same database name
|
|
250
|
+
if (
|
|
251
|
+
components.path != ""
|
|
252
|
+
and components.path != "/"
|
|
253
|
+
and not fnmatch.fnmatch(database, components.path.lstrip("/"))
|
|
254
|
+
):
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
# Check username
|
|
258
|
+
if components.username is not None:
|
|
259
|
+
if username is None or username == "":
|
|
260
|
+
continue
|
|
261
|
+
if username != components.username:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Check hostname
|
|
265
|
+
if components.hostname is None:
|
|
266
|
+
raise DbAuthError("Missing host in URL: " + authDict["url"])
|
|
267
|
+
if not fnmatch.fnmatch(host, components.hostname):
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
# Check port
|
|
271
|
+
if components.port is not None and (port is None or str(port) != str(components.port)):
|
|
272
|
+
continue
|
|
273
|
+
|
|
274
|
+
# Don't override username from connection string
|
|
275
|
+
if username is not None and username != "":
|
|
276
|
+
return (username, authDict["password"])
|
|
277
|
+
else:
|
|
278
|
+
if "username" not in authDict:
|
|
279
|
+
return (None, authDict["password"])
|
|
280
|
+
return (authDict["username"], authDict["password"])
|
|
281
|
+
|
|
282
|
+
raise DbAuthNotFoundError(
|
|
283
|
+
f"No matching DbAuth configuration for: ({dialectname}, {username}, {host}, {port}, {database})"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def getUrl(self, url: str) -> str:
|
|
287
|
+
"""Fill in a username and password in a database connection URL.
|
|
288
|
+
|
|
289
|
+
This function parses the URL and calls `getAuth`.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
url : `str`
|
|
294
|
+
Database connection URL.
|
|
295
|
+
|
|
296
|
+
Returns
|
|
297
|
+
-------
|
|
298
|
+
url : `str`
|
|
299
|
+
Database connection URL with username and password.
|
|
300
|
+
|
|
301
|
+
Raises
|
|
302
|
+
------
|
|
303
|
+
DbAuthError
|
|
304
|
+
Raised if the input is missing elements, an authorization
|
|
305
|
+
dictionary is missing elements, the authorization file is
|
|
306
|
+
misconfigured, or no matching authorization is found.
|
|
307
|
+
|
|
308
|
+
See Also
|
|
309
|
+
--------
|
|
310
|
+
getAuth : Retrieve authentication credentials.
|
|
311
|
+
"""
|
|
312
|
+
components = urllib.parse.urlparse(url)
|
|
313
|
+
username, password = self.getAuth(
|
|
314
|
+
components.scheme,
|
|
315
|
+
components.username,
|
|
316
|
+
components.hostname,
|
|
317
|
+
components.port,
|
|
318
|
+
components.path.lstrip("/"),
|
|
319
|
+
)
|
|
320
|
+
hostname = components.hostname
|
|
321
|
+
assert hostname is not None
|
|
322
|
+
if ":" in hostname: # ipv6
|
|
323
|
+
hostname = f"[{hostname}]"
|
|
324
|
+
assert username is not None
|
|
325
|
+
netloc = "{}:{}@{}".format(
|
|
326
|
+
urllib.parse.quote(username, safe=""), urllib.parse.quote(password, safe=""), hostname
|
|
327
|
+
)
|
|
328
|
+
if components.port is not None:
|
|
329
|
+
netloc += ":" + str(components.port)
|
|
330
|
+
return urllib.parse.urlunparse(
|
|
331
|
+
(
|
|
332
|
+
components.scheme,
|
|
333
|
+
netloc,
|
|
334
|
+
components.path,
|
|
335
|
+
components.params,
|
|
336
|
+
components.query,
|
|
337
|
+
components.fragment,
|
|
338
|
+
)
|
|
339
|
+
)
|
lsst/utils/deprecated.py
CHANGED
|
@@ -9,18 +9,21 @@
|
|
|
9
9
|
# Use of this source code is governed by a 3-clause BSD-style
|
|
10
10
|
# license that can be found in the LICENSE file.
|
|
11
11
|
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
12
14
|
__all__ = ["deprecate_pybind11", "suppress_deprecations"]
|
|
13
15
|
|
|
14
16
|
import functools
|
|
15
17
|
import unittest.mock
|
|
16
18
|
import warnings
|
|
19
|
+
from collections.abc import Iterator
|
|
17
20
|
from contextlib import contextmanager
|
|
18
|
-
from typing import Any
|
|
21
|
+
from typing import Any
|
|
19
22
|
|
|
20
23
|
import deprecated.sphinx
|
|
21
24
|
|
|
22
25
|
|
|
23
|
-
def deprecate_pybind11(obj: Any, reason: str, version: str, category:
|
|
26
|
+
def deprecate_pybind11(obj: Any, reason: str, version: str, category: type[Warning] = FutureWarning) -> Any:
|
|
24
27
|
"""Deprecate a pybind11-wrapped C++ interface function, method or class.
|
|
25
28
|
|
|
26
29
|
This needs to use a pass-through Python wrapper so that
|
|
@@ -35,17 +38,17 @@ def deprecate_pybind11(obj: Any, reason: str, version: str, category: Type[Warni
|
|
|
35
38
|
obj : function, method, or class
|
|
36
39
|
The function, method, or class to deprecate.
|
|
37
40
|
reason : `str`
|
|
38
|
-
Reason for deprecation, passed to `~deprecated.sphinx.deprecated
|
|
41
|
+
Reason for deprecation, passed to `~deprecated.sphinx.deprecated`.
|
|
39
42
|
version : 'str'
|
|
40
43
|
Next major version in which the interface will be deprecated,
|
|
41
|
-
passed to `~deprecated.sphinx.deprecated
|
|
44
|
+
passed to `~deprecated.sphinx.deprecated`.
|
|
42
45
|
category : `Warning`
|
|
43
|
-
Warning category, passed to `~deprecated.sphinx.deprecated
|
|
46
|
+
Warning category, passed to `~deprecated.sphinx.deprecated`.
|
|
44
47
|
|
|
45
48
|
Returns
|
|
46
49
|
-------
|
|
47
50
|
obj : function, method, or class
|
|
48
|
-
Wrapped function, method, or class
|
|
51
|
+
Wrapped function, method, or class.
|
|
49
52
|
|
|
50
53
|
Examples
|
|
51
54
|
--------
|
|
@@ -73,7 +76,7 @@ def deprecate_pybind11(obj: Any, reason: str, version: str, category: Type[Warni
|
|
|
73
76
|
|
|
74
77
|
|
|
75
78
|
@contextmanager
|
|
76
|
-
def suppress_deprecations(category:
|
|
79
|
+
def suppress_deprecations(category: type[Warning] = FutureWarning) -> Iterator[None]:
|
|
77
80
|
"""Suppress warnings generated by `deprecated.sphinx.deprecated`.
|
|
78
81
|
|
|
79
82
|
Naively, one might attempt to suppress these warnings by using
|
lsst/utils/doImport.py
CHANGED
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
# Use of this source code is governed by a 3-clause BSD-style
|
|
10
10
|
# license that can be found in the LICENSE file.
|
|
11
11
|
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
12
14
|
__all__ = ("doImport", "doImportType")
|
|
13
15
|
|
|
14
16
|
import importlib
|
|
15
17
|
import types
|
|
16
|
-
from typing import List, Optional, Type, Union
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def doImport(importable: str) ->
|
|
20
|
+
def doImport(importable: str) -> types.ModuleType | type:
|
|
20
21
|
"""Import a python object given an importable string and return it.
|
|
21
22
|
|
|
22
23
|
Parameters
|
|
@@ -43,26 +44,24 @@ def doImport(importable: str) -> Union[types.ModuleType, Type]:
|
|
|
43
44
|
if not isinstance(importable, str):
|
|
44
45
|
raise TypeError(f"Unhandled type of importable, val: {importable}")
|
|
45
46
|
|
|
46
|
-
def tryImport(
|
|
47
|
-
module: str, fromlist: List[str], previousError: Optional[str]
|
|
48
|
-
) -> Union[types.ModuleType, Type]:
|
|
47
|
+
def tryImport(module: str, fromlist: list[str], previousError: str | None) -> types.ModuleType | type:
|
|
49
48
|
pytype = importlib.import_module(module)
|
|
50
49
|
# Can have functions inside classes inside modules
|
|
51
50
|
for f in fromlist:
|
|
52
51
|
try:
|
|
53
52
|
pytype = getattr(pytype, f)
|
|
54
|
-
except AttributeError:
|
|
53
|
+
except AttributeError as e:
|
|
55
54
|
extra = f"({previousError})" if previousError is not None else ""
|
|
56
55
|
raise ImportError(
|
|
57
56
|
f"Could not get attribute '{f}' from '{module}' when importing '{importable}' {extra}"
|
|
58
|
-
)
|
|
57
|
+
) from e
|
|
59
58
|
return pytype
|
|
60
59
|
|
|
61
60
|
# Go through the import path attempting to load the module
|
|
62
61
|
# and retrieve the class or function as an attribute. Shift components
|
|
63
62
|
# from the module list to the attribute list until something works.
|
|
64
63
|
moduleComponents = importable.split(".")
|
|
65
|
-
infileComponents:
|
|
64
|
+
infileComponents: list[str] = []
|
|
66
65
|
previousError = None
|
|
67
66
|
|
|
68
67
|
while moduleComponents:
|
|
@@ -88,7 +87,7 @@ def doImport(importable: str) -> Union[types.ModuleType, Type]:
|
|
|
88
87
|
raise ModuleNotFoundError(f"Unable to import {importable!r} {extra}")
|
|
89
88
|
|
|
90
89
|
|
|
91
|
-
def doImportType(importable: str) ->
|
|
90
|
+
def doImportType(importable: str) -> type:
|
|
92
91
|
"""Import a python type given an importable string and return it.
|
|
93
92
|
|
|
94
93
|
Parameters
|