source: rattail/rattail/config.py @ 0ab553c

Last change on this file since 0ab553c was 0ab553c, checked in by Lance Edgar <ledgar@…>, 10 months ago

Tweak how we create config parser object, for python 3 vs. 2

per some deprecation warnings

  • Property mode set to 100644
File size: 29.1 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2018 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Application Configuration
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import os
30import re
31import sys
32import shlex
33import datetime
34from six.moves import configparser
35import warnings
36import logging
37import logging.config
38
39import six
40
41from rattail.util import load_entry_points, import_module_path
42from rattail.exceptions import WindowsExtensionsNotInstalled, ConfigurationError
43from rattail.files import temp_path
44from rattail.logging import TimeConverter
45from rattail.util import prettify
46
47
48log = logging.getLogger(__name__)
49
50
51def parse_bool(value):
52    """
53    Derive a boolean from the given string value.
54    """
55    if value is None:
56        return None
57    if isinstance(value, bool):
58        return value
59    if value.lower() in ('true', 'yes', 'on', '1'):
60        return True
61    return False
62
63
64def parse_list(value):
65    """
66    Parse a configuration value, splitting by whitespace and/or commas and
67    taking quoting into account, etc., yielding a list of strings.
68    """
69    if value is None:
70        return []
71    # Per the shlex docs (https://docs.python.org/2/library/shlex.html):
72    # "Prior to Python 2.7.3, this module did not support Unicode input."
73    if six.PY2 and isinstance(value, six.text_type):
74        value = value.encode('utf-8')
75    parser = shlex.shlex(value)
76    parser.whitespace += ','
77    parser.whitespace_split = True
78    values = list(parser)
79    for i, value in enumerate(values):
80        if value.startswith('"') and value.endswith('"'):
81            values[i] = value[1:-1]
82    return values
83
84
85class RattailConfig(object):
86    """
87    Rattail config object; this represents the sum total of configuration
88    available to the running app.  The actual config available falls roughly
89    into two categories: the "defaults" and the "db" (more on these below).
90    The general idea here is that one might wish to provide some default
91    settings within some config file(s) and/or the command line itself, but
92    then allow all settings found in the database to override those defaults.
93    However, all variations on this theme are supported, e.g. "use db settings
94    but prefer those from file", "never use db settings", and so on.
95
96    As for the "defaults" aspect of the config, this is read only once upon
97    application startup.  It almost certainly involves one (or more) config
98    file(s), but in addition to that, the application itself is free to embed
99    default settings within the config object.  When this occurs, there will be
100    no distinction made between settings which came from a file versus those
101    which were established as defaults by the application logic.
102
103    As for the "db" aspect of the config, of course this ultimately hinges upon
104    the config defaults.  If a default Rattail database connection is defined,
105    then the ``Setting`` table within that database may also be consulted for
106    config values.  When this is done, the ``Setting.name`` is determined by
107    concatenating the ``section`` and ``option`` arguments from the
108    :meth:`get()` call, with a period (``'.'``) in between.
109    """
110
111    def __init__(self, files=[], usedb=None, preferdb=None):
112        self.files_requested = []
113        self.files_read = []
114        if six.PY3:
115            self.parser = configparser.ConfigParser()
116        else: # PY2
117            self.parser = configparser.SafeConfigParser()
118        for path in files:
119            self.read_file(path)
120        self.usedb = usedb
121        if self.usedb is None:
122            self.usedb = self.getbool('rattail.config', 'usedb', usedb=False, default=False)
123        self.preferdb = preferdb
124        if self.usedb and self.preferdb is None:
125            self.preferdb = self.getbool('rattail.config', 'preferdb', usedb=False, default=False)
126
127        # Attempt to detect lack of SQLAlchemy libraries etc.  This allows us
128        # to avoid installing those on a machine which will not need to access
129        # a database etc.
130        self._session_factory = None
131        if self.usedb:
132            try:
133                from rattail.db import Session
134            except ImportError: # pragma: no cover
135                log.warning("config created with `usedb = True`, but can't import "
136                            "`rattail.db.Session`, so setting `usedb = False` instead",
137                            exc_info=True)
138                self.usedeb = False
139                self.preferdb = False
140            else:
141                self._session_factory = lambda: (Session(), True)
142
143    def read_file(self, path, recurse=True):
144        """
145        Read in config from the given file.
146        """
147        path = os.path.abspath(path)
148        if path in self.files_requested:
149            log.debug("ignoring config file which was already requested: {0}".format(path))
150            return
151
152        log.debug("will attempt to read config from file: {0}".format(path))
153        self.files_requested.append(path)
154
155        if six.PY3:
156            ConfigParser = configparser.ConfigParser
157        else: # PY2
158            ConfigParser = configparser.SafeConfigParser
159        parser = ConfigParser(dict(
160            here=os.path.dirname(path),
161        ))
162        if not parser.read(path):
163            log.debug("ConfigParser.read() failed")
164            return
165
166        # If recursing, walk the complete config file inheritance chain.
167        if recurse:
168            if parser.has_section('rattail.config'):
169                if parser.has_option('rattail.config', 'include'):
170                    includes = parse_list(parser.get('rattail.config', 'include'))
171                    for included in includes:
172                        self.read_file(included, recurse=True)
173
174        # Okay, now we can finally read this file into our main parser.
175        self.parser.read(path)
176        self.files_read.append(path)
177        log.info("config was read from file: {0}".format(path))
178
179    def configure_logging(self):
180        """
181        This first checks current config to determine whether or not we're
182        supposed to be configuring logging at all.  If not, nothing more is
183        done.
184
185        If we are to configure logging, then this will save the current config
186        parser defaults to a temporary file, and use this file to configure
187        Python's standard logging module.
188        """
189        if not self.getbool('rattail.config', 'configure_logging', usedb=False, default=False):
190            return
191
192        # Coerce all logged timestamps to the local timezone, if possible.
193        logging.Formatter.converter = TimeConverter(self)
194
195        # Flush all current config to a single file, for input to fileConfig().
196        path = temp_path(suffix='.conf')
197        with open(path, 'wt') as f:
198            self.parser.write(f)
199
200        try:
201            logging.config.fileConfig(path, disable_existing_loggers=False)
202        except configparser.NoSectionError as error:
203            log.warning("tried to configure logging, but got NoSectionError: {0}".format(error))
204        else:
205            log.debug("configured logging")
206        finally:
207            os.remove(path)
208
209    def setdefault(self, section, option, value):
210        """
211        Establishes a new default for the given setting, if none exists yet.
212        The effective default value is returned in all cases.
213        """
214        exists = True
215        if not self.parser.has_section(section):
216            self.parser.add_section(section)
217            exists = False
218        elif not self.parser.has_option(section, option):
219            exists = False
220        if not exists:
221            self.parser.set(section, option, value)
222        return self.parser.get(section, option)
223
224    def set(self, section, option, value):
225        """
226        Set a value within the config's parser data set, i.e. the "defaults".
227        This should probably be used sparingly, though one expected use is
228        within tests (for convenience).
229        """
230        if not self.parser.has_section(section):
231            self.parser.add_section(section)
232        self.parser.set(section, option, value)
233
234    def get(self, section, option, usedb=None, preferdb=None, session=None, default=None):
235        """
236        Retrieve a value from config.
237        """
238        usedb = usedb if usedb is not None else self.usedb
239        if usedb:
240            preferdb = preferdb if preferdb is not None else getattr(self, 'preferdb', False)
241        else:
242            preferdb = False
243       
244        if usedb and preferdb:
245            value = self._getdb(section, option, session=session)
246            if value is not None:
247                return value
248
249        if self.parser.has_option(section, option):
250            return self.parser.get(section, option)
251
252        if usedb and not preferdb:
253            value = self._getdb(section, option, session=session)
254            if value is not None:
255                return value
256
257        return default
258
259    def _getdb(self, section, option, session=None):
260        """
261        Retrieve a config value from database settings table.
262        """
263        from rattail.db import api
264
265        close = False
266        if session is None:
267            session, close = self._session_factory()
268        value = api.get_setting(session, '{}.{}'.format(section, option))
269        if close:
270            session.close()
271        return value
272
273    def setdb(self, section, option, value, session=None):
274        """
275        Set a config value in the database settings table.  Note that the
276        ``value`` arg should be a Unicode object.
277        """
278        from rattail.db import api
279
280        close = False
281        if session is None:
282            session, close = self._session_factory()
283        api.save_setting(session, '{}.{}'.format(section, option), value)
284        if close:
285            session.commit()
286            session.close()
287
288    def getbool(self, *args, **kwargs):
289        """
290        Retrieve a boolean value from config.
291        """
292        value = self.get(*args, **kwargs)
293        return parse_bool(value)
294
295    def getint(self, *args, **kwargs):
296        """
297        Retrieve an integer value from config.
298        """
299        value = self.get(*args, **kwargs)
300        if value is None:
301            return None
302        if isinstance(value, int):
303            return value
304        return int(value)
305
306    def getdate(self, *args, **kwargs):
307        """
308        Retrieve a date value from config.
309        """
310        value = self.get(*args, **kwargs)
311        if value is None:
312            return None
313        if isinstance(value, datetime.date):
314            return value
315        return datetime.datetime.strptime(value, '%Y-%m-%d').date()
316
317    def getlist(self, *args, **kwargs):
318        """
319        Retrieve a list of string values from a single config option.
320        """
321        value = self.get(*args, **kwargs)
322        if value is None:
323            return None
324        if isinstance(value, six.string_types):
325            return parse_list(value)
326        return value            # maybe a caller-provided default?
327
328    def get_dict(self, section):
329        """
330        Convenience method which returns a dictionary of options contained
331        within the given section.  Note that this method only supports the
332        "default" config settings, i.e. those within the underlying parser.
333        """
334        settings = {}
335        if self.parser.has_section(section):
336            for option in self.parser.options(section):
337                settings[option] = self.parser.get(section, option)
338        return settings
339
340    def require(self, section, option, **kwargs):
341        """
342        Fetch a value from current config, and raise an error if no value can
343        be found.
344        """
345        if 'default' in kwargs:
346            warnings.warn("You have provided a default value to the `RattailConfig.require()` "
347                          "method.  This is allowed but also somewhat pointless, since `get()` "
348                          "would suffice if a default is known.", UserWarning)
349
350        msg = kwargs.pop('msg', None)
351        value = self.get(section, option, **kwargs)
352        if value is not None:
353            return value
354
355        if msg is None:
356            msg = "Missing or invalid config"
357        msg = "{0}; please set '{1}' in the [{2}] section of your config file".format(
358            msg, option, section)
359        raise ConfigurationError(msg)
360
361    ##############################
362    # convenience methods
363    ##############################
364
365    def app_title(self, default=None):
366        """
367        Returns official display title for the current app.
368        """
369        if not default:
370            return self.require('rattail', 'app_title')
371        return self.get('rattail', 'app_title', default=default)
372
373    def node_title(self, default=None):
374        """
375        Returns official display title for the current app.
376        """
377        title = self.get('rattail', 'node_title')
378        if title:
379            return title
380        return self.app_title(default=default)
381
382    def node_type(self, default=None):
383        """
384        Returns the "type" of current node.  What this means will generally
385        depend on the app logic.
386        """
387        try:
388            return self.require('rattail', 'node_type', usedb=False)
389        except ConfigurationError:
390            if default:
391                return default
392            raise
393
394    def production(self):
395        """
396        Returns boolean indicating whether the app is running in production mode
397        """
398        return self.getbool('rattail', 'production', default=False)
399
400    def demo(self):
401        """
402        Returns boolean indicating whether the app is running in demo mode
403        """
404        return self.getbool('rattail', 'demo', default=False)
405
406    def versioning_enabled(self):
407        """
408        Returns boolean indicating whether data versioning is enabled.
409        """
410        return self.getbool('rattail.db', 'versioning.enabled', usedb=False,
411                            default=False)
412
413    def appdir(self, require=True):
414        """
415        Returns path to the 'app' dir, if known.
416        """
417        get = self.require if require else self.get
418        return get('rattail', 'appdir')
419
420    def workdir(self, require=True):
421        """
422        Returns boolean indicating whether the config indicates production mode.
423        """
424        get = self.require if require else self.get
425        return get('rattail', 'workdir')
426
427    def batch_filedir(self, key=None):
428        """
429        Returns path to root folder where batches (optionally of type 'key')
430        are stored.
431        """
432        path = os.path.abspath(self.require('rattail', 'batch.files'))
433        if key:
434            return os.path.join(path, key)
435        return path
436
437    def batch_filepath(self, key, uuid, filename=None, makedirs=False):
438        """
439        Returns absolute path to a batch's data folder, with optional filename
440        appended.  If ``makedirs`` is set, the batch data folder will be
441        created if it does not already exist.
442        """
443        rootdir = self.batch_filedir(key)
444        filedir = os.path.join(rootdir, uuid[:2], uuid[2:])
445        if makedirs and not os.path.exists(filedir):
446            os.makedirs(filedir)
447        if filename:
448            return os.path.join(filedir, filename)
449        return filedir
450
451    def export_filedir(self, key=None):
452        """
453        Returns path to root folder where exports (optionally of type 'key')
454        are stored.
455        """
456        path = os.path.abspath(self.require('rattail', 'export.files'))
457        if key:
458            return os.path.join(path, key)
459        return path
460
461    def export_filepath(self, key, uuid, filename=None, makedirs=False):
462        """
463        Returns absolute path to export data file, generated from the given args.
464        """
465        rootdir = self.export_filedir(key)
466        filedir = os.path.join(rootdir, uuid[:2], uuid[2:])
467        if makedirs and not os.path.exists(filedir):
468            os.makedirs(filedir)
469        if filename:
470            return os.path.join(filedir, filename)
471        return filedir
472
473    def upgrade_filedir(self):
474        """
475        Returns path to root folder where upgrade files are stored.
476        """
477        path = os.path.abspath(self.require('rattail.upgrades', 'files'))
478        return path
479
480    def upgrade_filepath(self, uuid, filename=None, makedirs=False):
481        """
482        Returns absolute path to upgrade data file, generated from the given args.
483        """
484        rootdir = self.upgrade_filedir()
485        filedir = os.path.join(rootdir, uuid[:2], uuid[2:])
486        if makedirs and not os.path.exists(filedir):
487            os.makedirs(filedir)
488        if filename:
489            return os.path.join(filedir, filename)
490        return filedir
491
492    def upgrade_command(self, default='/bin/sleep 30'):
493        """
494        Returns command to be used when performing upgrades.
495        """
496        # NOTE: we don't allow command to be specified in DB, for security reasons..
497        return self.getlist('rattail.upgrades', 'command', default=default, usedb=False)
498
499    def datasync_url(self):
500        """
501        Returns configured URL for managing datasync daemon.
502        """
503        return self.get('rattail.datasync', 'url')
504
505    def get_enum(self):
506        """
507        Returns a reference to configured 'enum' module; defaults to
508        :mod:`rattail.enum`.
509        """
510        spec = self.get('rattail', 'enum', default='rattail.enum')
511        return import_module_path(spec)
512
513    def get_model(self):
514        """
515        Returns a reference to configured 'model' module; defaults to
516        :mod:`rattail.db.model`.
517        """
518        spec = self.get('rattail', 'model', default='rattail.db.model', usedb=False)
519        return import_module_path(spec)
520
521    def product_key(self):
522        """
523        Returns the name of the attribute which should be treated as the
524        canonical product key field, e.g. 'upc' or 'item_id' etc.
525        """
526        return self.get('rattail', 'product.key', default='upc')
527
528    def product_key_title(self, key=None):
529        """
530        Returns the title string to be used when displaying product key field,
531        e.g. "UPC" or "Part No." etc.
532        """
533        title = self.get('rattail', 'product.key_title')
534        if title:
535            return title
536        if not key:
537            key = self.product_key()
538        if key == 'upc':
539            return "UPC"
540        if key == 'item_id':
541            return "Item ID"
542        return prettify(key)
543
544    def single_store(self):
545        """
546        Returns boolean indicating whether the system is configured to behave
547        as if it belongs to a single Store.
548        """
549        return self.getbool('rattail', 'single_store', default=False)
550
551    def get_store(self, session):
552        """
553        Returns a :class:`rattail.db.model.Store` instance corresponding to app
554        config, or ``None``.
555        """
556        from rattail.db import api
557
558        store = self.get('rattail', 'store')
559        if store:
560            return api.get_store(session, store)
561
562
563    ##############################
564    # deprecated methods
565    ##############################
566
567    def options(self, section):
568        warnings.warn("RattailConfig.option() is deprecated, please find "
569                      "another way to accomplish what you're after.",
570                      DeprecationWarning)
571        return self.parser.options(section)
572
573    def has_option(self, section, option):
574        warnings.warn("RattailConfig.has_option() is deprecated, please find "
575                      "another way to accomplish what you're after.",
576                      DeprecationWarning)
577        return self.parser.has_option(section, option)
578
579
580class ConfigExtension(object):
581    """
582    Base class for all config extensions.
583    """
584    key = None
585
586    def __repr__(self):
587        return "ConfigExtension(key={0})".format(repr(self.key))
588
589    def configure(self, config):
590        """
591        All subclasses should override this method, to extend the config object
592        in any way necessary etc.
593        """
594
595
596def make_config(files=None, usedb=None, preferdb=None, env=os.environ, winsvc=None, extend=True, versioning=None):
597    """
598    Returns a new config object, initialized with the given parameters and
599    further modified by all registered config extensions.
600
601    :param versioning: Controls whether or not the versioning system is
602       configured with the new config object.  If ``True``, versioning will be
603       configured.  If ``False`` then it will not be configured.  If ``None``
604       (the default) then versioning will be configured only if the config
605       object itself says that it should be.
606    """
607    if files is None:
608        files = env.get('RATTAIL_CONFIG_FILES')
609        if files is not None:
610            files = files.split(os.pathsep)
611        else:
612            files = default_system_paths() + default_user_paths()
613    elif isinstance(files, six.string_types):
614        files = [files]
615
616    # If making config for a Windows service, we must read the default config
617    # file(s) first, and check it to see if there is an alternate config file
618    # which should be considered the "root" file.  Normally we specify the root
619    # config file(s) via command line etc., but there is no practical way to
620    # pass parameters to a Windows service.  This way we can effectively do
621    # just that, via config.
622    if winsvc is not None:
623        parser = configparser.SafeConfigParser()
624        parser.read(files)
625        if parser.has_section('rattail.config'):
626            key = 'winsvc.{0}'.format(winsvc)
627            if parser.has_option('rattail.config', key):
628                files = parse_list(parser.get('rattail.config', key))
629
630    # Initial config object will have values read from the given file paths,
631    # and kwargs, but no other app defaults etc. will have been applied yet.
632    config = RattailConfig(files, usedb=usedb, preferdb=preferdb)
633    config.configure_logging()
634
635    if config.getbool('rattail', 'suppress_psycopg2_wheel_warning', usedb=False):
636        # TODO: revisit this, does it require action from us?
637        # suppress this warning about psycopg2 wheel; not sure what it means yet
638        # exactly but it's causing frequent noise for us...
639        warnings.filterwarnings(
640            'ignore',
641            r'^The psycopg2 wheel package will be renamed from release 2\.8; in order to keep '
642            'installing from binary please use "pip install psycopg2-binary" instead\. For details '
643            'see: <http://initd.org/psycopg/docs/install.html#binary-install-from-pypi>\.',
644            UserWarning,
645            r'^psycopg2$',
646        )
647
648    # Apply extra config for all available extensions.
649    if extend:
650        extensions = load_entry_points('rattail.config.extensions')
651        for extension in extensions.values():
652            log.debug("applying '{0}' config extension".format(extension.key))
653            extension().configure(config)
654
655    # maybe configure versioning
656    if versioning is None:
657        versioning = config.versioning_enabled()
658    if versioning:
659        from rattail.db.config import configure_versioning
660        configure_versioning(config)
661
662    return config
663
664
665def default_system_paths():
666    """
667    Returns a list of default system-level config file paths, according to the
668    current platform.
669    """
670    if sys.platform == 'win32':
671
672        # Use the Windows Extensions libraries to fetch official defaults.
673        try:
674            from win32com.shell import shell, shellcon
675        except ImportError:
676            raise WindowsExtensionsNotInstalled
677        else:
678            return [
679                os.path.join(shell.SHGetSpecialFolderPath(
680                    0, shellcon.CSIDL_COMMON_APPDATA), 'rattail.conf'),
681                os.path.join(shell.SHGetSpecialFolderPath(
682                    0, shellcon.CSIDL_COMMON_APPDATA), 'rattail', 'rattail.conf'),
683            ]
684
685    return [
686        '/etc/rattail.conf',
687        '/etc/rattail/rattail.conf',
688        '/usr/local/etc/rattail.conf',
689        '/usr/local/etc/rattail/rattail.conf',
690    ]
691
692
693def default_user_paths():
694    """
695    Returns a list of default user-level config file paths, according to the
696    current platform.
697    """
698    if sys.platform == 'win32':
699
700        # Use the Windows Extensions libraries to fetch official defaults.
701        try:
702            from win32com.shell import shell, shellcon
703        except ImportError:
704            raise WindowsExtensionsNotInstalled
705        else:
706            return [
707                os.path.join(shell.SHGetSpecialFolderPath(
708                    0, shellcon.CSIDL_APPDATA), 'rattail.conf'),
709                os.path.join(shell.SHGetSpecialFolderPath(
710                    0, shellcon.CSIDL_APPDATA), 'rattail', 'rattail.conf'),
711            ]
712
713    return [
714        os.path.expanduser('~/.rattail.conf'),
715        os.path.expanduser('~/.rattail/rattail.conf'),
716    ]
717
718
719def get_user_dir(create=False):
720    """
721    Returns a path to the "preferred" user-level folder, in which additional
722    config files (etc.) may be placed as needed.  This essentially returns a
723    platform-specific variation of ``~/.rattail/``.
724
725    If ``create`` is ``True``, then the folder will be created if it does not
726    already exist.
727    """
728    if sys.platform == 'win32':
729
730        # Use the Windows Extensions libraries to fetch official defaults.
731        try:
732            from win32com.shell import shell, shellcon
733        except ImportError:
734            raise WindowsExtensionsNotInstalled
735        else:
736            path = os.path.join(shell.SHGetSpecialFolderPath(
737                0, shellcon.CSIDL_APPDATA), 'rattail')
738
739    else:
740        path = os.path.expanduser('~/.rattail')
741
742    if create and not os.path.exists(path):
743        os.mkdir(path)
744    return path
745
746
747def get_user_file(filename, createdir=False):
748    """
749    Returns a full path to a user-level config file location.  This is obtained
750    by first calling :func:`get_user_dir()` and then joining the result with
751    ``filename``.
752
753    The ``createdir`` argument will be passed to :func:`get_user_dir()` as its
754    ``create`` arg, and may be used to ensure the user-level folder exists.
755    """
756    return os.path.join(get_user_dir(create=createdir), filename)
757
758
759class ConfigProfile(object):
760    """
761    Generic class to represent a config "profile", as used by the filemon and
762    datasync daemons, etc.
763
764    .. todo::
765       This clearly needs more documentation.
766    """
767
768    @property
769    def section(self):
770        """
771        Each subclass of ``ConfigProfile`` must define this.
772        """
773        raise NotImplementedError
774
775    def _config_string(self, option, **kwargs):
776        return self.config.get(self.section, '{0}.{1}'.format(self.key, option), **kwargs)
777
778    def _config_boolean(self, option, default=None):
779        return self.config.getbool(self.section, '{0}.{1}'.format(self.key, option),
780                                   default=default)
781
782    def _config_int(self, option, minimum=1, default=None):
783        """
784        Retrieve the *integer* value for the given option.
785        """
786        option = '{}.{}'.format(self.key, option)
787
788        # try to read value from config
789        value = self.config.getint(self.section, option)
790        if value is not None:
791
792            # found a value; validate it
793            if value < minimum:
794                log.warning("config value %s is too small; falling back to minimum "
795                            "of %s for option: %s", value, minimum, option)
796                value = minimum
797
798        # or, use default value, if valid
799        elif default is not None and default >= minimum:
800            value = default
801
802        # or, just use minimum value
803        else:
804            value = minimum
805
806        return value
807
808    def _config_list(self, option):
809        return parse_list(self._config_string(option))
810
811
812class FreeTDSLoggingFilter(logging.Filter):
813    """
814    Custom logging filter, to suppress certain "write to server failed"
815    messages relating to FreeTDS database connections.  They seem harmless and
816    just cause unwanted error emails.
817    """
818
819    def __init__(self, *args, **kwargs):
820        logging.Filter.__init__(self, *args, **kwargs)
821        self.pattern = re.compile(r'(?:Read from|Write to) the server failed')
822
823    def filter(self, record):
824        if (record.name == 'sqlalchemy.pool.QueuePool'
825            and record.funcName == '_finalize_fairy'
826            and record.levelno == logging.ERROR
827            and record.msg == "Exception during reset or similar"
828            and record.exc_info
829            and self.pattern.search(six.text_type(record.exc_info[1]))):
830
831            # Log this as a warning instead of error, to cut down on our noise.
832            record.levelno = logging.WARNING
833            record.levelname = 'WARNING'
834
835        return True
Note: See TracBrowser for help on using the repository browser.