Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/app.py: 100%

67 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-05-15 14:25 -0500

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# WuttJamaican -- Base package for Wutta Framework 

5# Copyright © 2023-2024 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24WuttJamaican - app handler 

25""" 

26 

27import os 

28import warnings 

29 

30from wuttjamaican.util import load_entry_points, load_object, parse_bool 

31 

32 

33class AppHandler: 

34 """ 

35 Base class and default implementation for top-level :term:`app 

36 handler`. 

37 

38 aka. "the handler to handle all handlers" 

39 

40 aka. "one handler to bind them all" 

41 

42 For more info see :doc:`/narr/handlers/app`. 

43 

44 There is normally no need to create one of these yourself; rather 

45 you should call :meth:`~wuttjamaican.conf.WuttaConfig.get_app()` 

46 on the :term:`config object` if you need the app handler. 

47 

48 :param config: Config object for the app. This should be an 

49 instance of :class:`~wuttjamaican.conf.WuttaConfig`. 

50 

51 .. attribute:: providers 

52 

53 Dictionary of :class:`AppProvider` instances, as returned by 

54 :meth:`get_all_providers()`. 

55 """ 

56 

57 def __init__(self, config): 

58 self.config = config 

59 self.handlers = {} 

60 

61 @property 

62 def appname(self): 

63 """ 

64 The :term:`app name` for the current app. This is just an 

65 alias for :attr:`wuttjamaican.conf.WuttaConfig.appname`. 

66 

67 Note that this ``appname`` does not necessariy reflect what 

68 you think of as the name of your (e.g. custom) app. It is 

69 more fundamental than that; your Python package naming and the 

70 :term:`app title` are free to use a different name as their 

71 basis. 

72 """ 

73 return self.config.appname 

74 

75 def __getattr__(self, name): 

76 """ 

77 Custom attribute getter, called when the app handler does not 

78 already have an attribute named with ``name``. 

79 

80 This will delegate to the set of :term:`app providers<app 

81 provider>`; the first provider with an appropriately-named 

82 attribute wins, and that value is returned. 

83 

84 :returns: The first value found among the set of app 

85 providers. 

86 """ 

87 

88 if name == 'providers': 

89 self.providers = self.get_all_providers() 

90 return self.providers 

91 

92 # if 'providers' not in self.__dict__: 

93 # self.__dict__['providers'] = self.get_all_providers() 

94 

95 for provider in self.providers.values(): 

96 if hasattr(provider, name): 

97 return getattr(provider, name) 

98 

99 raise AttributeError(f"attr not found: {name}") 

100 

101 def get_all_providers(self): 

102 """ 

103 Load and return all registered providers. 

104 

105 Note that you do not need to call this directly; instead just 

106 use :attr:`providers`. 

107 

108 :returns: Dictionary keyed by entry point name; values are 

109 :class:`AppProvider` *instances*. 

110 """ 

111 providers = load_entry_points(f'{self.appname}.providers') 

112 for key in list(providers): 

113 providers[key] = providers[key](self.config) 

114 return providers 

115 

116 def make_appdir(self, path, subfolders=None, **kwargs): 

117 """ 

118 Establish an :term:`app dir` at the given path. 

119 

120 Default logic only creates a few subfolders, meant to help 

121 steer the admin toward a convention for sake of where to put 

122 things. But custom app handlers are free to do whatever. 

123 

124 :param path: Path to the desired app dir. If the path does 

125 not yet exist then it will be created. But regardless it 

126 should be "refreshed" (e.g. missing subfolders created) 

127 when this method is called. 

128 

129 :param subfolders: Optional list of subfolder names to create 

130 within the app dir. If not specified, defaults will be: 

131 ``['data', 'log', 'work']``. 

132 """ 

133 appdir = path 

134 if not os.path.exists(appdir): 

135 os.makedirs(appdir) 

136 

137 if not subfolders: 

138 subfolders = ['data', 'log', 'work'] 

139 

140 for name in subfolders: 

141 path = os.path.join(appdir, name) 

142 if not os.path.exists(path): 

143 os.mkdir(path) 

144 

145 def make_engine_from_config( 

146 self, 

147 config_dict, 

148 prefix='sqlalchemy.', 

149 **kwargs): 

150 """ 

151 Construct a new DB engine from configuration dict. 

152 

153 This is a wrapper around upstream 

154 :func:`sqlalchemy:sqlalchemy.engine_from_config()`. For even 

155 broader context of the SQLAlchemy 

156 :class:`~sqlalchemy:sqlalchemy.engine.Engine` and their 

