source: rattail/rattail/config.py @ 9588664

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

Don't consult DB settings when determining app node type

i.e. config file only for that

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