Changeset - dd69733ac5fa
[Not reviewed]
0 3 3
Lance Edgar (lance) - 21 months ago 2023-01-18 16:51:57
lance@edbob.org
Move common logic for `poser install` commands into rattail

ideally all apps are installed the same way, easily..
6 files changed with 584 insertions and 216 deletions:
0 comments (0 inline, 0 general)
rattail/app.py
Show inline comments
 
@@ -1184,17 +1184,21 @@ class AppHandler(object):
 
                os.mkdir(path)
 

	
 
    def render_mako_template(self, template_path, context,
 
                             output_path=None, **kwargs):
 
                             template=None, output_path=None, **kwargs):
 
        """
 
        Convenience method to render any (specified) Mako template.
 
        """
 
        output = Template(filename=template_path).render(**context)
 
        if not template:
 
            template = Template(filename=template_path)
 
        output = template.render(**context)
 
        if output_path:
 
            with open(output_path, 'wt') as f:
 
                f.write(output)
 
        return output
 

	
 
    def make_config_file(self, file_type, output_path, template_path=None,
 
    def make_config_file(self, file_type, output_path,
 
                         template=None,
 
                         template_path=None,
 
                         **kwargs):
 
        """
 
        Write a new config file of given type to specified location.
 
@@ -1208,6 +1212,8 @@ class AppHandler(object):
 
           written.  If this is a folder, then the filename is deduced
 
           from the ``file_type``.
 

	
 
        :param template: Optional reference to a Mako template instance.
 

	
 
        :param template_path: Optional path to config file template to
 
           use.  If not specified, it will be looked up dynamically
 
           based on the ``file_type``.  Note that the first template
 
@@ -1221,7 +1227,7 @@ class AppHandler(object):
 
        :returns: Final path to which new config file was written.
 
        """
 
        # lookup template if not specified
 
        if not template_path:
 
        if not template and not template_path:
 
            template_path = self.find_config_template(file_type)
 
            if not template_path:
 
                raise RuntimeError("config template not found for type: {}".format(file_type))
 
@@ -1231,7 +1237,7 @@ class AppHandler(object):
 
            output_path = os.path.join(output_path, '{}.conf'.format(file_type))
 

	
 
        # just copy file as-is unless it's mako
 
        if not template_path.endswith('.mako'):
 
        if not template and not template_path.endswith('.mako'):
 
            shutil.copy(template_path, output_path)
 
            return output_path
 

	
 
@@ -1250,6 +1256,7 @@ class AppHandler(object):
 
        }
 
        context.update(kwargs)
 
        self.render_mako_template(template_path, context,
 
                                  template=template,
 
                                  output_path=output_path)
 
        return output_path
 

	
rattail/commands/__init__.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2022 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -28,3 +28,4 @@ from __future__ import unicode_literals, absolute_import
 

	
 
from .core import main, Command, Subcommand, date_argument, list_argument
 
from .importing import ImportSubcommand, ImportFromCSV, ImportFileSubcommand, ExportFileSubcommand
 
from .install import InstallSubcommand
rattail/commands/install.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
#  Rattail 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.
 
#
 
#  Rattail 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
 
#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
 
#
 
################################################################################
 
