fix: ensure config has no app when constructor finishes
had to move `make_engine_from_config()` out of app handler and define as a separate function, so that `get_engines()` did not need to instantiate the app handler. because if it did, then config extensions would lose the ability to set a default app handler - er, they could do it but it would be ignored
This commit is contained in:
		
							parent
							
								
									3ab181b129
								
							
						
					
					
						commit
						c3efbfbf7b
					
				
					 6 changed files with 110 additions and 102 deletions
				
			
		| 
						 | 
					@ -142,63 +142,6 @@ class AppHandler:
 | 
				
			||||||
            if not os.path.exists(path):
 | 
					            if not os.path.exists(path):
 | 
				
			||||||
                os.mkdir(path)
 | 
					                os.mkdir(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def make_engine_from_config(
 | 
					 | 
				
			||||||
            self,
 | 
					 | 
				
			||||||
            config_dict,
 | 
					 | 
				
			||||||
            prefix='sqlalchemy.',
 | 
					 | 
				
			||||||
            **kwargs):
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        Construct a new DB engine from configuration dict.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        This is a wrapper around upstream
 | 
					 | 
				
			||||||
        :func:`sqlalchemy:sqlalchemy.engine_from_config()`.  For even
 | 
					 | 
				
			||||||
        broader context of the SQLAlchemy
 | 
					 | 
				
			||||||
        :class:`~sqlalchemy:sqlalchemy.engine.Engine` and their
 | 
					 | 
				
			||||||
        configuration, see :doc:`sqlalchemy:core/engines`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 dict, 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]
 | 
					 | 
				
			||||||
           default.url = sqlite:///tmp/default.sqlite
 | 
					 | 
				
			||||||
           default.poolclass = sqlalchemy.pool:NullPool
 | 
					 | 
				
			||||||
           default.pool_pre_ping = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Note that if present, the ``poolclass`` value must be a "spec"
 | 
					 | 
				
			||||||
        string, as required by
 | 
					 | 
				
			||||||
        :func:`~wuttjamaican.util.load_object()`.
 | 
					 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
        import sqlalchemy as sa
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        config_dict = dict(config_dict)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # convert 'poolclass' arg to actual class
 | 
					 | 
				
			||||||
        key = f'{prefix}poolclass'
 | 
					 | 
				
			||||||
        if key in config_dict and 'poolclass' not in kwargs:
 | 
					 | 
				
			||||||
            kwargs['poolclass'] = load_object(config_dict.pop(key))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # convert 'pool_pre_ping' arg to boolean
 | 
					 | 
				
			||||||
        key = f'{prefix}pool_pre_ping'
 | 
					 | 
				
			||||||
        if key in config_dict and 'pool_pre_ping' not in kwargs:
 | 
					 | 
				
			||||||
            kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        engine = sa.engine_from_config(config_dict, prefix, **kwargs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return engine
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def make_session(self, **kwargs):
 | 
					    def make_session(self, **kwargs):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Creates a new SQLAlchemy session for the app DB.  By default
 | 
					        Creates a new SQLAlchemy session for the app DB.  By default
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -570,12 +570,12 @@ class WuttaConfig:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        See also :doc:`/narr/handlers/app`.
 | 
					        See also :doc:`/narr/handlers/app`.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if not hasattr(self, 'app'):
 | 
					        if not hasattr(self, '_app'):
 | 
				
			||||||
            spec = self.get(f'{self.appname}.app.handler', usedb=False,
 | 
					            spec = self.get(f'{self.appname}.app.handler', usedb=False,
 | 
				
			||||||
                            default='wuttjamaican.app:AppHandler')
 | 
					                            default='wuttjamaican.app:AppHandler')
 | 
				
			||||||
            factory = load_object(spec)
 | 
					            factory = load_object(spec)
 | 
				
			||||||
            self.app = factory(self)
 | 
					            self._app = factory(self)
 | 
				
			||||||
        return self.app
 | 
					        return self._app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WuttaConfigExtension:
 | 
					class WuttaConfigExtension:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
################################################################################
 | 
					################################################################################
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  WuttJamaican -- Base package for Wutta Framework
 | 
					#  WuttJamaican -- Base package for Wutta Framework
 | 
				
			||||||
#  Copyright © 2023 Lance Edgar
 | 
					#  Copyright © 2023-2024 Lance Edgar
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
#  This file is part of Wutta Framework.
 | 
					#  This file is part of Wutta Framework.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ from collections import OrderedDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican.util import parse_list
 | 
					from wuttjamaican.util import load_object, parse_bool, parse_list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_engines(config, prefix):
 | 
					def get_engines(config, prefix):
 | 
				
			||||||
| 
						 | 
					@ -62,8 +62,6 @@ def get_engines(config, prefix):
 | 
				
			||||||
    :returns: A dictionary of SQLAlchemy engines, with keys matching
 | 
					    :returns: A dictionary of SQLAlchemy engines, with keys matching
 | 
				
			||||||
       those found in config.
 | 
					       those found in config.
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    app = config.get_app()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    keys = config.get(f'{prefix}.keys', usedb=False)
 | 
					    keys = config.get(f'{prefix}.keys', usedb=False)
 | 
				
			||||||
    if keys:
 | 
					    if keys:
 | 
				
			||||||
        keys = parse_list(keys)
 | 
					        keys = parse_list(keys)
 | 
				
			||||||
| 
						 | 
					@ -75,11 +73,11 @@ def get_engines(config, prefix):
 | 
				
			||||||
    for key in keys:
 | 
					    for key in keys:
 | 
				
			||||||
        key = key.strip()
 | 
					        key = key.strip()
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            engines[key] = app.make_engine_from_config(cfg, prefix=f'{key}.')
 | 
					            engines[key] = make_engine_from_config(cfg, prefix=f'{key}.')
 | 
				
			||||||
        except KeyError:
 | 
					        except KeyError:
 | 
				
			||||||
            if key == 'default':
 | 
					            if key == 'default':
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    engines[key] = app.make_engine_from_config(cfg, prefix='sqlalchemy.')
 | 
					                    engines[key] = make_engine_from_config(cfg, prefix='sqlalchemy.')
 | 
				
			||||||
                except KeyError:
 | 
					                except KeyError:
 | 
				
			||||||
                    pass
 | 
					                    pass
 | 
				
			||||||
    return engines
 | 
					    return engines
 | 
				
			||||||
| 
						 | 
					@ -100,3 +98,56 @@ def get_setting(session, name):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    sql = sa.text("select value from setting where name = :name")
 | 
					    sql = sa.text("select value from setting where name = :name")
 | 
				
			||||||
    return session.execute(sql, params={'name': name}).scalar()
 | 
					    return session.execute(sql, params={'name': name}).scalar()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_engine_from_config(
 | 
				
			||||||
 | 
					        config_dict,
 | 
				
			||||||
 | 
					        prefix='sqlalchemy.',
 | 
				
			||||||
 | 
					        **kwargs):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Construct a new DB engine from configuration dict.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is a wrapper around upstream
 | 
				
			||||||
 | 
					    :func:`sqlalchemy:sqlalchemy.engine_from_config()`.  For even
 | 
				
			||||||
 | 
					    broader context of the SQLAlchemy
 | 
				
			||||||
 | 
					    :class:`~sqlalchemy:sqlalchemy.engine.Engine` and their
 | 
				
			||||||
 | 
					    configuration, see :doc:`sqlalchemy:core/engines`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 dict, 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]
 | 
				
			||||||
 | 
					       default.url = sqlite:///tmp/default.sqlite
 | 
				
			||||||
 | 
					       default.poolclass = sqlalchemy.pool:NullPool
 | 
				
			||||||
 | 
					       default.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(config_dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # convert 'poolclass' arg to actual class
 | 
				
			||||||
 | 
					    key = f'{prefix}poolclass'
 | 
				
			||||||
 | 
					    if key in config_dict and 'poolclass' not in kwargs:
 | 
				
			||||||
 | 
					        kwargs['poolclass'] = load_object(config_dict.pop(key))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # convert 'pool_pre_ping' arg to boolean
 | 
				
			||||||
 | 
					    key = f'{prefix}pool_pre_ping'
 | 
				
			||||||
 | 
					    if key in config_dict and 'pool_pre_ping' not in kwargs:
 | 
				
			||||||
 | 
					        kwargs['pool_pre_ping'] = parse_bool(config_dict.pop(key))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    engine = sa.engine_from_config(config_dict, prefix, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return engine
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,8 @@ from unittest import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from sqlalchemy import orm
 | 
					from sqlalchemy import orm
 | 
				
			||||||
 | 
					from sqlalchemy.engine import Engine
 | 
				
			||||||
 | 
					from sqlalchemy.pool import NullPool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican.db import conf
 | 
					from wuttjamaican.db import conf
 | 
				
			||||||
from wuttjamaican.conf import WuttaConfig
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
| 
						 | 
					@ -93,3 +95,38 @@ class TestGetSetting(TestCase):
 | 
				
			||||||
    def test_missing_value(self):
 | 
					    def test_missing_value(self):
 | 
				
			||||||
        value = conf.get_setting(self.session, 'foo')
 | 
					        value = conf.get_setting(self.session, 'foo')
 | 
				
			||||||
        self.assertIsNone(value)
 | 
					        self.assertIsNone(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestMakeEngineFromConfig(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_basic(self):
 | 
				
			||||||
 | 
					        engine = conf.make_engine_from_config({
 | 
				
			||||||
 | 
					            'sqlalchemy.url': 'sqlite://',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        self.assertIsInstance(engine, Engine)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_poolclass(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        engine = conf.make_engine_from_config({
 | 
				
			||||||
 | 
					            'sqlalchemy.url': 'sqlite://',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        self.assertNotIsInstance(engine.pool, NullPool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        engine = conf.make_engine_from_config({
 | 
				
			||||||
 | 
					            'sqlalchemy.url': 'sqlite://',
 | 
				
			||||||
 | 
					            'sqlalchemy.poolclass': 'sqlalchemy.pool:NullPool',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        self.assertIsInstance(engine.pool, NullPool)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_pool_pre_ping(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        engine = conf.make_engine_from_config({
 | 
				
			||||||
 | 
					            'sqlalchemy.url': 'sqlite://',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        self.assertFalse(engine.pool._pre_ping)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        engine = conf.make_engine_from_config({
 | 
				
			||||||
 | 
					            'sqlalchemy.url': 'sqlite://',
 | 
				
			||||||
 | 
					            'sqlalchemy.pool_pre_ping': 'true',
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        self.assertTrue(engine.pool._pre_ping)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,8 +9,6 @@ from unittest.mock import patch, MagicMock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import sqlalchemy as sa
 | 
					import sqlalchemy as sa
 | 
				
			||||||
from sqlalchemy import orm
 | 
					from sqlalchemy import orm
 | 
				
			||||||
from sqlalchemy.engine import Engine
 | 
					 | 
				
			||||||
from sqlalchemy.pool import NullPool
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from wuttjamaican import app, db
 | 
					from wuttjamaican import app, db
 | 
				
			||||||
from wuttjamaican.conf import WuttaConfig
 | 
					from wuttjamaican.conf import WuttaConfig
 | 
				
			||||||
| 
						 | 
					@ -47,38 +45,6 @@ class TestAppHandler(TestCase):
 | 
				
			||||||
        self.assertEqual(len(os.listdir(tempdir)), 3)
 | 
					        self.assertEqual(len(os.listdir(tempdir)), 3)
 | 
				
			||||||
        shutil.rmtree(tempdir)
 | 
					        shutil.rmtree(tempdir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_make_engine_from_config_basic(self):
 | 
					 | 
				
			||||||
        engine = self.app.make_engine_from_config({
 | 
					 | 
				
			||||||
            'sqlalchemy.url': 'sqlite://',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        self.assertIsInstance(engine, Engine)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_make_engine_from_config_poolclass(self):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        engine = self.app.make_engine_from_config({
 | 
					 | 
				
			||||||
            'sqlalchemy.url': 'sqlite://',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        self.assertNotIsInstance(engine.pool, NullPool)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        engine = self.app.make_engine_from_config({
 | 
					 | 
				
			||||||
            'sqlalchemy.url': 'sqlite://',
 | 
					 | 
				
			||||||
            'sqlalchemy.poolclass': 'sqlalchemy.pool:NullPool',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        self.assertIsInstance(engine.pool, NullPool)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_make_engine_from_config_pool_pre_ping(self):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        engine = self.app.make_engine_from_config({
 | 
					 | 
				
			||||||
            'sqlalchemy.url': 'sqlite://',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        self.assertFalse(engine.pool._pre_ping)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        engine = self.app.make_engine_from_config({
 | 
					 | 
				
			||||||
            'sqlalchemy.url': 'sqlite://',
 | 
					 | 
				
			||||||
            'sqlalchemy.pool_pre_ping': 'true',
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        self.assertTrue(engine.pool._pre_ping)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_make_session(self):
 | 
					    def test_make_session(self):
 | 
				
			||||||
        session = self.app.make_session()
 | 
					        session = self.app.make_session()
 | 
				
			||||||
        self.assertIsInstance(session, db.Session.class_)
 | 
					        self.assertIsInstance(session, db.Session.class_)
 | 
				
			||||||
| 
						 | 
					@ -118,7 +84,7 @@ class TestAppProvider(TestCase):
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        self.config = WuttaConfig(appname='wuttatest')
 | 
					        self.config = WuttaConfig(appname='wuttatest')
 | 
				
			||||||
        self.app = app.AppHandler(self.config)
 | 
					        self.app = app.AppHandler(self.config)
 | 
				
			||||||
        self.config.app = self.app
 | 
					        self.config._app = self.app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_constructor(self):
 | 
					    def test_constructor(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,7 +164,7 @@ class TestGenericHandler(TestCase):
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        self.config = WuttaConfig(appname='wuttatest')
 | 
					        self.config = WuttaConfig(appname='wuttatest')
 | 
				
			||||||
        self.app = app.AppHandler(self.config)
 | 
					        self.app = app.AppHandler(self.config)
 | 
				
			||||||
        self.config.app = self.app
 | 
					        self.config._app = self.app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_constructor(self):
 | 
					    def test_constructor(self):
 | 
				
			||||||
        handler = app.GenericHandler(self.config)
 | 
					        handler = app.GenericHandler(self.config)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -228,6 +228,17 @@ configure_logging = true
 | 
				
			||||||
            config = conf.WuttaConfig(files=[myfile])
 | 
					            config = conf.WuttaConfig(files=[myfile])
 | 
				
			||||||
            logging.config.fileConfig.assert_called_once()
 | 
					            logging.config.fileConfig.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_config_has_no_app_after_init(self):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # initial config should *not* have an app yet, otherwise
 | 
				
			||||||
 | 
					        # extensions cannot specify a default app handler
 | 
				
			||||||
 | 
					        config = conf.WuttaConfig()
 | 
				
			||||||
 | 
					        self.assertFalse(hasattr(config, '_app'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # but after that we can get an app okay
 | 
				
			||||||
 | 
					        app = config.get_app()
 | 
				
			||||||
 | 
					        self.assertIs(app, config._app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_setdefault(self):
 | 
					    def test_setdefault(self):
 | 
				
			||||||
        config = conf.WuttaConfig()
 | 
					        config = conf.WuttaConfig()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue