First commit, basic config (with db) and app handler
this has 100% test coverage and i intend to keep it that way. api docs have a good start but still need narrative. several more things must be added before i can seriously consider incorporating into rattail but this seemed a good save point
This commit is contained in:
		
						commit
						5c3c42d6b3
					
				
					 36 changed files with 3322 additions and 0 deletions
				
			
		
							
								
								
									
										39
									
								
								src/wuttjamaican/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/wuttjamaican/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - base package for Wutta Framework | ||||
| 
 | ||||
| There is just one function exposed in the root namespace: | ||||
| :func:`~wuttjamaican.conf.make_config()` | ||||
| 
 | ||||
| Typical usage is something like:: | ||||
| 
 | ||||
|    import wuttjamaican as wj | ||||
| 
 | ||||
|    config = wj.make_config(appname='poser') | ||||
|    app = config.get_app() | ||||
| """ | ||||
| 
 | ||||
| from ._version import __version__ | ||||
| 
 | ||||
| from .conf import make_config | ||||
							
								
								
									
										3
									
								
								src/wuttjamaican/_version.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/wuttjamaican/_version.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| 
 | ||||
| __version__ = '0.1.0' | ||||
							
								
								
									
										98
									
								
								src/wuttjamaican/app.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/wuttjamaican/app.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,98 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - app handler | ||||
| """ | ||||
| 
 | ||||
| from wuttjamaican.util import load_entry_points | ||||
| 
 | ||||
| 
 | ||||
| class AppHandler: | ||||
|     """ | ||||
|     Base class and default implementation for top-level app handler. | ||||
| 
 | ||||
|     aka. "the handler to handle all handlers" | ||||
| 
 | ||||
|     aka. "one handler to bind them all" | ||||
| 
 | ||||
|     There is normally no need to create one of these yourself; rather | ||||
|     you should call :meth:`~wuttjamaican.conf.WuttaConfig.get_app()` | ||||
|     on the config object if you need the app handler. | ||||
| 
 | ||||
|     :param config: Config object for the app.  This should be an | ||||
|        instance of :class:`~wuttjamaican.conf.WuttaConfig`. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, config): | ||||
|         self.config = config | ||||
|         self.handlers = {} | ||||
| 
 | ||||
|     def make_session(self, **kwargs): | ||||
|         """ | ||||
|         Creates a new SQLAlchemy session for the app DB.  By default | ||||
|         this will create a new :class:`~wuttjamaican.db.sess.Session` | ||||
|         instance. | ||||
| 
 | ||||
|         :returns: SQLAlchemy session for the app DB. | ||||
|         """ | ||||
|         from .db import Session | ||||
| 
 | ||||
|         return Session(**kwargs) | ||||
| 
 | ||||
|     def short_session(self, **kwargs): | ||||
|         """ | ||||
|         Returns a context manager for a short-lived database session. | ||||
| 
 | ||||
|         This is a convenience wrapper around | ||||
|         :class:`~wuttjamaican.db.sess.short_session`. | ||||
| 
 | ||||
|         If caller does not specify ``factory`` nor ``config`` params, | ||||
|         this method will provide a default factory in the form of | ||||
|         :meth:`make_session`. | ||||
|         """ | ||||
|         from .db import short_session | ||||
| 
 | ||||
|         if 'factory' not in kwargs and 'config' not in kwargs: | ||||
|             kwargs['factory'] = self.make_session | ||||
| 
 | ||||
|         return short_session(**kwargs) | ||||
| 
 | ||||
|     def get_setting(self, session, name, **kwargs): | ||||
|         """ | ||||
|         Get a setting value from the DB. | ||||
| 
 | ||||
|         This does *not* consult the config object directly to | ||||
|         determine the setting value; it always queries the DB. | ||||
| 
 | ||||
|         Default implementation is just a convenience wrapper around | ||||
|         :func:`~wuttjamaican.db.conf.get_setting()`. | ||||
| 
 | ||||
|         :param session: App DB session. | ||||
| 
 | ||||
|         :param name: Name of the setting to get. | ||||
| 
 | ||||
|         :returns: Setting value as string, or ``None``. | ||||
|         """ | ||||
|         from .db import get_setting | ||||
| 
 | ||||
|         return get_setting(session, name) | ||||
							
								
								
									
										649
									
								
								src/wuttjamaican/conf.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										649
									
								
								src/wuttjamaican/conf.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,649 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - app configuration | ||||
| """ | ||||
| 
 | ||||
| import configparser | ||||
| import importlib | ||||
| import logging | ||||
| import os | ||||
| import sys | ||||
| import tempfile | ||||
| 
 | ||||
| import config as configuration | ||||
| 
 | ||||