"""
 
Installer Commands
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import stat
 
import subprocess
 
import sys
 

	
 
from alembic.util.messaging import obfuscate_url_pw
 
from mako.lookup import TemplateLookup
 

	
 
from rattail.commands.core import Subcommand
 
from rattail.files import resource_path
 

	
 

	
 
class InstallSubcommand(Subcommand):
 
    """
 
    Base class and default implementation for ``poser install``
 
    commands.  Note that there is no ``rattail install`` command.
 
    """
 
    name = 'install'
 

	
 
    # nb. these must be explicitly set b/c config is not available
 
    # when running normally, e.g. `poser -n install`
 
    #app_title = "Poser"
 
    #app_package = 'poser'
 
    #app_eggname = 'Poser'
 
    #app_pypiname = 'Poser'
 

	
 
    def run(self, args):
 

	
 
        self.templates = InstallerTemplateLookup(directories=[
 
            resource_path('{}:templates/installer'.format(self.app_package)),
 
            resource_path('rattail:templates/installer'),
 
        ])
 

	
 
        self.schema_installed = False
 
        self.do_install_steps()
 

	
 
        self.rprint("\n\t[bold green]initial setup is complete![/bold green]")
 

	
 
        self.show_goodbye()
 
        self.rprint()
 

	
 
    def do_install_steps(self):
 
        self.show_welcome()
 
        self.sanity_check()
 

	
 
        # prompt user for db info
 
        dbinfo = self.get_dbinfo()
 

	
 
        # get context for generated app files
 
        context = self.make_template_context(dbinfo)
 

	
 
        # make the appdir
 
        self.make_appdir(context)
 

	
 
        # install db schema if user likes
 
        self.schema_installed = self.install_db_schema(dbinfo)
 

	
 
    def show_welcome(self, **kwargs):
 

	
 
        # nb. technically these would be required and auto-installed
 
        # as needed (later), but seems better to do this explicitly
 
        # up-front before any other command output
 
        self.require_prompt_toolkit()
 
        self.require_rich()
 

	
 
        self.rprint("\n\t[blue]Welcome to {}![/blue]".format(self.app_title))
 
        self.rprint("\n\tThis tool will install and configure a new app.")
 
        self.rprint("\n\t[italic]NB. You should already have created a new database in PostgreSQL or MySQL.[/italic]")
 

	
 
        # continue?
 
        if not self.basic_prompt("continue?", True, is_bool=True):
 
            self.rprint()
 
            sys.exit(0)
 

	
 
    def sanity_check(self, **kwargs):
 

	
 
        # appdir must not yet exist
 
        appdir = os.path.join(sys.prefix, 'app')
 
        if os.path.exists(appdir):
 
            self.rprint("\n\t[bold red]appdir already exists:[/bold red]  {}\n".format(appdir))
 
            sys.exit(1)
 

	
 
    def get_dbinfo(self, **kwargs):
 
        dbinfo = {}
 
        
 
        # get db info
 
        dbinfo['dbtype'] = self.basic_prompt('db type', 'postgresql')
 
        dbinfo['dbhost'] = self.basic_prompt('db host', 'localhost')
 
        default_port = '3306' if dbinfo['dbtype'] == 'mysql' else '5432'
 
        dbinfo['dbport'] = self.basic_prompt('db port', default_port)
 
        dbinfo['dbname'] = self.basic_prompt('db name', self.app_package)
 
        dbinfo['dbuser'] = self.basic_prompt('db user', 'rattail')
 

	
 
        # get db password
 
        dbinfo['dbpass'] = None
 
        while not dbinfo['dbpass']:
 
            dbinfo['dbpass'] = self.basic_prompt('db pass', is_password=True)
 

	
 
        # test db connection
 
        self.rprint("\n\ttesting db connection... ", end='')
 
        dbinfo['dburl'] = self.make_db_url(dbinfo['dbtype'],
 
                                           dbinfo['dbhost'],
 
                                           dbinfo['dbport'],
 
                                           dbinfo['dbname'],
 
                                           dbinfo['dbuser'],
 
                                           dbinfo['dbpass'])
 
        error = self.test_db_connection(dbinfo['dburl'])
 
        if error:
 
            self.rprint("[bold red]cannot connect![/bold red] ..error was:")
 
            self.rprint("\n{}".format(error))
 
            self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n")
 
            sys.exit(1)
 
        self.rprint("[bold green]good[/bold green]")
 

	
 
        return dbinfo
 

	
 
    def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass):
 
        try:
 
            # newer style
 
            from sqlalchemy.engine import URL
 
            factory = URL.create
 
        except ImportError:
 
            # older style
 
            from sqlalchemy.engine.url import URL
 
            factory = URL
 

	
 
        if dbtype == 'mysql':
 
            drivername = 'mysql+mysqlconnector'
 
        else:
 
            drivername = 'postgresql+psycopg2'
 

	
 
        return factory(drivername=drivername,
 
                       username=dbuser,
 
                       password=dbpass,
 
                       host=dbhost,
 
                       port=dbport,
 
                       database=dbname)
 

	
 
    def test_db_connection(self, url):
 
        from sqlalchemy import create_engine
 

	
 
        engine = create_engine(url)
 

	
 
        # check for random table; does not matter if it exists, we
 
        # just need to test interaction and this is a neutral way
 
        try:
 
            engine.has_table('whatever')
 
        except Exception as error:
 
            return str(error)
 

	
 
    def make_template_context(self, dbinfo, **kwargs):
 
        envname = os.path.basename(sys.prefix)
 
        appdir = os.path.join(sys.prefix, 'app')
 
        return {
 
            'envdir': sys.prefix,
 
            'envname': envname,
 
            'app_package': self.app_package,
 
            'app_title': self.app_title,
 
            'pypi_name': self.app_pypiname,
 
            'appdir': appdir,
 
            'db_url': dbinfo['dburl'],
 
            'pyramid_egg': self.app_eggname,
 
            'beaker_key': envname,
 
        }
 

	
 
    def make_appdir(self, context, **kwargs):
 
        rootpkg = self.app_package
 

	
 
        # appdir
 
        appdir = os.path.join(sys.prefix, 'app')
 
        self.app.make_appdir(appdir)
 

	
 
        # rattail.conf
 
        template = self.templates.get_template('rattail.conf.mako')
 
        self.app.make_config_file(
 
            'rattail', os.path.join(appdir, 'rattail.conf'),
 
            template=template, **context)
 

	
 
        # quiet.conf, silent.conf
 
        self.app.make_config_file('quiet', appdir)
 
        self.app.make_config_file('silent', appdir)
 

	
 
        # web.conf
 
        self.app.make_config_file(
 
            'web-complete', os.path.join(appdir, 'web.conf'),
 
            **context)
 

	
 
        # upgrade.sh
 
        template = self.templates.get_template('upgrade.sh.mako')
 
        path = os.path.join(appdir, 'upgrade.sh')
 
        self.app.render_mako_template(None, context, template=template,
 
                                      output_path=path)
 
        os.chmod(path, stat.S_IRWXU
 
                 | stat.S_IRGRP
 
                 | stat.S_IXGRP
 
                 | stat.S_IROTH
 
                 | stat.S_IXOTH)
 

	
 
        self.rprint("\n\tappdir created at:  [bold green]{}[/bold green]".format(appdir))
 

	
 
    def install_db_schema(self, dbinfo, **kwargs):
 

	
 
        if not self.basic_prompt("install db schema?", True, is_bool=True):
 
            return False
 

	
 
        self.rprint()
 

	
 
        # install db schema
 
        cmd = [os.path.join(sys.prefix, 'bin', 'alembic'),
 
               '-c', os.path.join(sys.prefix, 'app', 'rattail.conf'),
 
               'upgrade', 'heads']
 
        subprocess.check_call(cmd)
 

	
 
        # put initial settings
 
        self.put_settings()
 

	
 
        self.rprint("\n\tdb schema installed to:  [bold green]{}[/bold green]".format(
 
            obfuscate_url_pw(dbinfo['dburl'])))
 

	
 
        # make admin, if user likes
 
        self.make_admin_user()
 

	
 
        return True
 

	
 
    def put_settings(self, **kwargs):
 

	
 
        # set falafel theme
 
        self.put_setting('tailbone.theme', 'falafel')
 

	
 
        # hide theme picker
 
        self.put_setting('tailbone.themes.expose_picker', 'false')
 

	
 
        # set main image
 
        self.put_setting('tailbone.main_image_url', '/tailbone/img/home_logo.png')
 

	
 
        # set header image
 
        self.put_setting('tailbone.header_image_url', '/tailbone/img/rattail.ico')
 

	
 
        # set favicon image
 
        self.put_setting('tailbone.favicon_url', '/tailbone/img/rattail.ico')
 

	
 
        # set default grid page size
 
        self.put_setting('tailbone.grid.default_pagesize', '20')
 

	
 
    def put_setting(self, name, value):
 
        cmd = [os.path.join(sys.prefix, 'bin', 'rattail'),
 
               '-c', os.path.join(sys.prefix, 'app', 'silent.conf'),
 
               'setting-put', name, value]
 
        subprocess.check_call(cmd)
 

	
 
    def make_admin_user(self, **kwargs):
 

	
 
        if not self.basic_prompt("create admin user?", True, is_bool=True):
 
            return False
 

	
 
        # get admin credentials
 
        username = self.basic_prompt('admin username', 'admin')
 
        password = None
 
        while not password:
 
            password = self.basic_prompt('admin password', is_password=True)
 
            if password:
 
                confirm = self.basic_prompt('confirm password', is_password=True)
 
                if not confirm or confirm != password:
 
                    self.rprint("[bold yellow]passwords did not match[/bold yellow]")
 
                    password = None
 
        fullname = self.basic_prompt('full name')
 

	
 
        self.rprint()
 

	
 
        # make admin user
 
        cmd = [os.path.join(sys.prefix, 'bin', 'rattail'),
 
               '-c', os.path.join(sys.prefix, 'app', 'quiet.conf'),
 
               'make-user', '--admin', username, '--password', password]
 
        if fullname:
 
            cmd.extend(['--full-name', fullname])
 
        subprocess.check_call(cmd)
 

	
 
        self.rprint("\n\tadmin user created:  [bold green]{}[/bold green]".format(
 
            username))
 
        return True
 

	
 
    def show_goodbye(self):
 
        if self.schema_installed:
 
            self.rprint("\n\tyou can run the web app with:")
 
            self.rprint("\n\t[blue]cd {}[/blue]".format(sys.prefix))
 
            self.rprint("\t[blue]bin/pserve file+ini:app/web.conf[/blue]")
 

	
 

	
 
class InstallerTemplateLookup(TemplateLookup):
 
    """
 
    This logic was largely copied/inspired from pyramid_mako,
 
    https://github.com/Pylons/pyramid_mako/blob/main/src/pyramid_mako/__init__.py
 
    """
 

	
 
    def adjust_uri(self, uri, relativeto):
 

	
 
        # do not adjust "resource path spec" uri
 
        isabs = os.path.isabs(uri)
 
        if (not isabs) and (':' in uri):
 
            return uri
 

	
 
        return super(InstallerTemplateLookup, self).adjust_uri(uri, relativeto)
 

	
 
    def get_template(self, uri):
 

	
 
        # check if uri looks like a "resource path spec"
 
        isabs = os.path.isabs(uri)
 
        if (not isabs) and (':' in uri):
 

	
 
            # it does..first use normal logic to try fetching from cache
 
            try:
 
                if self.filesystem_checks:
 
                    return self._check(uri, self._collection[uri])
 
                else:
 
                    return self._collection[uri]
 
            except KeyError as e:
 

	
 
                # but if not already in cache, must convert resource
 
                # path spec to absolute path on disk, and load that
 
                path = resource_path(uri)
 
                if os.path.exists(path):
 
                    return self._load(path, uri)
 

	
 
        # fallback to normal logic
 
        return super(InstallerTemplateLookup, self).get_template(uri)
rattail/projects/rattail/package/commands.py.mako
Show inline comments
 
@@ -3,15 +3,9 @@
 
${name} commands
 
"""
 

	
 