157 configuration, see :doc:`sqlalchemy:core/engines`. 

158 

159 The purpose of the customization is to allow certain 

160 attributes of the engine to be driven by config, whereas the 

161 upstream function is more limited in that regard. The 

162 following in particular: 

163 

164 * ``poolclass`` 

165 * ``pool_pre_ping`` 

166 

167 If these options are present in the configuration dict, they 

168 will be coerced to appropriate Python equivalents and then 

169 passed as kwargs to the upstream function. 

170 

171 An example config file leveraging this feature: 

172 

173 .. code-block:: ini 

174 

175 [wutta.db] 

176 default.url = sqlite:///tmp/default.sqlite 

177 default.poolclass = sqlalchemy.pool:NullPool 

178 default.pool_pre_ping = true 

179 

180 Note that if present, the ``poolclass`` value must be a "spec" 

181 string, as required by 

182 :func:`~wuttjamaican.util.load_object()`. 

183 """ 

184 import sqlalchemy as sa 

185 

186 config_dict = dict(config_dict) 

187 

188 # convert 'poolclass' arg to actual class 

189 key = f'{prefix}poolclass' 

190 if key in config_dict and 'poolclass' not in kwargs: 

191 kwargs['poolclass'] = load_object(config_dict.pop(key)) 

192 

193 # convert 'pool_pre_ping' arg to boolean 

194 key = f'{prefix}pool_pre_ping' 

195 if key in config_dict and 'pool_pre_ping' not in kwargs: 

196 kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key)) 

197 

198 engine = sa.engine_from_config(config_dict, prefix, **kwargs) 

199 

200 return engine 

201 

202 def make_session(self, **kwargs): 

203 """ 

204 Creates a new SQLAlchemy session for the app DB. By default 

205 this will create a new :class:`~wuttjamaican.db.sess.Session` 

206 instance. 

207 

208 :returns: SQLAlchemy session for the app DB. 

209 """ 

210 from .db import Session 

211 

212 return Session(**kwargs) 

213 

214 def short_session(self, **kwargs): 

215 """ 

216 Returns a context manager for a short-lived database session. 

217 

218 This is a convenience wrapper around 

219 :class:`~wuttjamaican.db.sess.short_session`. 

220 

221 If caller does not specify ``factory`` nor ``config`` params, 

222 this method will provide a default factory in the form of 

223 :meth:`make_session`. 

224 """ 

225 from .db import short_session 

226 

227 if 'factory' not in kwargs and 'config' not in kwargs: 

228 kwargs['factory'] = self.make_session 

229 

230 return short_session(**kwargs) 

231 

232 def get_setting(self, session, name, **kwargs): 

233 """ 

234 Get a setting value from the DB. 

235 

236 This does *not* consult the config object directly to 

237 determine the setting value; it always queries the DB. 

238 

239 Default implementation is just a convenience wrapper around 

240 :func:`~wuttjamaican.db.conf.get_setting()`. 

241 

242 :param session: App DB session. 

243 

244 :param name: Name of the setting to get. 

245 

246 :returns: Setting value as string, or ``None``. 

247 """ 

248 from .db import get_setting 

249 

250 return get_setting(session, name) 

251 

252 

253class AppProvider: 

254 """ 

255 Base class for :term:`app providers<app provider>`. 

256 

257 These can add arbitrary extra functionality to the main :term:`app 

258 handler`. See also :doc:`/narr/providers/app`. 

259 

260 :param config: Config object for the app. This should be an 

261 instance of :class:`~wuttjamaican.conf.WuttaConfig`. 

262 

263 Instances have the following attributes: 

264 

265 .. attribute:: config 

266 

267 Reference to the config object. 

268 

269 .. attribute:: app 

270 

271 Reference to the parent app handler. 

272 """ 

273 

274 def __init__(self, config): 

275 

276 if isinstance(config, AppHandler): 

277 warnings.warn("passing app handler to app provider is deprecated; " 

278 "must pass config object instead", 

279 DeprecationWarning, stacklevel=2) 

280 config = config.config 

281 

282 self.config = config 

283 self.app = config.get_app() 

284 

285 

286class GenericHandler: 

287 """ 

288 Generic base class for handlers. 

289 

290 When the :term:`app` defines a new *type* of :term:`handler` it 

291 may subclass this when defining the handler base class. 

292 

293 :param config: Config object for the app. This should be an 

294 instance of :class:`~wuttjamaican.conf.WuttaConfig`. 

295 """ 

296 

297 def __init__(self, config, **kwargs): 

298 self.config = config 

299 self.app = self.config.get_app()