| from wuttjamaican.util import (load_entry_points, load_object, | ||||
|                                parse_bool, parse_list, | ||||
|                                UNSPECIFIED) | ||||
| from wuttjamaican.exc import ConfigurationError | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| class WuttaConfig: | ||||
|     """ | ||||
|     Configuration class for Wutta Framework | ||||
| 
 | ||||
|     A single instance of this class is typically created on app | ||||
|     startup, by calling :func:`make_config()`. | ||||
| 
 | ||||
|     The global config object is mainly responsible for providing | ||||
|     config values to the app, via :meth:`get()` and similar methods. | ||||
| 
 | ||||
|     The config object may have more than one place to look when | ||||
|     finding values.  This can vary somewhat but often the priority for | ||||
|     lookup is like: | ||||
| 
 | ||||
|     * settings table in the DB | ||||
|     * one or more INI files | ||||
|     * "defaults" provided by app logic | ||||
| 
 | ||||
|     :param files: List of file paths from which to read config values. | ||||
| 
 | ||||
|     :param appname: This string forms the basis of certain other | ||||
|        things, namely some of the config keys which will be checked to | ||||
|        determine default behavior of the config object itself (if they | ||||
|        are not specified via constructor). | ||||
| 
 | ||||
|     :param usedb: Flag indicating whether config values should ever be | ||||
|        looked up from the DB.  Note that you can override this when | ||||
|        calling :meth:`get()`. | ||||
| 
 | ||||
|     :param preferdb: Flag indicating whether values from DB should be | ||||
|        preferred over the values from INI files or app defaults.  Note | ||||
|        that you can override this when calling :meth:`get()`. | ||||
| 
 | ||||
|     Attributes available on the config instance: | ||||
| 
 | ||||
|     .. attribute:: configuration | ||||
| 
 | ||||
|        Reference to the | ||||
|        :class:`python-configuration:config.ConfigurationSet` instance | ||||
|        which houses the full set of config values which are kept in | ||||
|        memory.  This does *not* contain settings from DB, but *does* | ||||
|        contain :attr:`defaults` as well as values read from INI files. | ||||
| 
 | ||||
|     .. attribute:: defaults | ||||
| 
 | ||||
|        Reference to the | ||||
|        :class:`python-configuration:config.Configuration` instance | ||||
|        containing config *default* values.  This is exposed in case | ||||
|        it's useful, but in practice you should not update it directly; | ||||
|        instead use :meth:`setdefault()`. | ||||
| 
 | ||||
|     .. attribute:: files_read | ||||
| 
 | ||||
|        List of all INI config files which were read on app startup. | ||||
|        These are listed in the same order as they were read.  This | ||||
|        sequence also reflects priority for value lookups, i.e. the | ||||
|        first file with the value wins. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__( | ||||
|             self, | ||||
|             files=[], | ||||
|             defaults={}, | ||||
|             appname='wutta', | ||||
|             usedb=None, | ||||
|             preferdb=None, | ||||
|             configure_logging=None, | ||||
|     ): | ||||
|         self.appname = appname | ||||
|         configs = [] | ||||
| 
 | ||||
|         # read all files requested | ||||
|         self.files_read = [] | ||||
|         for path in files: | ||||
|             self._load_ini_configs(path, configs, require=True) | ||||
|         log.debug("config files were: %s", self.files_read) | ||||
| 
 | ||||
|         # add config for use w/ setdefault() | ||||
|         self.defaults = configuration.Configuration(defaults) | ||||
|         configs.append(self.defaults) | ||||
| 
 | ||||
|         # master config set | ||||
|         self.configuration = configuration.ConfigurationSet(*configs) | ||||
| 
 | ||||
|         # establish logging | ||||
|         if configure_logging is None: | ||||
|             configure_logging = self.get_bool(f'{self.appname}.config.configure_logging', | ||||
|                                               default=False, usedb=False) | ||||
|         if configure_logging: | ||||
|             self._configure_logging() | ||||
| 
 | ||||
|         # usedb flag | ||||
|         self.usedb = usedb | ||||
|         if self.usedb is None: | ||||
|             self.usedb = self.get_bool(f'{self.appname}.config.usedb', | ||||
|                                        default=False, usedb=False) | ||||
| 
 | ||||
|         # preferdb flag | ||||
|         self.preferdb = preferdb | ||||
|         if self.usedb and self.preferdb is None: | ||||
|             self.preferdb = self.get_bool(f'{self.appname}.config.preferdb', | ||||
|                                           default=False, usedb=False) | ||||
| 
 | ||||
|         # configure main app DB if applicable, or disable usedb flag | ||||
|         try: | ||||
|             from .db import Session, get_engines | ||||
|         except ImportError: | ||||
|             if self.usedb: | ||||
|                 log.warning("config created with `usedb = True`, but can't import " | ||||
|                             "DB module(s), so setting `usedb = False` instead", | ||||
|                             exc_info=True) | ||||
|                 self.usedb = False | ||||
|             self.preferdb = False | ||||
|         else: | ||||
|             self.appdb_engines = get_engines(self, f'{self.appname}.db') | ||||
|             self.appdb_engine = self.appdb_engines.get('default') | ||||
|             Session.configure(bind=self.appdb_engine) | ||||
| 
 | ||||
|         log.debug("config files read: %s", self.files_read) | ||||
| 
 | ||||
|     def _load_ini_configs(self, path, configs, require=True): | ||||
|         path = os.path.abspath(path) | ||||
| 
 | ||||
|         # try to load config from the given path | ||||
|         try: | ||||
|             config = configuration.config_from_ini(path, read_from_file=True) | ||||
|         except FileNotFoundError: | ||||
|             if not require: | ||||
|                 log.warning("INI config file not found: %s", path) | ||||
|                 return | ||||
|             raise | ||||
| 
 | ||||
|         # ok add that one to the mix | ||||
|         configs.append(config) | ||||
|         self.files_read.append(path) | ||||
| 
 | ||||
|         # need parent folder of that path, for %(here)s interpolation | ||||
|         here = os.path.dirname(path) | ||||
| 
 | ||||
|         # bring in any "required" files | ||||
|         requires = config.get(f'{self.appname}.config.require') | ||||
|         if requires: | ||||
|             for path in parse_list(requires): | ||||
|                 path = path % {'here': here} | ||||
|                 self._load_ini_configs(path, configs, require=True) | ||||
| 
 | ||||
|         # bring in any "included" files | ||||
|         includes = config.get(f'{self.appname}.config.include') | ||||
|         if includes: | ||||
|             for path in parse_list(includes): | ||||
|                 path = path % {'here': here} | ||||
|                 self._load_ini_configs(path, configs, require=False) | ||||
| 
 | ||||
|     def setdefault( | ||||
|             self, | ||||
|             key, | ||||
|             value): | ||||
|         """ | ||||
|         Establish a default config value for the given key. | ||||
| 
 | ||||
|         Note that there is only *one* default value per key.  If | ||||
|         multiple calls are made with the same key, the first will set | ||||
|         the default and subsequent calls have no effect. | ||||
| 
 | ||||