import os
 
import stat
 
import subprocess
 
import sys
 

	
 
from alembic.util.messaging import obfuscate_url_pw
 

	
 
from rattail import commands
 
from rattail.files import resource_path
 

	
 
from ${python_name} import __version__
 

	
 
@@ -43,215 +37,19 @@ class HelloWorld(commands.Subcommand):
 
    description = __doc__.strip()
 

	
 
    def run(self, args):
 
        self.stdout.write("hello world!\n")
 
        self.rprint("\n\t[blue]Welcome to ${name} {}![/blue]\n".format(__version__))
 

	
 

	
 
class Install(commands.Subcommand):
 
class Install(commands.InstallSubcommand):
 
    """
 
    Install the ${name} app
 
    """
 
    name = 'install'
 
    description = __doc__.strip()
 

	
 
    def run(self, args):
 

	
 
        # nb. technically these would be required and auto-installed
 
        # as needed (later), but seems better to do this explicitly
 
        # up-front before any other command output
 
        self.require_prompt_toolkit()
 
        self.require_rich()
 

	
 
        self.rprint("\n\t[blue]Welcome to ${name}![/blue]")
 
        self.rprint("\n\tThis tool will install and configure a new app.")
 
        self.rprint("\n\t[italic]NB. You should already have created a new database in PostgreSQL or MySQL.[/italic]")
 

	
 
        # continue?
 
        if not self.basic_prompt("continue?", True, is_bool=True):
 
            self.rprint()
 
            sys.exit(0)
 

	
 
        # appdir must not yet exist
 
        appdir = os.path.join(sys.prefix, 'app')
 
        if os.path.exists(appdir):
 
            self.rprint("\n\t[bold red]appdir already exists:[/bold red]  {}\n".format(appdir))
 
            sys.exit(1)
 

	
 
        # get db info
 
        dbtype = self.basic_prompt('db type', 'postgresql')
 
        dbhost = self.basic_prompt('db host', 'localhost')
 
        dbport = self.basic_prompt('db port', '3306' if dbtype == 'mysql' else '5432')
 
        dbname = self.basic_prompt('db name', '${python_name}')
 
        dbuser = self.basic_prompt('db user', 'rattail')
 

	
 
        # get db password
 
        dbpass = None
 
        while not dbpass:
 
            dbpass = self.basic_prompt('db pass', is_password=True)
 

	
 
        # test db connection
 
        self.rprint("\n\ttesting db connection... ", end='')
 
        dburl = self.make_db_url(dbtype, dbhost, dbport, dbname, dbuser, dbpass)
 
        error = self.test_db_connection(dburl)
 
        if error:
 
            self.rprint("[bold red]cannot connect![/bold red] ..error was:")
 
            self.rprint("\n{}".format(error))
 
            self.rprint("\n\t[bold yellow]aborting mission[/bold yellow]\n")
 
            sys.exit(1)
 
        self.rprint("[bold green]good[/bold green]")
 

	
 
        # make the appdir
 
        self.app.make_appdir(appdir)
 

	
 
        # shared context for generated app files
 
        envname = os.path.basename(sys.prefix)
 
        context = {
 
            'envdir': sys.prefix,
 
            'envname': envname,
 
            'app_package': '${python_name}',
 
            'app_title': "${name}",
 
            'pypi_name': '${python_project_name}',
 
            'appdir': appdir,
 
            'db_url': dburl,
 
            'pyramid_egg': '${egg_name}',
 
            'beaker_key': envname,
 
        }
 

	
 
        # make config files
 
        rattail_conf = self.app.make_config_file(
 
            'rattail', os.path.join(appdir, 'rattail.conf'),
 
            template_path=resource_path('${python_name}:templates/installer/rattail.conf.mako'),
 
            **context)
 
        quiet_conf = self.app.make_config_file('quiet', appdir)
 
        web_conf = self.app.make_config_file(
 
            'web-complete', os.path.join(appdir, 'web.conf'),
 
            **context)
 

	
 
        # make upgrade script
 
        path = os.path.join(appdir, 'upgrade.sh')
 
        self.app.render_mako_template(
 
            resource_path('${python_name}:templates/installer/upgrade.sh.mako'),
 
            context, output_path=path)
 
        os.chmod(path, stat.S_IRWXU
 
                 | stat.S_IRGRP
 
                 | stat.S_IXGRP
 
                 | stat.S_IROTH
 
                 | stat.S_IXOTH)
 

	
 
        self.rprint("\n\tappdir created at:  [bold green]{}[/bold green]".format(appdir))
 

	
 
        bindir = os.path.join(sys.prefix, 'bin')
 

	
 
        schema_installed = False
 
        if self.basic_prompt("install db schema?", True, is_bool=True):
 
            self.rprint()
 

	
 
            # install db schema
 
            alembic = os.path.join(bindir, 'alembic')
 
            cmd = [alembic, '-c', rattail_conf, 'upgrade', 'heads']
 
            subprocess.check_call(cmd)
 
            schema_installed = True
 

	
 
            rattail = os.path.join(bindir, 'rattail')
 

	
 
            # set falafel theme
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.theme', 'falafel']
 
            subprocess.check_call(cmd)
 

	
 
            # hide theme picker
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.themes.expose_picker', 'false']
 
            subprocess.check_call(cmd)
 

	
 
            # set main image
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.main_image_url', '/tailbone/img/home_logo.png']
 
            subprocess.check_call(cmd)
 

	
 
            # set header image
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.header_image_url', '/tailbone/img/rattail.ico']
 
            subprocess.check_call(cmd)
 

	
 
            # set favicon image
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.favicon_url', '/tailbone/img/rattail.ico']
 
            subprocess.check_call(cmd)
 

	
 
            # set default grid page size
 
            cmd = [rattail, '-c', quiet_conf, '--no-versioning',
 
                   'setting-put', 'tailbone.grid.default_pagesize', '20']
 
            subprocess.check_call(cmd)
 

	
 
            self.rprint("\n\tdb schema installed to:  [bold green]{}[/bold green]".format(
 
                obfuscate_url_pw(dburl)))
 

	
 
            if self.basic_prompt("create admin user?", True, is_bool=True):
 

	
 
                # get admin credentials
 
                username = self.basic_prompt('admin username', 'admin')
 
                password = None
 
                while not password:
 
                    password = self.basic_prompt('admin password', is_password=True)
 
                    if password:
 
                        confirm = self.basic_prompt('confirm password', is_password=True)
 
                        if not confirm or confirm != password:
 
                            self.rprint("[bold yellow]passwords did not match[/bold yellow]")
 
                            password = None
 
                fullname = self.basic_prompt('full name')
 

	
 
                self.rprint()
 

	
 
                # make admin user
 
                rattail = os.path.join(bindir, 'rattail')
 
                cmd = [rattail, '-c', quiet_conf, 'make-user', '-A', username,
 
                       '--password', password]
 
                if fullname:
 
                    cmd.extend(['--full-name', fullname])
 
                subprocess.check_call(cmd)
 

	
 
                self.rprint("\n\tadmin user created:  [bold green]{}[/bold green]".format(
 
                    username))
 

	
 
        self.rprint("\n\t[bold green]initial setup is complete![/bold green]")
 

	
 
        if schema_installed:
 
            self.rprint("\n\tyou can run the web app with:")
 
            self.rprint("\n\t[blue]cd {}[/blue]".format(sys.prefix))
 
            self.rprint("\t[blue]bin/pserve file+ini:app/web.conf[/blue]")
 

	
 
        self.rprint()
 

	
 
    def make_db_url(self, dbtype, dbhost, dbport, dbname, dbuser, dbpass):
 
        try:
 
            # newer style
 
            from sqlalchemy.engine import URL
 
            factory = URL.create
 
        except ImportError:
 
            # older style
 
            from sqlalchemy.engine.url import URL
 
            factory = URL
 

	
 
        if dbtype == 'mysql':
 
            drivername = 'mysql+mysqlconnector'
 
        else:
 
            drivername = 'postgresql+psycopg2'
 

	
 
        return factory(drivername=drivername,
 
                       username=dbuser,
 
                       password=dbpass,
 
                       host=dbhost,
 
                       port=dbport,
 
                       database=dbname)
 

	
 
    def test_db_connection(self, url):
 
        from sqlalchemy import create_engine
 

	
 
        engine = create_engine(url)
 

	
 
        # check for random table; does not matter if it exists, we
 
        # just need to test interaction and this is a neutral way
 
        try:
 
            engine.has_table('whatever')
 
        except Exception as error:
 
            return str(error)
 
    # nb. these must be explicitly set b/c config is not available
 
    # when running normally, e.g. `${python_name} -n install`
 
    app_title = "${name}"
 
    app_package = '${python_name}'
 
    app_eggname = '${egg_name}'
 
    app_pypiname = '${python_project_name}'
