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
« 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"""
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
41log = logging.getLogger(__name__)
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 """
52 email_modules = ["wuttaweb.emails"]
53 email_templates = ["wuttaweb:email-templates"]
55 def get_web_handler(self):
56 """
57 Get the configured "web" handler for the app.
59 Specify a custom handler in your config file like this:
61 .. code-block:: ini
63 [wutta]
64 web.handler_spec = poser.web.handler:PoserWebHandler
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"]
77def make_wutta_config(settings, config_maker=None, **kwargs):
78 """
79 Make a WuttaConfig object from the given settings.
81 Note that ``settings`` dict will (typically) correspond to the
82 ``[app:main]`` section of your config file.
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:
89 .. code-block:: ini
91 [app:main]
92 wutta.config = %(__file__)s
94 The ``%(__file__)s`` is auto-replaced with the config file path,
95 so ultimately ``settings`` would contain something like (at
96 minimum)::
98 {'wutta.config': '/path/to/config/file'}
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:
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 )
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
119 # configure database sessions
120 if hasattr(wutta_config, "appdb_engine"):
121 wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine)
123 return wutta_config
126def make_pyramid_config(settings):
127 """
128 Make and return a Pyramid config object from the given settings.
130 The config is initialized with certain features deemed useful for
131 all apps.
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 )
142 # update settings per current theme
143 establish_theme(settings)
145 pyramid_config = Configurator(settings=settings)
147 # configure user authorization / authentication
148 pyramid_config.set_security_policy(WuttaSecurityPolicy())
150 # require CSRF token for POST
151 pyramid_config.set_default_csrf_options(
152 require_csrf=True, token="_csrf", header="X-CSRF-TOKEN"
153 )
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")
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")
167 # add some more config magic
168 pyramid_config.add_directive(
169 "add_wutta_master_view", "wuttaweb.conf.add_master_view"
170 )
172 return pyramid_config
175def main(global_config, **settings): # pylint: disable=unused-argument
176 """
177 Make and return the WSGI application, per given settings.
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.
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)
193 pyramid_config.include("wuttaweb.static")
194 pyramid_config.include("wuttaweb.subscribers")
195 pyramid_config.include("wuttaweb.views")
197 return pyramid_config.make_wsgi_app()
200def make_wsgi_app(main_app=None, config=None):
201 """
202 Make and return a WSGI app, using the given Paste app factory.
204 See also :func:`make_asgi_app()` for the ASGI equivalent.
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.
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::
213 from wuttaweb.app import make_wsgi_app
215 def my_main(global_config, **settings):
216 # TODO: build your app
217 pass
219 def make_my_wsgi_app():
220 return make_wsgi_app(my_main)
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`.
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.
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()
239 # extract pyramid settings
240 settings = config.get_dict("app:main")
242 # keep same config object
243 settings["wutta_config"] = config
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")
253 # construct a pyramid app "per usual"
254 return factory({}, **settings)
257def make_asgi_app(main_app=None, config=None):
258 """
259 Make and return a ASGI app, using the given Paste app factory.
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)
268def establish_theme(settings):
269 """
270 Establishes initial theme on app startup. This mostly involves
271 updating the given ``settings`` dict.
273 This function is called automatically from within
274 :func:`make_pyramid_config()`.
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']``.
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"]
286 theme = get_effective_theme(config)
287 settings["wuttaweb.theme"] = theme
289 directories = settings["mako.directories"]
290 if isinstance(directories, str):
291 directories = config.parse_list(directories)
293 path = get_theme_template_path(config)
294 directories.insert(0, path)
295 settings["mako.directories"] = directories