|         :returns: The current config value, *outside of the DB*.  For | ||||
|            various reasons this method may not be able to lookup | ||||
|            settings from the DB, e.g. during app init.  So it can only | ||||
|            determine the value per INI files + config defaults. | ||||
|         """ | ||||
|         # set default value, if not already set | ||||
|         self.defaults.setdefault(key, value) | ||||
| 
 | ||||
|         # get current value, sans db | ||||
|         return self.get(key, usedb=False) | ||||
| 
 | ||||
|     def get( | ||||
|             self, | ||||
|             key, | ||||
|             default=UNSPECIFIED, | ||||
|             require=False, | ||||
|             message=None, | ||||
|             usedb=None, | ||||
|             preferdb=None, | ||||
|             session=None, | ||||
|     ): | ||||
|         """ | ||||
|         Retrieve a string value from config. | ||||
| 
 | ||||
|         .. warning:: | ||||
| 
 | ||||
|             While the point of this method is to return a *string* | ||||
|             value, it is possible for a key to be present in config | ||||
|             which corresponds to a "subset" of the config, and not a | ||||
|             simple value.  For instance with this config file: | ||||
| 
 | ||||
|             .. code-block:: ini | ||||
| 
 | ||||
|                [foo] | ||||
|                bar = 1 | ||||
|                bar.baz = 2 | ||||
| 
 | ||||
|             If you invoke ``config.get('foo.bar')`` the return value | ||||
|             is somewhat ambiguous.  At first glance it should return | ||||
|             ``'1'`` - but just as valid would be to return the dict:: | ||||
| 
 | ||||
|                {'baz': '2'} | ||||
| 
 | ||||
|             And similarly, if you invoke ``config.get('foo')`` then | ||||
|             the return value "should be" the dict:: | ||||
| 
 | ||||
|                {'bar': '1', | ||||
|                 'bar.baz': '2'} | ||||
| 
 | ||||
|             Despite all that ambiguity, again the whole point of this | ||||
|             method is to return a *string* value, only.  Therefore in | ||||
|             any case where the return value "should be" a dict, per | ||||
|             logic described above, this method will *ignore* that and | ||||
|             simply return ``None`` (or rather the ``default`` value). | ||||
| 
 | ||||
|             It is important also to understand that in fact, there is | ||||
|             no "real" ambiguity per se, but rather a dict (subset) | ||||
|             would always get priority over a simple string value.  So | ||||
|             in the first example above, ``config.get('foo.bar')`` will | ||||
|             always return the ``default`` value.  The string value | ||||
|             ``'1'`` will never be returned since the dict/subset | ||||
|             overshadows it, and this method will only return the | ||||
|             default value in lieu of any dict. | ||||
| 
 | ||||
|         :param key: String key for which value should be returned. | ||||
| 
 | ||||
|         :param default: Default value to be returned, if config does | ||||
|            not contain the key.  If no default is specified, ``None`` | ||||
|            will be assumed. | ||||
| 
 | ||||
|         :param require: If set, an error will be raised if config does | ||||
|            not contain the key.  If not set, default value is returned | ||||
|            (which may be ``None``). | ||||
| 
 | ||||
|            Note that it is an error to specify a default value if you | ||||
|            also specify ``require=True``. | ||||
| 
 | ||||
|         :param message: Optional first part of message to be used, | ||||
|            when raising a "value not found" error.  If not specified, | ||||
|            a default error message will be generated. | ||||
| 
 | ||||
|         :param usedb: Flag indicating whether config values should be | ||||
|            looked up from the DB.  The default for this param is | ||||
|            ``None``, in which case the :attr:`usedb` flag determines | ||||
|            the behavior. | ||||
| 
 | ||||
|         :param preferdb: Flag indicating whether config values from DB | ||||
|            should be preferred over values from INI files and/or app | ||||
|            defaults.  The default for this param is ``None``, in which | ||||
|            case the :attr:`preferdb` flag determines the behavior. | ||||
| 
 | ||||
|         :param session: Optional SQLAlchemy session to use for DB lookups. | ||||
|            NOTE: This param is not yet implemented; currently ignored. | ||||
| 
 | ||||
|         :returns: Value as string. | ||||
| 
 | ||||
|         """ | ||||
|         if require and default is not UNSPECIFIED: | ||||
|             raise ValueError("must not specify default value when require=True") | ||||
| 
 | ||||
|         # should we use/prefer db? | ||||
|         if usedb is None: | ||||
|             usedb = self.usedb | ||||
|         if usedb and preferdb is None: | ||||
|             preferdb = self.preferdb | ||||
| 
 | ||||
|         # read from db first if so requested | ||||
|         if usedb and preferdb: | ||||
|             value = self.get_from_db(key, session=session) | ||||
|             if value is not None: | ||||
|                 return value | ||||
| 
 | ||||
|         # read from defaults + INI files | ||||
|         value = self.configuration.get(key) | ||||
|         if value is not None: | ||||
| 
 | ||||
|             # nb. if the "value" corresponding to the given key is in | ||||
|             # fact a subset/dict of more config values, then we must | ||||
|             # "ignore" that.  so only return the value if it is *not* | ||||
|             # such a config subset. | ||||
|             if not isinstance(value, configuration.Configuration): | ||||
|                 return value | ||||
| 
 | ||||
|         # read from db last if so requested | ||||
|         if usedb and not preferdb: | ||||
|             value = self.get_from_db(key, session=session) | ||||
|             if value is not None: | ||||
|                 return value | ||||
| 
 | ||||
|         # raise error if required value not found | ||||
|         if require: | ||||
|             message = message or "missing or invalid config" | ||||
|             raise ConfigurationError(f"{message}; please set config value for: {key}") | ||||
| 
 | ||||
|         # give the default value if specified | ||||
|         if default is not UNSPECIFIED: | ||||
|             return default | ||||
| 
 | ||||
|     def get_from_db(self, key, session=None): | ||||
|         """ | ||||
|         Retrieve a config value from database settings table. | ||||
| 
 | ||||
