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

79 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-31 19:25 -0600

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

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

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024-2025 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""" 

24Application 

25""" 

26 

27import logging 

28import os 

29 

30from wuttjamaican.app import AppProvider 

31from wuttjamaican.conf import make_config 

32 

33from asgiref.wsgi import WsgiToAsgi 

34from pyramid.config import Configurator 

35 

36import wuttaweb.db 

37from wuttaweb.auth import WuttaSecurityPolicy 

38from wuttaweb.util import get_effective_theme, get_theme_template_path 

39 

40 

41log = logging.getLogger(__name__) 

42 

43 

44class WebAppProvider(AppProvider): 

45 """ 

46 The :term:`app provider` for WuttaWeb. This adds some methods to 

47 the :term:`app handler`, which are specific to web apps. It also 

48 registers some :term:`email templates <email template>` for the 

49 app, etc. 

50 """ 

51 

52 email_modules = ["wuttaweb.emails"] 

53 email_templates = ["wuttaweb:email-templates"] 

54 

55 def get_web_handler(self): 

56 """ 

57 Get the configured "web" handler for the app. 

58 

59 Specify a custom handler in your config file like this: 

60 

61 .. code-block:: ini 

62 

63 [wutta] 

64 web.handler_spec = poser.web.handler:PoserWebHandler 

65 

66 :returns: Instance of :class:`~wuttaweb.handler.WebHandler`. 

67 """ 

68 if "web" not in self.app.handlers: 

69 spec = self.config.get( 

70 f"{self.appname}.web.handler_spec", 

71 default="wuttaweb.handler:WebHandler", 

72 ) 

73 self.app.handlers["web"] = self.app.load_object(spec)(self.config) 

74 return self.app.handlers["web"] 

75 

76 

77def make_wutta_config(settings, config_maker=None, **kwargs): 

78 """ 

79 Make a WuttaConfig object from the given settings. 

80 

81 Note that ``settings`` dict will (typically) correspond to the 

82 ``[app:main]`` section of your config file. 

83 

84 Regardless, the ``settings`` must contain a special key/value 

85 which is needed to identify the location of the config file. 

86 Assuming the typical scenario then, your config file should have 

87 an entry like this: 

88 

89 .. code-block:: ini 

90 

91 [app:main] 

92 wutta.config = %(__file__)s 

93 

94 The ``%(__file__)s`` is auto-replaced with the config file path, 

95 so ultimately ``settings`` would contain something like (at 

96 minimum):: 

97 

98 {'wutta.config': '/path/to/config/file'} 

99 

100 If this config file path cannot be discovered, an error is raised. 

101 """ 

102 wutta_config = settings.get("wutta_config") 

103 if not wutta_config: 

104 

105 # validate config file path 

106 path = settings.get("wutta.config") 

107 if not path or not os.path.exists(path): 

108 raise ValueError( 

109 "Please set 'wutta.config' in [app:main] " 

110 "section of config to the path of your " 

111 "config file. Lame, but necessary." 

112 ) 

113 

114 # make config, add to settings 

115 config_maker = config_maker or make_config 

116 wutta_config = config_maker(path, **kwargs) 

117 settings["wutta_config"] = wutta_config 

118 

119 # configure database sessions 

120 if hasattr(wutta_config, "appdb_engine"): 

121 wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine) 

122 

123 return wutta_config 

124 

125 

126def make_pyramid_config(settings): 

127 """ 

128 Make and return a Pyramid config object from the given settings. 

129 

130 The config is initialized with certain features deemed useful for 

131 all apps. 

132 

133 :returns: Instance of 

134 :class:`pyramid:pyramid.config.Configurator`. 

135 """ 

136 settings.setdefault("fanstatic.versioning", "true") 

137 settings.setdefault("mako.directories", ["wuttaweb:templates"]) 

138 settings.setdefault( 

139 "pyramid_deform.template_search_path", "wuttaweb:templates/deform" 

140 ) 

141 

142 # update settings per current theme 

143 establish_theme(settings) 

144 

145 pyramid_config = Configurator(settings=settings) 

146 

147 # configure user authorization / authentication 

148 pyramid_config.set_security_policy(WuttaSecurityPolicy()) 

149 

150 # require CSRF token for POST 

151 pyramid_config.set_default_csrf_options( 

152 require_csrf=True, token="_csrf", header="X-CSRF-TOKEN" 

153 ) 

154 

155 pyramid_config.include("pyramid_beaker") 

156 pyramid_config.include("pyramid_deform") 

157 pyramid_config.include("pyramid_fanstatic") 

158 pyramid_config.include("pyramid_mako") 

159 pyramid_config.include("pyramid_tm") 

160 

161 # add some permissions magic 