rattail/templates/installer/rattail.conf.mako
Show inline comments
 
new file 100644
 
## -*- mode: conf; -*-
 

	
 
<%text>############################################################</%text>
 
#
 
# main config for ${app_title}
 
#
 
<%text>############################################################</%text>
 

	
 

	
 
<%text>##############################</%text>
 
# rattail
 
<%text>##############################</%text>
 

	
 
${self.section_rattail()}
 

	
 
${self.section_rattail_config()}
 

	
 
${self.section_rattail_db()}
 

	
 
${self.section_rattail_mail()}
 

	
 
${self.section_rattail_upgrades()}
 

	
 

	
 
<%text>##############################</%text>
 
# alembic
 
<%text>##############################</%text>
 

	
 
${self.section_alembic()}
 

	
 

	
 
<%text>##############################</%text>
 
# logging
 
<%text>##############################</%text>
 

	
 
${self.sectiongroup_logging()}
 

	
 

	
 
######################################################################
 
## section templates below
 
######################################################################
 

	
 
<%def name="section_rattail()">
 
[rattail]
 
app_title = ${app_title}
 
app_package = ${app_package}
 
timezone.default = ${timezone}
 
appdir = ${appdir}
 
datadir = ${os.path.join(appdir, 'data')}
 
batch.files = ${os.path.join(appdir, 'data', 'batch')}
 