|         This is a convenience wrapper around | ||||
|         :meth:`~wuttjamaican.app.AppHandler.get_setting()`. | ||||
|         """ | ||||
|         app = self.get_app() | ||||
|         with app.short_session(session=session) as s: | ||||
|             return app.get_setting(s, key) | ||||
| 
 | ||||
|     def require(self, *args, **kwargs): | ||||
|         """ | ||||
|         Retrieve a value from config, or raise error if no value can | ||||
|         be found.  This is just a shortcut, so these work the same:: | ||||
| 
 | ||||
|            config.get('foo', require=True) | ||||
| 
 | ||||
|            config.require('foo') | ||||
|         """ | ||||
|         kwargs['require'] = True | ||||
|         return self.get(*args, **kwargs) | ||||
| 
 | ||||
|     def get_bool(self, *args, **kwargs): | ||||
|         """ | ||||
|         Retrieve a boolean value from config. | ||||
| 
 | ||||
|         Accepts same params as :meth:`get()` but if a value is found, | ||||
|         it will be coerced to boolean via | ||||
|         :func:`~wuttjamaican.util.parse_bool()`. | ||||
|         """ | ||||
|         value = self.get(*args, **kwargs) | ||||
|         return parse_bool(value) | ||||
| 
 | ||||
|     def get_dict(self, prefix): | ||||
|         """ | ||||
|         Retrieve a particular group of values, as a dictionary. | ||||
| 
 | ||||
|         Please note, this will only return values from INI files + | ||||
|         defaults.  It will *not* return values from DB settings.  In | ||||
|         other words it assumes ``usedb=False``. | ||||
| 
 | ||||
|         For example given this config file: | ||||
| 
 | ||||
|         .. code-block:: ini | ||||
| 
 | ||||
|            [wutta.db] | ||||
|            keys = default, host | ||||
|            default.url = sqlite:///tmp/default.sqlite | ||||
|            host.url = sqlite:///tmp/host.sqlite | ||||
|            host.pool_pre_ping = true | ||||
| 
 | ||||
|         One can get the "dict" for SQLAlchemy engine config via:: | ||||
| 
 | ||||
|            config.get_dict('wutta.db') | ||||
| 
 | ||||
|         And the dict would look like:: | ||||
| 
 | ||||
|            {'keys': 'default, host', | ||||
|             'default.url': 'sqlite:///tmp/default.sqlite', | ||||
|             'host.url': 'sqlite:///tmp/host.sqlite', | ||||
|             'host.pool_pre_ping': 'true'} | ||||
| 
 | ||||
|         :param prefix: String prefix corresponding to a subsection of | ||||
|            the config. | ||||
| 
 | ||||
|         :returns: Dictionary containing the config subsection. | ||||
|         """ | ||||
|         try: | ||||
|             values = self.configuration[prefix] | ||||
|         except KeyError: | ||||
|             return {} | ||||
| 
 | ||||
|         return values.as_dict() | ||||
| 
 | ||||
|     def _configure_logging(self): | ||||
|         """ | ||||
|         This will save the current config parser defaults to a | ||||
|         temporary file, and use this file to configure Python's | ||||
|         standard logging module. | ||||
|         """ | ||||
|         # write current values to file suitable for logging auto-config | ||||
|         path = self._write_logging_config_file() | ||||
|         try: | ||||
|             logging.config.fileConfig(path, disable_existing_loggers=False) | ||||
|         except configparser.NoSectionError as error: | ||||
|             log.warning("tried to configure logging, but got NoSectionError: %s", error) | ||||
|         else: | ||||
|             log.debug("configured logging") | ||||
|         finally: | ||||
|             os.remove(path) | ||||
| 
 | ||||
|     def _write_logging_config_file(self): | ||||
| 
 | ||||
|         # load all current values into configparser | ||||
|         parser = configparser.RawConfigParser() | ||||
|         for section, values in self.configuration.items(): | ||||
|             parser.add_section(section) | ||||
|             for option, value in values.items(): | ||||
|                 parser.set(section, option, value) | ||||
| 
 | ||||
|         # write INI file and return path | ||||
|         fd, path = tempfile.mkstemp(suffix='.conf') | ||||
|         os.close(fd) | ||||
|         with open(path, 'wt') as f: | ||||
|             parser.write(f) | ||||
|         return path | ||||
| 
 | ||||
|     def get_app(self): | ||||
|         """ | ||||
|         Returns the global :class:`~wuttjamaican.app.AppHandler` | ||||
|         instance, creating it if necessary. | ||||
|         """ | ||||
|         if not hasattr(self, 'app'): | ||||
|             spec = self.get(f'{self.appname}.app.handler', usedb=False, | ||||
|                             default='wuttjamaican.app:AppHandler') | ||||
|             factory = load_object(spec) | ||||
|             self.app = factory(self) | ||||
|         return self.app | ||||
| 
 | ||||
| 
 | ||||
| def generic_default_files(appname): | ||||
|     """ | ||||
|     Returns a list of default file paths which might be used for | ||||
|     making a config object.  This function does not check if the paths | ||||
|     actually exist. | ||||
| 
 | ||||
|     :param appname: App name to be used as basis for default filenames. | ||||
| 
 | ||||
