Coverage for .tox/coverage/lib/python3.11/site-packages/wuttjamaican/app.py: 100%
67 statements
« prev ^ index » next coverage.py v7.3.2, created at 2024-05-15 14:25 -0500
« prev ^ index » next coverage.py v7.3.2, created at 2024-05-15 14:25 -0500
1# -*- coding: utf-8; -*-
2################################################################################
3#
4# WuttJamaican -- Base package for Wutta Framework
5# Copyright © 2023-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"""
24WuttJamaican - app handler
25"""
27import os
28import warnings
30from wuttjamaican.util import load_entry_points, load_object, parse_bool
33class AppHandler:
34 """
35 Base class and default implementation for top-level :term:`app
36 handler`.
38 aka. "the handler to handle all handlers"
40 aka. "one handler to bind them all"
42 For more info see :doc:`/narr/handlers/app`.
44 There is normally no need to create one of these yourself; rather
45 you should call :meth:`~wuttjamaican.conf.WuttaConfig.get_app()`
46 on the :term:`config object` if you need the app handler.
48 :param config: Config object for the app. This should be an
49 instance of :class:`~wuttjamaican.conf.WuttaConfig`.
51 .. attribute:: providers
53 Dictionary of :class:`AppProvider` instances, as returned by
54 :meth:`get_all_providers()`.
55 """
57 def __init__(self, config):
58 self.config = config
59 self.handlers = {}
61 @property
62 def appname(self):
63 """
64 The :term:`app name` for the current app. This is just an
65 alias for :attr:`wuttjamaican.conf.WuttaConfig.appname`.
67 Note that this ``appname`` does not necessariy reflect what
68 you think of as the name of your (e.g. custom) app. It is
69 more fundamental than that; your Python package naming and the
70 :term:`app title` are free to use a different name as their
71 basis.
72 """
73 return self.config.appname
75 def __getattr__(self, name):
76 """
77 Custom attribute getter, called when the app handler does not
78 already have an attribute named with ``name``.
80 This will delegate to the set of :term:`app providers<app
81 provider>`; the first provider with an appropriately-named
82 attribute wins, and that value is returned.
84 :returns: The first value found among the set of app
85 providers.
86 """
88 if name == 'providers':
89 self.providers = self.get_all_providers()
90 return self.providers
92 # if 'providers' not in self.__dict__:
93 # self.__dict__['providers'] = self.get_all_providers()
95 for provider in self.providers.values():
96 if hasattr(provider, name):
97 return getattr(provider, name)
99 raise AttributeError(f"attr not found: {name}")
101 def get_all_providers(self):
102 """
103 Load and return all registered providers.
105 Note that you do not need to call this directly; instead just
106 use :attr:`providers`.
108 :returns: Dictionary keyed by entry point name; values are
109 :class:`AppProvider` *instances*.
110 """
111 providers = load_entry_points(f'{self.appname}.providers')
112 for key in list(providers):
113 providers[key] = providers[key](self.config)
114 return providers
116 def make_appdir(self, path, subfolders=None, **kwargs):
117 """
118 Establish an :term:`app dir` at the given path.
120 Default logic only creates a few subfolders, meant to help
121 steer the admin toward a convention for sake of where to put
122 things. But custom app handlers are free to do whatever.
124 :param path: Path to the desired app dir. If the path does
125 not yet exist then it will be created. But regardless it
126 should be "refreshed" (e.g. missing subfolders created)
127 when this method is called.
129 :param subfolders: Optional list of subfolder names to create
130 within the app dir. If not specified, defaults will be:
131 ``['data', 'log', 'work']``.
132 """
133 appdir = path
134 if not os.path.exists(appdir):
135 os.makedirs(appdir)
137 if not subfolders:
138 subfolders = ['data', 'log', 'work']
140 for name in subfolders:
141 path = os.path.join(appdir, name)
142 if not os.path.exists(path):
143 os.mkdir(path)
145 def make_engine_from_config(
146 self,
147 config_dict,
148 prefix='sqlalchemy.',
149 **kwargs):
150 """
151 Construct a new DB engine from configuration dict.
153 This is a wrapper around upstream
154 :func:`sqlalchemy:sqlalchemy.engine_from_config()`. For even
155 broader context of the SQLAlchemy
156 :class:`~sqlalchemy:sqlalchemy.engine.Engine` and their
157 configuration, see :doc:`sqlalchemy:core/engines`.
159 The purpose of the customization is to allow certain
160 attributes of the engine to be driven by config, whereas the
161 upstream function is more limited in that regard. The
162 following in particular:
164 * ``poolclass``
165 * ``pool_pre_ping``
167 If these options are present in the configuration dict, they
168 will be coerced to appropriate Python equivalents and then
169 passed as kwargs to the upstream function.
171 An example config file leveraging this feature:
173 .. code-block:: ini
175 [wutta.db]
176 default.url = sqlite:///tmp/default.sqlite
177 default.poolclass = sqlalchemy.pool:NullPool
178 default.pool_pre_ping = true
180 Note that if present, the ``poolclass`` value must be a "spec"
181 string, as required by
182 :func:`~wuttjamaican.util.load_object()`.
183 """
184 import sqlalchemy as sa
186 config_dict = dict(config_dict)
188 # convert 'poolclass' arg to actual class
189 key = f'{prefix}poolclass'
190 if key in config_dict and 'poolclass' not in kwargs:
191 kwargs['poolclass'] = load_object(config_dict.pop(key))
193 # convert 'pool_pre_ping' arg to boolean
194 key = f'{prefix}pool_pre_ping'
195 if key in config_dict and 'pool_pre_ping' not in kwargs:
196 kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key))
198 engine = sa.engine_from_config(config_dict, prefix, **kwargs)
200 return engine
202 def make_session(self, **kwargs):
203 """
204 Creates a new SQLAlchemy session for the app DB. By default
205 this will create a new :class:`~wuttjamaican.db.sess.Session`
206 instance.
208 :returns: SQLAlchemy session for the app DB.
209 """
210 from .db import Session
212 return Session(**kwargs)
214 def short_session(self, **kwargs):
215 """
216 Returns a context manager for a short-lived database session.
218 This is a convenience wrapper around
219 :class:`~wuttjamaican.db.sess.short_session`.
221 If caller does not specify ``factory`` nor ``config`` params,
222 this method will provide a default factory in the form of
223 :meth:`make_session`.
224 """
225 from .db import short_session
227 if 'factory' not in kwargs and 'config' not in kwargs:
228 kwargs['factory'] = self.make_session
230 return short_session(**kwargs)
232 def get_setting(self, session, name, **kwargs):
233 """
234 Get a setting value from the DB.
236 This does *not* consult the config object directly to
237 determine the setting value; it always queries the DB.
239 Default implementation is just a convenience wrapper around
240 :func:`~wuttjamaican.db.conf.get_setting()`.
242 :param session: App DB session.
244 :param name: Name of the setting to get.
246 :returns: Setting value as string, or ``None``.
247 """
248 from .db import get_setting
250 return get_setting(session, name)
253class AppProvider:
254 """
255 Base class for :term:`app providers<app provider>`.
257 These can add arbitrary extra functionality to the main :term:`app
258 handler`. See also :doc:`/narr/providers/app`.
260 :param config: Config object for the app. This should be an
261 instance of :class:`~wuttjamaican.conf.WuttaConfig`.
263 Instances have the following attributes:
265 .. attribute:: config
267 Reference to the config object.
269 .. attribute:: app
271 Reference to the parent app handler.
272 """
274 def __init__(self, config):
276 if isinstance(config, AppHandler):
277 warnings.warn("passing app handler to app provider is deprecated; "
278 "must pass config object instead",
279 DeprecationWarning, stacklevel=2)
280 config = config.config
282 self.config = config
283 self.app = config.get_app()
286class GenericHandler:
287 """
288 Generic base class for handlers.
290 When the :term:`app` defines a new *type* of :term:`handler` it
291 may subclass this when defining the handler base class.
293 :param config: Config object for the app. This should be an
294 instance of :class:`~wuttjamaican.conf.WuttaConfig`.
295 """
297 def __init__(self, config, **kwargs):
298 self.config = config
299 self.app = self.config.get_app()