Coverage for python/lsst/daf/butler/registry/connectionString.py: 32%

33 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 02:27 -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/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ("DB_AUTH_ENVVAR", "DB_AUTH_PATH", "ConnectionStringFactory") 

25 

26from typing import TYPE_CHECKING 

27 

28from sqlalchemy.engine import url 

29 

30from ._dbAuth import DbAuth, DbAuthNotFoundError 

31 

32if TYPE_CHECKING: 32 ↛ 33line 32 didn't jump to line 33, because the condition on line 32 was never true

33 from ._config import RegistryConfig 

34 

35DB_AUTH_ENVVAR = "LSST_DB_AUTH" 

36"""Default name of the environmental variable that will be used to locate DB 

37credentials configuration file. """ 

38 

39DB_AUTH_PATH = "~/.lsst/db-auth.yaml" 

40"""Default path at which it is expected that DB credentials are found.""" 

41 

42 

43class ConnectionStringFactory: 

44 """Factory for `sqlalchemy.engine.url.URL` instances. 

45 

46 The factory constructs a connection string URL object by parsing the 

47 connection string, the 'db' key in the registry configuration. 

48 Username, password, host, port or database can be specified as keys in the 

49 config explicitly. If username or password are missing a matching DB is 

50 found in the credentials file pointed to by `DB_AUTH_ENVVAR` or 

51 `DB_AUTH_PATH` values. 

52 """ 

53 

54 keys = ("username", "password", "host", "port", "database") 

55 

56 @classmethod 

57 def fromConfig(cls, registryConfig: RegistryConfig) -> url.URL: 

58 """Parses the `db`, and, if they exist, username, password, host, port 

59 and database keys from the given config. 

60 

61 If no username and password are found in the connection string, or in 

62 the config, they are retrieved from a file at `DB_AUTH_PATH` or 

63 `DB_AUTH_ENVVAR`. Sqlite dialect does not require a password. 

64 

65 The `db` key value of the given config specifies the default connection 

66 string, such that if no additional connection string parameters are 

67 provided or retrieved, the `db` key is returned unmodified. 

68 

69 Parameters 

70 ---------- 

71 config : `ButlerConfig`, `RegistryConfig`, `Config` or `str` 

72 Registry configuration 

73 

74 Returns 

75 ------- 

76 connectionString : `sqlalchemy.engine.url.URL` 

77 URL object representing the connection string. 

78 

79 Raises 

80 ------ 

81 DbAuthPermissionsError 

82 If the credentials file has incorrect permissions. 

83 DbAuthError 

84 A problem occurred when retrieving DB authentication. 

85 

86 Notes 

87 ----- 

88 Matching requires dialect, host and database keys. If dialect is not 

89 specified in the db string and host and database keys are not found in 

90 the db string, or as explicit keys in the config, the matcher returns 

91 an unchanged connection string. 

92 Insufficiently specified connection strings are interpreted as an 

93 indication that a 3rd party authentication mechanism, such as Oracle 

94 Wallet, is being used and therefore are left unmodified. 

95 """ 

96 # this import can not live on the top because of circular import issue 

97 from ._config import RegistryConfig 

98 

99 regConf = RegistryConfig(registryConfig) 

100 conStr = url.make_url(regConf["db"]) 

101 

102 for key in cls.keys: 

103 if getattr(conStr, key) is None: 

104 # check for SQLAlchemy >= 1.4 

105 if hasattr(conStr, "set"): 

106 conStr = conStr.set(**{key: regConf.get(key)}) 

107 else: 

108 # SQLAlchemy 1.3 or earlier, mutate in place 

109 setattr(conStr, key, regConf.get(key)) 

110 

111 # Allow 3rd party authentication mechanisms by assuming connection 

112 # string is correct when we can not recognize (dialect, host, database) 

113 # matching keys. 

114 if any((conStr.drivername is None, conStr.host is None, conStr.database is None)): 

115 return conStr 

116 

117 # Ignore when credentials are not set up, or when no matches are found 

118 try: 

119 dbAuth = DbAuth(DB_AUTH_PATH, DB_AUTH_ENVVAR) 

120 auth = dbAuth.getAuth( 

121 conStr.drivername, conStr.username, conStr.host, conStr.port, conStr.database 

122 ) 

123 except DbAuthNotFoundError: 

124 # credentials file doesn't exist or no matches were found 

125 pass 

126 else: 

127 # only assign auth when *no* errors were raised 

128 if hasattr(conStr, "set"): 

129 # for SQLAlchemy >= 1.4 

130 conStr = conStr.set(username=auth[0], password=auth[1]) 

131 else: 

132 # SQLAlchemy 1.3 or earlier, mutate in place 

133 conStr.username = auth[0] 

134 conStr.password = auth[1] 

135 

136 return conStr