|     :returns: List of default file paths. | ||||
|     """ | ||||
|     if sys.platform == 'win32': | ||||
|         # use pywin32 to fetch official defaults | ||||
|         try: | ||||
|             from win32com.shell import shell, shellcon | ||||
|         except ImportError: | ||||
|             return [] | ||||
| 
 | ||||
|         return [ | ||||
|             # e.g. C:\..??      TODO: what is the user-specific path on win32? | ||||
|             os.path.join(shell.SHGetSpecialFolderPath( | ||||
|                 0, shellcon.CSIDL_APPDATA), appname, f'{appname}.conf'), | ||||
|             os.path.join(shell.SHGetSpecialFolderPath( | ||||
|                 0, shellcon.CSIDL_APPDATA), f'{appname}.conf'), | ||||
| 
 | ||||
|             # e.g. C:\ProgramData\wutta\wutta.conf | ||||
|             os.path.join(shell.SHGetSpecialFolderPath( | ||||
|                 0, shellcon.CSIDL_COMMON_APPDATA), appname, f'{appname}.conf'), | ||||
|             os.path.join(shell.SHGetSpecialFolderPath( | ||||
|                 0, shellcon.CSIDL_COMMON_APPDATA), f'{appname}.conf'), | ||||
|         ] | ||||
| 
 | ||||
|     # default paths for *nix | ||||
|     return [ | ||||
|         f'{sys.prefix}/app/{appname}.conf', | ||||
| 
 | ||||
|         os.path.expanduser(f'~/.{appname}/{appname}.conf'), | ||||
|         os.path.expanduser(f'~/.{appname}.conf'), | ||||
| 
 | ||||
|         f'/usr/local/etc/{appname}/{appname}.conf', | ||||
|         f'/usr/local/etc/{appname}.conf', | ||||
| 
 | ||||
|         f'/etc/{appname}/{appname}.conf', | ||||
|         f'/etc/{appname}.conf', | ||||
|     ] | ||||
| 
 | ||||
| 
 | ||||
| def make_config( | ||||
|         files=None, | ||||
|         plus_files=None, | ||||
|         appname='wutta', | ||||
|         env_files_name=None, | ||||
|         env_plus_files_name=None, | ||||
|         env=None, | ||||
|         default_files=None, | ||||
|         usedb=None, | ||||
|         preferdb=None, | ||||
|         extend=True, | ||||
|         extension_entry_points=None): | ||||
|     """ | ||||
|     Returns a new config object (presumably for global use), | ||||
|     initialized per the given parameters and (usually) further | ||||
|     modified by all registered config extensions. | ||||
| 
 | ||||
|     :param files: Config file path(s) to be loaded.  If not specified, | ||||
|        then some "default" behavior will be attempted.  (This will | ||||
|        check for env var or fallback to system default paths.  Or you | ||||
|        can override all that by specifying some path(s) here.) | ||||
| 
 | ||||
|     :param plus_files: Additional config path(s) to be loaded.  You | ||||
|        may specify a "config tweak" file(s) here, and leave ``files`` | ||||
|        empty, to get "defaults plus tweak" behavior. | ||||
| 
 | ||||
|     :param appname: Optional "app name" to use as basis for other | ||||
|        things - namely, constructing the default config file paths | ||||
|        etc.  For instance the default ``appname`` value is ``'wutta'`` | ||||
|        which leads to default env vars like ``WUTTA_CONFIG_FILES``. | ||||
| 
 | ||||
|     :param env_files_name: Name of the environment variable to read, | ||||
|        if ``files`` is not specified.  The default is | ||||
|        ``WUTTA_CONFIG_FILES`` unless you override ``appname``. | ||||
| 
 | ||||
|     :param env_plus_files_name: Name of the environment variable to | ||||
|        read, if ``plus_files`` is not specified.  The default is | ||||
|        ``WUTTA_CONFIG_PLUS_FILES`` unless you override ``appname``. | ||||
| 
 | ||||
|     :param env: Optional override for the ``os.environ`` dict. | ||||
| 
 | ||||
|     :param default_files: Optional way to identify the "default" file | ||||
|        path(s), if neither ``files`` nor ``env_files_name`` yield | ||||
|        anything.  This can be a list of potential file paths, or a | ||||
|        callable which returns such a list.  If a callable, it should | ||||
|        accept a single ``appname`` arg. | ||||
| 
 | ||||
|     :param usedb: Passed to the :class:`WuttaConfig` constructor. | ||||
| 
 | ||||
|     :param preferdb: Passed to the :class:`WuttaConfig` constructor. | ||||
| 
 | ||||
|     :param extend: Whether to "auto-extend" the config with all | ||||
|        registered extensions. | ||||
| 
 | ||||
|        As a general rule, ``make_config()`` should only be called | ||||
|        once, upon app startup.  This is because some of the config | ||||
|        extensions may do things which should only happen one time. | ||||
|        However if ``extend=False`` is specified, then no extensions | ||||
|        are invoked, so this may be done multiple times. | ||||
| 
 | ||||
|        (Why anyone would need this, is another question..maybe only | ||||
|        useful for tests.) | ||||
| 
 | ||||
