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

79 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 16:36 -0500

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 

40log = logging.getLogger(__name__) 

41 

42 

43class WebAppProvider(AppProvider): 

44 """ 

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

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

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

48 app, etc. 

49 """ 

50 

51 email_modules = ["wuttaweb.emails"] 

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

53 

54 def get_web_handler(self): 

55 """ 

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

57 

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

59 

60 .. code-block:: ini 

61 

62 [wutta] 

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

64 

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

66 """ 

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

68 spec = self.config.get( 

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

70 default="wuttaweb.handler:WebHandler", 

71 ) 

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

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

74 

75 

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

77 """ 

78 Make a WuttaConfig object from the given settings. 

79 

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

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

82 

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

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

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

86 an entry like this: 

87 

88 .. code-block:: ini 

89 

90 [app:main] 

91 wutta.config = %(__file__)s 

92 

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

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

95 minimum):: 

96 

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

98 

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

100 """ 

101 wutta_config = settings.get("wutta_config") 

102 if not wutta_config: 

103 

104 # validate config file path 

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

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

107 raise ValueError( 

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

109 "section of config to the path of your " 

110 "config file. Lame, but necessary." 

111 ) 

112 

113 # make config, add to settings 

114 config_maker = config_maker or make_config 

115 wutta_config = config_maker(path, **kwargs) 

116 settings["wutta_config"] = wutta_config 

117 

118 # configure database sessions 

119 if hasattr(wutta_config, "appdb_engine"): 

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

121 

122 return wutta_config 

123 

124 

125def make_pyramid_config(settings): 

126 """ 

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

128 

129 The config is initialized with certain features deemed useful for 

130 all apps. 

131 

132 :returns: Instance of 

133 :class:`pyramid:pyramid.config.Configurator`. 

134 """ 

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

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

137 settings.setdefault( 

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

139 ) 

140 

141 # update settings per current theme 

142 establish_theme(settings) 

143 

144 pyramid_config = Configurator(settings=settings) 

145 

146 # configure user authorization / authentication 

147 pyramid_config.set_security_policy(WuttaSecurityPolicy()) 

148 

149 # require CSRF token for POST 

150 pyramid_config.set_default_csrf_options( 

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

152 ) 

153 

154 pyramid_config.include("pyramid_beaker") 

155 pyramid_config.include("pyramid_deform") 

156 pyramid_config.include("pyramid_fanstatic") 

157 pyramid_config.include("pyramid_mako") 

158 pyramid_config.include("pyramid_tm") 

159 

160 # add some permissions magic 

161 pyramid_config.add_directive( 

162 "add_wutta_permission_group", "wuttaweb.auth.add_permission_group" 

163 ) 

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

165 

166 # add some more config magic 

167 pyramid_config.add_directive( 

168 "add_wutta_master_view", "wuttaweb.conf.add_master_view" 

169 ) 

170 

171 return pyramid_config 

172 

173 

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

175 """ 

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

177 

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

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

180 for general WSGI servers. For the latter see 

181 :func:`make_wsgi_app()` instead. 

182 

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

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

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

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

187 views and other config. 

188 """ 

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

190 pyramid_config = make_pyramid_config(settings) 

191 

192 pyramid_config.include("wuttaweb.static") 

193 pyramid_config.include("wuttaweb.subscribers") 

194 pyramid_config.include("wuttaweb.views") 

195 

196 return pyramid_config.make_wsgi_app() 

197 

198 

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

200 """ 

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

202 

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

204 

205 This function could be used directly for general WSGI servers 

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

207 app factory. 

208 

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

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

211 

212 from wuttaweb.app import make_wsgi_app 

213 

214 def my_main(global_config, **settings): 

215 # TODO: build your app 

216 pass 

217 

218 def make_my_wsgi_app(): 

219 return make_wsgi_app(my_main) 

220 

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

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

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

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

225 

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

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

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

229 

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

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

232 variable. 

233 """ 

234 if not config: 

235 config = make_config() 

236 app = config.get_app() 

237 

238 # extract pyramid settings 

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

240 

241 # keep same config object 

242 settings["wutta_config"] = config 

243 

244 # determine the app factory 

245 if isinstance(main_app, str): 

246 factory = app.load_object(main_app) 

247 elif callable(main_app): 

248 factory = main_app 

249 else: 

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

251 

252 # construct a pyramid app "per usual" 

253 return factory({}, **settings) 

254 

255 

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

257 """ 

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

259 

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

261 called in the same way etc. 

262 """ 

263 wsgi_app = make_wsgi_app(main_app, config=config) 

264 return WsgiToAsgi(wsgi_app) 

265 

266 

267def establish_theme(settings): 

268 """ 

269 Establishes initial theme on app startup. This mostly involves 

270 updating the given ``settings`` dict. 

271 

272 This function is called automatically from within 

273 :func:`make_pyramid_config()`. 

274 

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

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

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

278 

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

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

281 template path is listed first. 

282 """ 

283 config = settings["wutta_config"] 

284 

285 theme = get_effective_theme(config) 

286 settings["wuttaweb.theme"] = theme 

287 

288 directories = settings["mako.directories"] 

289 if isinstance(directories, str): 

290 directories = config.parse_list(directories) 

291 

292 path = get_theme_template_path(config) 

293 directories.insert(0, path) 

294 settings["mako.directories"] = directories