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
« 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"""
27import logging
28import os
30from wuttjamaican.app import AppProvider
31from wuttjamaican.conf import make_config
33from asgiref.wsgi import WsgiToAsgi
34from pyramid.config import Configurator
36import wuttaweb.db
37from wuttaweb.auth import WuttaSecurityPolicy
38from wuttaweb.util import get_effective_theme, get_theme_template_path
40log = logging.getLogger(__name__)
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 """
51 email_modules = ["wuttaweb.emails"]
52 email_templates = ["wuttaweb:email-templates"]
54 def get_web_handler(self):
55 """
56 Get the configured "web" handler for the app.
58 Specify a custom handler in your config file like this:
60 .. code-block:: ini
62 [wutta]
63 web.handler_spec = poser.web.handler:PoserWebHandler
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"]
76def make_wutta_config(settings, config_maker=None, **kwargs):
77 """
78 Make a WuttaConfig object from the given settings.
80 Note that ``settings`` dict will (typically) correspond to the
81 ``[app:main]`` section of your config file.
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:
88 .. code-block:: ini
90 [app:main]
91 wutta.config = %(__file__)s
93 The ``%(__file__)s`` is auto-replaced with the config file path,
94 so ultimately ``settings`` would contain something like (at
95 minimum)::
97 {'wutta.config': '/path/to/config/file'}
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:
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 )
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
118 # configure database sessions
119 if hasattr(wutta_config, "appdb_engine"):
120 wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine)
122 return wutta_config
125def make_pyramid_config(settings):
126 """
127 Make and return a Pyramid config object from the given settings.
129 The config is initialized with certain features deemed useful for
130 all apps.
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 )
141 # update settings per current theme
142 establish_theme(settings)
144 pyramid_config = Configurator(settings=settings)
146 # configure user authorization / authentication
147 pyramid_config.set_security_policy(WuttaSecurityPolicy())
149 # require CSRF token for POST
150 pyramid_config.set_default_csrf_options(
151 require_csrf=True, token="_csrf", header="X-CSRF-TOKEN"
152 )
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")
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")
166 # add some more config magic
167 pyramid_config.add_directive(
168 "add_wutta_master_view", "wuttaweb.conf.add_master_view"
169 )
171 return pyramid_config
174def main(global_config, **settings): # pylint: disable=unused-argument
175 """
176 Make and return the WSGI application, per given settings.
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.
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)
192 pyramid_config.include("wuttaweb.static")
193 pyramid_config.include("wuttaweb.subscribers")
194 pyramid_config.include("wuttaweb.views")
196 return pyramid_config.make_wsgi_app()
199def make_wsgi_app(main_app=None, config=None):
200 """
201 Make and return a WSGI app, using the given Paste app factory.
203 See also :func:`make_asgi_app()` for the ASGI equivalent.
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.
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::
212 from wuttaweb.app import make_wsgi_app
214 def my_main(global_config, **settings):
215 # TODO: build your app
216 pass
218 def make_my_wsgi_app():
219 return make_wsgi_app(my_main)
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`.
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.
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()
238 # extract pyramid settings
239 settings = config.get_dict("app:main")
241 # keep same config object
242 settings["wutta_config"] = config
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")
252 # construct a pyramid app "per usual"
253 return factory({}, **settings)
256def make_asgi_app(main_app=None, config=None):
257 """
258 Make and return a ASGI app, using the given Paste app factory.
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)
267def establish_theme(settings):
268 """
269 Establishes initial theme on app startup. This mostly involves
270 updating the given ``settings`` dict.
272 This function is called automatically from within
273 :func:`make_pyramid_config()`.
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']``.
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"]
285 theme = get_effective_theme(config)
286 settings["wuttaweb.theme"] = theme
288 directories = settings["mako.directories"]
289 if isinstance(directories, str):
290 directories = config.parse_list(directories)
292 path = get_theme_template_path(config)
293 directories.insert(0, path)
294 settings["mako.directories"] = directories