|     :param extension_entry_points: Name of the ``setuptools`` entry | ||||
|        points section, used to identify registered config extensions. | ||||
|        The default is ``wutta.config.extensions`` unless you override | ||||
|        ``appname``. | ||||
|     """ | ||||
|     if env is None: | ||||
|         env = os.environ | ||||
| 
 | ||||
|     # first identify any "primary" config files | ||||
|     if files is None: | ||||
|         if not env_files_name: | ||||
|             env_files_name = f'{appname.upper()}_CONFIG_FILES' | ||||
| 
 | ||||
|         files = env.get(env_files_name) | ||||
|         if files is not None: | ||||
|             files = files.split(os.pathsep) | ||||
| 
 | ||||
|         elif default_files: | ||||
|             if callable(default_files): | ||||
|                 files = default_files(appname) or [] | ||||
|             elif isinstance(default_files, str): | ||||
|                 files = [default_files] | ||||
|             else: | ||||
|                 files = list(default_files) | ||||
| 
 | ||||
|         else: | ||||
|             files = [] | ||||
|             for path in generic_default_files(appname): | ||||
|                 if os.path.exists(path): | ||||
|                     files.append(path) | ||||
| 
 | ||||
|     elif isinstance(files, str): | ||||
|         files = [files] | ||||
|     else: | ||||
|         files = list(files) | ||||
| 
 | ||||
|     # then identify any "plus" (config tweak) files | ||||
|     if plus_files is None: | ||||
|         if not env_plus_files_name: | ||||
|             env_plus_files_name = f'{appname.upper()}_CONFIG_PLUS_FILES' | ||||
| 
 | ||||
|         plus_files = env.get(env_plus_files_name) | ||||
|         if plus_files is not None: | ||||
|             plus_files = plus_files.split(os.pathsep) | ||||
| 
 | ||||
|         else: | ||||
|             plus_files = [] | ||||
| 
 | ||||
|     elif isinstance(plus_files, str): | ||||
|         plus_files = [plus_files] | ||||
|     else: | ||||
|         plus_files = list(plus_files) | ||||
| 
 | ||||
|     # combine all files | ||||
|     files.extend(plus_files) | ||||
| 
 | ||||
|     # make config object | ||||
|     config = WuttaConfig(files, appname=appname, | ||||
|                          usedb=usedb, preferdb=preferdb) | ||||
| 
 | ||||
|     # maybe extend config object | ||||
|     if extend: | ||||
|         if not extension_entry_points: | ||||
|             extension_entry_points = f'{appname}.config.extensions' | ||||
| 
 | ||||
|         # apply all registered extensions | ||||
|         # TODO: maybe let config disable some extensions? | ||||
|         extensions = load_entry_points(extension_entry_points) | ||||
|         for extension in extensions.values(): | ||||
|             log.debug("applying config extension: %s", extension.key) | ||||
|             extension().configure(config) | ||||
| 
 | ||||
|     return config | ||||
							
								
								
									
										28
									
								
								src/wuttjamaican/db/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/wuttjamaican/db/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican -  app database | ||||
| """ | ||||
| 
 | ||||
| from .sess import Session, short_session | ||||
| from .conf import get_setting, get_engines | ||||
							
								
								
									
										149
									
								
								src/wuttjamaican/db/conf.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								src/wuttjamaican/db/conf.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,149 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican -  database configuration | ||||
| """ | ||||
| 
 | ||||
| from collections import OrderedDict | ||||
| 
 | ||||
| import sqlalchemy as sa | ||||
| 
 | ||||
| from wuttjamaican.util import load_object, parse_bool, parse_list | ||||
| 
 | ||||
| 
 | ||||
| def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs): | ||||
|     """ | ||||
|     Construct a new DB engine from configuration. | ||||
| 
 | ||||
|     This is a wrapper around upstream | ||||
|     :func:`sqlalchemy:sqlalchemy.engine_from_config()`. | ||||
| 
 | ||||
|     The purpose of the customization is to allow certain attributes of | ||||
|     the engine to be driven by config, whereas the upstream function | ||||
|     is more limited in that regard.  The following in particular: | ||||
| 
 | ||||
|     * ``poolclass`` | ||||
|     * ``pool_pre_ping`` | ||||
| 
 | ||||
|     If these options are present in the configuration dictionary, they | ||||
|     will be coerced to appropriate Python equivalents and then passed | ||||
|     as kwargs to the upstream function. | ||||
| 
 | ||||
|     An example config file leveraging this feature: | ||||
| 
 | ||||
|     .. code-block:: ini | ||||
| 
 | ||||
|        [wutta.db] | ||||
|        sqlalchemy.url = sqlite:///tmp/default.sqlite | ||||
|        sqlalchemy.poolclass = sqlalchemy.pool:NullPool | ||||
|        sqlalchemy.pool_pre_ping = true | ||||
| 
 | ||||
|     Note that if present, the ``poolclass`` value must be a "spec" | ||||
|     string, as required by :func:`~wuttjamaican.util.load_object()`. | ||||
|     """ | ||||
|     config_dict = dict(configuration) | ||||
| 
 | ||||
|     # convert 'poolclass' arg to actual class | ||||
|     key = f'{prefix}poolclass' | ||||
|     if key in config_dict: | ||||
|         kwargs.setdefault('poolclass', load_object(config_dict[key])) | ||||
|         del config_dict[key] | ||||
| 
 | ||||
|     # convert 'pool_pre_ping' arg to boolean | ||||
|     key = f'{prefix}pool_pre_ping' | ||||
|     if key in config_dict: | ||||
|         kwargs.setdefault('pool_pre_ping', parse_bool(config_dict[key])) | ||||
|         del config_dict[key] | ||||
| 
 | ||||
|     engine = sa.engine_from_config(config_dict, prefix, **kwargs) | ||||
| 
 | ||||
|     return engine | ||||
| 
 | ||||
| 
 | ||||
| def get_engines(config, prefix): | ||||
|     """ | ||||
|     Construct and return all database engines defined for a given | ||||
|     config prefix. | ||||
| 
 | ||||
|     For instance if you have a config file with: | ||||
| 
 | ||||
|     .. code-block:: ini | ||||
| 
 | ||||
|        [wutta.db] | ||||
|        keys = default, host | ||||
|        default.url = sqlite:///tmp/default.sqlite | ||||
|        host.url = sqlite:///tmp/host.sqlite | ||||
| 
 | ||||
|     And then you call this function to get those DB engines:: | ||||
| 
 | ||||
|        get_engines(config, 'wutta.db') | ||||
| 
 | ||||
|     The result of that will be like:: | ||||
| 
 | ||||
|        {'default': Engine(bind='sqlite:///tmp/default.sqlite'), | ||||
|         'host': Engine(bind='sqlite:///tmp/host.sqlite')} | ||||
| 
 | ||||
|     :param config: App config object. | ||||
| 
 | ||||