workdir = ${os.path.join(appdir, 'work')}
 
export.files = ${os.path.join(appdir, 'data', 'exports')}
 
</%def>
 

	
 
<%def name="section_rattail_config()">
 
[rattail.config]
 
# require = /etc/rattail/rattail.conf
 
configure_logging = true
 
usedb = true
 
preferdb = true
 
</%def>
 

	
 
<%def name="section_rattail_db()">
 
[rattail.db]
 
default.url = ${db_url}
 
versioning.enabled = true
 
</%def>
 

	
 
<%def name="section_rattail_mail()">
 
[rattail.mail]
 

	
 
# this is the global email shutoff switch
 
#send_emails = false
 

	
 
# recommended setup is to always talk to postfix on localhost and then
 
# it can handle any need complexities, e.g. sending to relay
 
smtp.server = localhost
 

	
 
# by default only email templates from rattail proper are used
 
templates = rattail:templates/mail
 

	
 
# this is the "default" email profile, from which all others initially
 
# inherit, but most/all profiles will override these values
 
default.prefix = [${app_title}]
 
default.from = rattail@localhost
 
default.to = root@localhost
 
# nb. in test environment it can be useful to disable by default, and
 
# then selectively enable certain (e.g. feedback, upgrade) emails
 
#default.enabled = false
 
