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

66 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2024-12-28 21:19 -0600

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

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

3# 

4# wuttaweb -- Web App for Wutta Framework 

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

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 

38 

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 email_modules = ['wuttaweb.emails'] 

51 email_templates = ['wuttaweb:email-templates'] 

52 

53 def get_web_handler(self, **kwargs): 

54 """ 

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

56 

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

58 

59 .. code-block:: ini 

60 

61 [wutta] 

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

63 

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

65 """ 

66 if 'web_handler' not in self.__dict__: 

67 spec = self.config.get(f'{self.appname}.web.handler_spec', 

68 default='wuttaweb.handler:WebHandler') 

69 self.web_handler = self.app.load_object(spec)(self.config) 

70 return self.web_handler 

71 

72 

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

74 """ 

75 Make a WuttaConfig object from the given settings. 

76 

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

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

79 

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

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

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

83 an entry like this: 

84 

85 .. code-block:: ini 

86 

87 [app:main] 

88 wutta.config = %(__file__)s 

89 

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

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

92 minimum):: 

93 

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

95 

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

97 """ 

98 wutta_config = settings.get('wutta_config') 

99 if not wutta_config: 

100 

101 # validate config file path 

102 path = settings.get('wutta.config') 

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

104 raise ValueError("Please set 'wutta.config' in [app:main] " 

105 "section of config to the path of your " 

106 "config file. Lame, but necessary.") 

107 

108 # make config, add to settings 

109 config_maker = config_maker or make_config 

110 wutta_config = config_maker(path, **kwargs) 

111 settings['wutta_config'] = wutta_config 

112 

113 # configure database sessions 

114 if hasattr(wutta_config, 'appdb_engine'): 

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

116 

117 return wutta_config 

118 

119 

120def make_pyramid_config(settings): 

121 """ 

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

123 

124 The config is initialized with certain features deemed useful for 

125 all apps. 

126 

127 :returns: Instance of 

128 :class:`pyramid:pyramid.config.Configurator`. 

129 """ 

130 settings.setdefault('fanstatic.versioning', 'true') 

131 settings.setdefault('mako.directories', ['wuttaweb:templates']) 

132 settings.setdefault('pyramid_deform.template_search_path', 

133 'wuttaweb:templates/deform') 

134 

135 pyramid_config = Configurator(settings=settings) 

136 

137 # configure user authorization / authentication 

138 pyramid_config.set_security_policy(WuttaSecurityPolicy()) 

139 

140 # require CSRF token for POST 

141 pyramid_config.set_default_csrf_options(require_csrf=True, 

142 token='_csrf', 

143 header='X-CSRF-TOKEN') 

144 

145 pyramid_config.include('pyramid_beaker') 

146 pyramid_config.include('pyramid_deform') 

147 pyramid_config.include('pyramid_fanstatic') 

148 pyramid_config.include('pyramid_mako') 

149 pyramid_config.include('pyramid_tm') 

150 

151 # add some permissions magic 

152 pyramid_config.add_directive('add_wutta_permission_group', 

153 'wuttaweb.auth.add_permission_group') 

154 pyramid_config.add_directive('add_wutta_permission', 

155 'wuttaweb.auth.add_permission') 

156 

157 return pyramid_config 

158 

159 

160def main(global_config, **settings): 

161 """ 

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

163 

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

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

166 for general WSGI servers. For the latter see 

167 :func:`make_wsgi_app()` instead. 

168 

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

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

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

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

173 views and other config. 

174 """ 

175 wutta_config = make_wutta_config(settings) 

176 pyramid_config = make_pyramid_config(settings) 

177 

178 pyramid_config.include('wuttaweb.static') 

179 pyramid_config.include('wuttaweb.subscribers') 

180 pyramid_config.include('wuttaweb.views') 

181 

182 return pyramid_config.make_wsgi_app() 

183 

184 

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

186 """ 

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

188 

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

190 

191 This function could be used directly for general WSGI servers 

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

193 app factory. 

194 

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

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

197 

198 from wuttaweb.app import make_wsgi_app 

199 

200 def my_main(global_config, **settings): 

201 # TODO: build your app 

202 pass 

203 

204 def make_my_wsgi_app(): 

205 return make_wsgi_app(my_main) 

206 

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

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

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

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

211 

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

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

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

215 

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

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

218 variable. 

219 """ 

220 if not config: 

221 config = make_config() 

222 app = config.get_app() 

223 

224 # extract pyramid settings 

225 settings = config.get_dict('app:main') 

226 

227 # keep same config object 

228 settings['wutta_config'] = config 

229 

230 # determine the app factory 

231 if isinstance(main_app, str): 

232 make_wsgi_app = app.load_object(main_app) 

233 elif callable(main_app): 

234 make_wsgi_app = main_app 

235 else: 

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

237 

238 # construct a pyramid app "per usual" 

239 return make_wsgi_app({}, **settings) 

240 

241 

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

243 """ 

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

245 

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

247 called in the same way etc. 

248 """ 

249 wsgi_app = make_wsgi_app(main_app, config=config) 

250 return WsgiToAsgi(wsgi_app)