|     :param prefix: Prefix for the config "section" which contains DB | ||||
|        connection info. | ||||
| 
 | ||||
|     :returns: A dictionary of SQLAlchemy engines, with keys matching | ||||
|        those found in config. | ||||
|     """ | ||||
|     keys = config.get(f'{prefix}.keys', usedb=False) | ||||
|     if keys: | ||||
|         keys = parse_list(keys) | ||||
|     else: | ||||
|         keys = ['default'] | ||||
| 
 | ||||
|     engines = OrderedDict() | ||||
|     cfg = config.get_dict(prefix) | ||||
|     for key in keys: | ||||
|         key = key.strip() | ||||
|         try: | ||||
|             engines[key] = engine_from_config(cfg, prefix=f'{key}.') | ||||
|         except KeyError: | ||||
|             if key == 'default': | ||||
|                 try: | ||||
|                     engines[key] = engine_from_config(cfg, prefix='sqlalchemy.') | ||||
|                 except KeyError: | ||||
|                     pass | ||||
|     return engines | ||||
| 
 | ||||
| 
 | ||||
| def get_setting(session, name): | ||||
|     """ | ||||
|     Get a setting value from the DB. | ||||
| 
 | ||||
|     Note that this assumes (for now?) the DB contains a table named | ||||
|     ``setting`` with ``(name, value)`` columns. | ||||
| 
 | ||||
|     :param session: App DB session. | ||||
| 
 | ||||
|     :param name: Name of the setting to get. | ||||
| 
 | ||||
|     :returns: Setting value as string, or ``None``. | ||||
|     """ | ||||
|     sql = sa.text("select value from setting where name = :name") | ||||
|     return session.execute(sql, params={'name': name}).scalar() | ||||
							
								
								
									
										104
									
								
								src/wuttjamaican/db/sess.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/wuttjamaican/db/sess.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,104 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican -  database sessions | ||||
| 
 | ||||
| .. class:: Session | ||||
| 
 | ||||
|    SQLAlchemy session class used for all (normal) app database | ||||
|    connections. | ||||
| 
 | ||||
|    See the upstream :class:`sqlalchemy:sqlalchemy.orm.Session` docs | ||||
|    for more info. | ||||
| """ | ||||
| 
 | ||||
| from sqlalchemy import orm | ||||
| 
 | ||||
| 
 | ||||
| Session = orm.sessionmaker() | ||||
| 
 | ||||
| 
 | ||||
| class short_session: | ||||
|     """ | ||||
|     Context manager for a short-lived database session. | ||||
| 
 | ||||
|     A canonical use case for this is when the config object needs to | ||||
|     grab a single setting value from the DB, but it does not have an | ||||
|     active DB session to do it.  This context manager is used to | ||||
|     produce the session, and close it when finished.  For example:: | ||||
| 
 | ||||
|        with short_session(config) as s: | ||||
|           result = s.query("select something from somewhere").scalar() | ||||
| 
 | ||||
|     How it goes about producing the session instance will depend on | ||||
|     which of the following 3 params are given (explained below): | ||||
| 
 | ||||
|     * ``config`` | ||||
|     * ``factory`` | ||||
|     * ``session`` | ||||
| 
 | ||||
|     Note that it is also okay if you provide *none* of the above | ||||
|     params, in which case the main :class:`Session` class will be used | ||||
|     as the factory. | ||||
| 
 | ||||
|     :param config: Optional app config object.  If a new session must | ||||
|        be created, the config will be consulted to determine the | ||||
|        factory which is used to create the new session. | ||||
| 
 | ||||
|     :param factory: Optional factory to use when making a new session. | ||||
|        If specified, this will override the ``config`` mechanism. | ||||
| 
 | ||||
|     :param session: Optional SQLAlchemy session instance.  If a valid | ||||
|        session is provided here, it will be used instead of creating a | ||||
|        new/temporary session. | ||||
| 
 | ||||
|     :param commit: Whether the temporary session should be committed | ||||
|        before it is closed.  This flag has no effect if a valid | ||||
|        ``session`` instance is provided, since no temporary session | ||||
|        will be created. | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, config=None, factory=None, session=None, commit=False): | ||||
|         self.config = config | ||||
|         self.factory = factory | ||||
|         self.session = session | ||||
|         self.private = not bool(session) | ||||
|         self.commit = commit | ||||
| 
 | ||||
|     def __enter__(self): | ||||
|         if not self.session: | ||||
|             if not self.factory: | ||||
|                 if self.config: | ||||
|                     app = self.config.get_app() | ||||
|                     self.factory = app.make_session | ||||
|                 else: | ||||
|                     self.factory = Session | ||||
|             self.session = self.factory() | ||||
|         return self.session | ||||
| 
 | ||||
|     def __exit__(self, exc_type, exc_value, traceback): | ||||
|         if self.private: | ||||
|             if self.commit: | ||||
|                 self.session.commit() | ||||
|             self.session.close() | ||||
|             self.session = None | ||||
							
								
								
									
										37
									
								
								src/wuttjamaican/exc.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/wuttjamaican/exc.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - exceptions | ||||
| """ | ||||
| 
 | ||||
| 
 | ||||
| class WuttaError(Exception): | ||||
|     """ | ||||
|     Base class for all exceptions specific to Wutta Framework. | ||||
|     """ | ||||
| 
 | ||||
| 
 | ||||
| class ConfigurationError(WuttaError): | ||||
|     """ | ||||
|     Generic class for configuration errors. | ||||
|     """ | ||||
							
								
								
									
										154
									
								
								src/wuttjamaican/util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								src/wuttjamaican/util.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,154 @@ | |||