</%def>
 

	
 
<%def name="section_rattail_upgrades()">
 
[rattail.upgrades]
 
command = ${os.path.join(appdir, 'upgrade.sh')} --verbose
 
files = ${os.path.join(appdir, 'data', 'upgrades')}
 
</%def>
 

	
 
<%def name="section_alembic()">
 
[alembic]
 
script_location = rattail.db:alembic
 
version_locations = ${app_package}.db:alembic/versions rattail.db:alembic/versions
 
</%def>
 

	
 
<%def name="sectiongroup_logging()">
 
[loggers]
 
keys = root, exc_logger, beaker, txn, sqlalchemy, django_db, flufl_bounce, requests
 

	
 
[handlers]
 
keys = file, console, email
 

	
 
[formatters]
 
keys = generic, console
 

	
 
[logger_root]
 
handlers = file, console
 
level = DEBUG
 

	
 
[logger_exc_logger]
 
qualname = exc_logger
 
handlers = email
 
level = ERROR
 

	
 
[logger_beaker]
 
qualname = beaker
 
handlers =
 
level = INFO
 

	
 
[logger_txn]
 
qualname = txn
 
handlers =
 
level = INFO
 

	
 
[logger_sqlalchemy]
 
qualname = sqlalchemy.engine
 
handlers =
 