162 pyramid_config.add_directive( 

163 "add_wutta_permission_group", "wuttaweb.auth.add_permission_group" 

164 ) 

165 pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission") 

166 

167 # add some more config magic 

168 pyramid_config.add_directive( 

169 "add_wutta_master_view", "wuttaweb.conf.add_master_view" 

170 ) 

171 

172 return pyramid_config 

173 

174 

175def main(global_config, **settings): # pylint: disable=unused-argument 

176 """ 

177 Make and return the WSGI application, per given settings. 

178 

179 This function is designed to be called via Paste, hence it does 

180 require params and therefore can't be used directly as app factory 

181 for general WSGI servers. For the latter see 

182 :func:`make_wsgi_app()` instead. 

183 

184 And this *particular* function is not even that useful, it only 

185 constructs an app with minimal views built-in to WuttaWeb. Most 

186 apps will define their own ``main()`` function (e.g. as 

187 ``poser.web.app:main``), similar to this one but with additional 

188 views and other config. 

189 """ 

190 wutta_config = make_wutta_config(settings) # pylint: disable=unused-variable 

191 pyramid_config = make_pyramid_config(settings) 

192 

193 pyramid_config.include("wuttaweb.static") 

194 pyramid_config.include("wuttaweb.subscribers") 

195 pyramid_config.include("wuttaweb.views") 

196 

197 return pyramid_config.make_wsgi_app() 

198 

199 

200def make_wsgi_app(main_app=None, config=None): 

201 """ 

202 Make and return a WSGI app, using the given Paste app factory. 

203 

204 See also :func:`make_asgi_app()` for the ASGI equivalent. 

205 

206 This function could be used directly for general WSGI servers 

207 (e.g. uvicorn), ***if*** you just want the built-in :func:`main()` 

208 app factory. 

209 

210 But most likely you do not, in which case you must define your own 

211 function and call this one with your preferred app factory:: 

212 

213 from wuttaweb.app import make_wsgi_app 

214 

215 def my_main(global_config, **settings): 

216 # TODO: build your app 

217 pass 

218 

219 def make_my_wsgi_app(): 

220 return make_wsgi_app(my_main) 

221 

222 So ``make_my_wsgi_app()`` could then be used as-is for general 

223 WSGI servers. However, note that this approach will require 

224 setting the ``WUTTA_CONFIG_FILES`` environment variable, unless 

225 running via :ref:`wutta-webapp`. 

226 

227 :param main_app: Either a Paste-compatible app factory, or 

228 :term:`spec` for one. If not specified, the built-in 

229 :func:`main()` is assumed. 

230 

231 :param config: Optional :term:`config object`. If not specified, 

232 one is created based on ``WUTTA_CONFIG_FILES`` environment 

233 variable. 

234 """ 

235 if not config: 

236 config = make_config() 

237 app = config.get_app() 

238 

239 # extract pyramid settings 

240 settings = config.get_dict("app:main") 

241 

242 # keep same config object 

243 settings["wutta_config"] = config 

244 

245 # determine the app factory 

246 if isinstance(main_app, str): 

247 factory = app.load_object(main_app) 

248 elif callable(main_app): 

249 factory = main_app 

250 else: 

251 raise ValueError("main_app must be spec or callable") 

252 

253 # construct a pyramid app "per usual" 

254 return factory({}, **settings) 

255 

256 

257def make_asgi_app(main_app=None, config=None): 

258 """ 

259 Make and return a ASGI app, using the given Paste app factory. 

260 

261 This works the same as :func:`make_wsgi_app()` and should be 

262 called in the same way etc. 

263 """ 

264 wsgi_app = make_wsgi_app(main_app, config=config) 

265 return WsgiToAsgi(wsgi_app) 

266 

267 

268def establish_theme(settings): 

269 """ 

270 Establishes initial theme on app startup. This mostly involves 

271 updating the given ``settings`` dict. 

272 

273 This function is called automatically from within 

274 :func:`make_pyramid_config()`. 

275 

276 It will first call :func:`~wuttaweb.util.get_effective_theme()` to 

277 read the current theme from the :term:`settings table`, and store 

278 this within ``settings['wuttaweb.theme']``. 

279 

280 It then calls :func:`~wuttaweb.util.get_theme_template_path()` and 

281 will update ``settings['mako.directories']`` such that the theme's 

282 template path is listed first. 

283 """ 

284 config = settings["wutta_config"] 

285 

286 theme = get_effective_theme(config) 

287 settings["wuttaweb.theme"] = theme 

288 

289 directories = settings["mako.directories"] 

290 if isinstance(directories, str): 

291 directories = config.parse_list(directories) 

292 

293 path = get_theme_template_path(config) 

294 directories.insert(0, path) 

295 settings["mako.directories"] = directories