| # -*- coding: utf-8; -*- | ||||
| ################################################################################ | ||||
| # | ||||
| #  WuttJamaican -- Base package for Wutta Framework | ||||
| #  Copyright © 2023 Lance Edgar | ||||
| # | ||||
| #  This file is part of Wutta Framework. | ||||
| # | ||||
| #  Wutta Framework is free software: you can redistribute it and/or modify it | ||||
| #  under the terms of the GNU General Public License as published by the Free | ||||
| #  Software Foundation, either version 3 of the License, or (at your option) any | ||||
| #  later version. | ||||
| # | ||||
| #  Wutta Framework is distributed in the hope that it will be useful, but | ||||
| #  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||||
| #  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for | ||||
| #  more details. | ||||
| # | ||||
| #  You should have received a copy of the GNU General Public License along with | ||||
| #  Wutta Framework.  If not, see <http://www.gnu.org/licenses/>. | ||||
| # | ||||
| ################################################################################ | ||||
| """ | ||||
| WuttJamaican - utilities | ||||
| """ | ||||
| 
 | ||||
| import importlib | ||||
| import logging | ||||
| import shlex | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| # nb. this is used as default kwarg value in some places, to | ||||
| # distinguish passing a ``None`` value, vs. *no* value at all | ||||
| UNSPECIFIED = object() | ||||
| 
 | ||||
| 
 | ||||
| def load_entry_points(group, ignore_errors=False): | ||||
|     """ | ||||
|     Load a set of ``setuptools``-style entry points. | ||||
| 
 | ||||
|     This is used to locate "plugins" and similar things, e.g. the set | ||||
|     of subcommands which belong to a main command. | ||||
| 
 | ||||
|     :param group: The group (string name) of entry points to be | ||||
|        loaded. | ||||
| 
 | ||||
|     :ignore_errors: If false (the default), any errors will be raised | ||||
|        normally.  If true, errors will be logged but not raised. | ||||
| 
 | ||||
|     :returns: A dictionary whose keys are the entry point names, and | ||||
|        values are the loaded entry points. | ||||
|     """ | ||||
|     entry_points = {} | ||||
| 
 | ||||
|     try: | ||||
|         # nb. this package was added in python 3.8 | ||||
|         import importlib.metadata | ||||
| 
 | ||||
|     except ImportError: | ||||
|         # older setup, must use pkg_resources | ||||
|         # TODO: remove this section once we require python 3.8 | ||||
|         from pkg_resources import iter_entry_points | ||||
| 
 | ||||
|         for entry_point in iter_entry_points(group): | ||||
|             try: | ||||
|                 ep = entry_point.load() | ||||
|             except: | ||||
|                 if not ignore_errors: | ||||
|                     raise | ||||
|                 log.warning("failed to load entry point: %s", entry_point, | ||||
|                             exc_info=True) | ||||
|             else: | ||||
|                 entry_points[entry_point.name] = ep | ||||
| 
 | ||||
|     else: | ||||
|         # newer setup (python >= 3.8); can use importlib | ||||
|         eps = importlib.metadata.entry_points() | ||||
|         for entry_point in eps.select(group=group): | ||||
|             try: | ||||
|                 ep = entry_point.load() | ||||
|             except: | ||||
|                 if not ignore_errors: | ||||
|                     raise | ||||
|                 log.warning("failed to load entry point: %s", entry_point, | ||||
|                             exc_info=True) | ||||
|             else: | ||||
|                 entry_points[entry_point.name] = ep | ||||
| 
 | ||||
|     return entry_points | ||||
| 
 | ||||
| 
 | ||||
| def load_object(spec): | ||||
|     """ | ||||
|     Load an arbitrary object from a module, according to the spec. | ||||
| 
 | ||||
|     The spec string should contain a dotted path to an importable module, | ||||
|     followed by a colon (``':'``), followed by the name of the object to be | ||||
|     loaded.  For example: | ||||
| 
 | ||||
|     .. code-block:: none | ||||
| 
 | ||||
|        wuttjamaican.util:parse_bool | ||||
| 
 | ||||
|     You'll notice from this example that "object" in this context refers to any | ||||
|     valid Python object, i.e. not necessarily a class instance.  The name may | ||||
|     refer to a class, function, variable etc.  Once the module is imported, the | ||||
|     ``getattr()`` function is used to obtain a reference to the named object; | ||||
|     therefore anything supported by that approach should work. | ||||
| 
 | ||||
|     :param spec: Spec string. | ||||
| 
 | ||||
|     :returns: The specified object. | ||||
|     """ | ||||
|     if not spec: | ||||
|         raise ValueError("no object spec provided") | ||||
| 
 | ||||
|     module_path, name = spec.split(':') | ||||
|     module = importlib.import_module(module_path) | ||||
|     return getattr(module, name) | ||||
| 
 | ||||
| 
 | ||||
| def parse_bool(value): | ||||
|     """ | ||||
|     Derive a boolean from the given string value. | ||||
|     """ | ||||
|     if value is None: | ||||
|         return None | ||||
|     if isinstance(value, bool): | ||||
|         return value | ||||
|     if str(value).lower() in ('true', 'yes', 'y', 'on', '1'): | ||||
|         return True | ||||
|     return False | ||||
| 
 | ||||
| 
 | ||||
| def parse_list(value): | ||||
|     """ | ||||
|     Parse a configuration value, splitting by whitespace and/or commas | ||||
|     and taking quoting into account etc., yielding a list of strings. | ||||
|     """ | ||||
|     if value is None: | ||||
|         return [] | ||||
|     parser = shlex.shlex(value) | ||||
|     parser.whitespace += ',' | ||||
|     parser.whitespace_split = True | ||||
|     values = list(parser) | ||||
|     for i, value in enumerate(values): | ||||
|         if value.startswith('"') and value.endswith('"'): | ||||
|             values[i] = value[1:-1] | ||||
|         elif value.startswith("'") and value.endswith("'"): | ||||
|             values[i] = value[1:-1] | ||||
|     return values | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Lance Edgar
						Lance Edgar