# handlers = file
 
# level = INFO
 

	
 
[logger_django_db]
 
qualname = django.db.backends
 
handlers =
 
level = INFO
 
# level = DEBUG
 

	
 
[logger_flufl_bounce]
 
qualname = flufl.bounce
 
handlers =
 
level = WARNING
 

	
 
[logger_requests]
 
qualname = requests
 
handlers =
 
# level = WARNING
 

	
 
[handler_file]
 
class = handlers.RotatingFileHandler
 
args = (${repr(os.path.join(appdir, 'log', 'rattail.log'))}, 'a', 1000000, 100, 'utf_8')
 
formatter = generic
 

	
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
formatter = console
 
# formatter = generic
 
# level = INFO
 
# level = WARNING
 

	
 
[handler_email]
 
class = handlers.SMTPHandler
 
args = ('localhost', 'rattail@localhost', ['root@localhost'], "[Rattail] Logging")
 
formatter = generic
 
level = ERROR
 

	
 
[formatter_generic]
 
format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_console]
 
format = %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
 
</%def>
rattail/templates/installer/upgrade.sh.mako
Show inline comments
 
new file 100644
 
#!/bin/sh -e
 
<%text>##################################################</%text>
 
#
 
# upgrade script for ${app_title} app
 
#
 
<%text>##################################################</%text>
 

	
 
if [ "$1" = "--verbose" ]; then
 
    VERBOSE='--verbose'
 
    QUIET=
 
else
 
    VERBOSE=
 
    QUIET='--quiet'
 
fi
 

	
 
cd ${envdir}
 

	
 
PIP='bin/pip'
 
ALEMBIC='bin/alembic'
 

	
 
# upgrade pip and friends
 
$PIP install $QUIET --disable-pip-version-check --upgrade pip
 
$PIP install $QUIET --upgrade setuptools wheel
 

	
 
# upgrade app proper
 
$PIP install $QUIET --upgrade --upgrade-strategy eager ${pypi_name}
 

	
 
# migrate schema
 
$ALEMBIC -c app/rattail.conf upgrade heads
0 comments (0 inline, 0 general)