Changeset - 26c093f12771
[Not reviewed]
rattail/config.py
Show inline comments
 
@@ -472,12 +472,12 @@ class RattailConfig(object):
 
        return self.getbool('rattail.db', 'versioning.enabled', usedb=False,
 
                            default=False)
 

	
 
    def appdir(self, require=True):
 
    def appdir(self, require=True, **kwargs):
 
        """
 
        Returns path to the 'app' dir, if known.
 
        """
 
        get = self.require if require else self.get
 
        return get('rattail', 'appdir')
 
        return get('rattail', 'appdir', **kwargs)
 

	
 
    def datadir(self, require=True):
 
        """
rattail/data/config/rattail-complete.conf.mako
Show inline comments
 
@@ -17,6 +17,7 @@ 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')}
 

	
 
[rattail.config]
 
# require = /etc/rattail/rattail.conf
rattail/db/__init__.py
Show inline comments
 
@@ -26,6 +26,9 @@ Database Stuff
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import sys
 

	
 
try:
 
    import sqlalchemy
 
except ImportError:
 
@@ -223,3 +226,15 @@ class ConfigExtension(BaseExtension):
 
                              'trainwreck import-trainwreck')
 
            config.setdefault('rattail.importing', 'to_trainwreck.from_trainwreck.import.legacy_handler_setting',
 
                              'trainwreck.importing, trainwreck.handler')
 

	
 
            # nb. cannot fetch DB settings during init
 
            appdir = config.appdir(require=False, usedb=False)
 
            if not appdir:
 
                appdir = os.path.join(sys.prefix, 'app')
 

	
 
            poser = config.get('rattail', 'poser', usedb=False,
 
                               default=os.path.join(appdir, 'poser'))
 

	
 
            # add poser to path if it exists
 
            if os.path.isdir(poser) and poser not in sys.path:
 
                sys.path.append(poser)
rattail/poser.py
Show inline comments
 
deleted file
rattail/poser/__init__.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2022 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/>.
 
#
 
################################################################################
 
"""
 
Poser Features
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
from .handler import PoserHandler
rattail/poser/handler.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8; -*-
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2022 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/>.
 
#
 
################################################################################
 
"""
 
Poser Handler
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import sys
 
import subprocess
 
import logging
 

	
 
import six
 
from mako.lookup import TemplateLookup
 

	
 
from rattail.app import GenericHandler
 
from rattail.files import resource_path
 
from rattail.util import import_module_path, import_reload, OrderedDict, simple_error
 
from rattail.reporting import ExcelReport
 
from rattail.config import ConfigExtension
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class PoserHandler(GenericHandler):
 
    """
 
    Base class and default implementation for Poser (custom code)
 
    handler.
 
    """
 

	
 
    def sanity_check(self):
 
        poserdir = self.get_default_poser_dir()
 
        return os.path.exists(poserdir)
 

	
 
    def get_default_poser_dir(self):
 
        appdir = self.config.appdir(require=False)
 
        if not appdir:
 
            appdir = os.path.join(sys.prefix, 'app')
 
        return os.path.join(appdir, 'poser')
 

	
 
    def make_poser_dir(self, path=None, **kwargs):
 
        """
 
        Create the directory structure for Poser.
 
        """
 
        # assume default path if none specified
 
        if not path:
 
            path = self.get_default_poser_dir()
 

	
 
        # path must not yet exist
 
        path = os.path.abspath(path)
 
        if os.path.exists(path):
 
            raise RuntimeError("folder already exists: {}".format(path))
 

	
 
        # make top-level dir
 
        os.makedirs(path)
 

	
 
        # normal refresh takes care of most of it
 
        self.refresh_poser_dir(path)
 

	
 
        # make git repo
 
        subprocess.check_call(['git', 'init', path])
 
        subprocess.check_call([
 
            'bash', '-c',
 
            "cd {} && git add poser .gitignore".format(path),
 
        ])
 

	
 
        return path
 

	
 
    def refresh_poser_dir(self, path=None, **kwargs):
 
        """
 
        Refresh the basic structure for Poser.
 
        """
 
        # assume default path if none specified
 
        if not path:
 
            path = self.get_default_poser_dir()
 

	
 
        # path must already exist
 
        path = os.path.abspath(path)
 
        if not os.path.exists(path):
 
            raise RuntimeError("folder does not exist: {}".format(path))
 

	
 
        # make poser pkg dir
 
        poser = os.path.join(path, 'poser')
 
        if not os.path.exists(poser):
 
            os.makedirs(poser)
 

	
 
        # add `__init__` stub
 
        init = os.path.join(poser, '__init__.py')
 
        if not os.path.exists(init):
 
            with open(init, 'wt') as f:
 
                pass
 

	
 
        # make 'db' subpackage
 
        db = os.path.join(poser, 'db')
 
        if not os.path.exists(db):
 
            os.makedirs(db)
 

	
 
        # add `__init__` stub
 
        init = os.path.join(db, '__init__.py')
 
        if not os.path.exists(init):
 
            with open(init, 'wt') as f:
 
                pass
 

	
 
        # make 'db.model' subpackage
 
        model = os.path.join(db, 'model')
 
        if not os.path.exists(model):
 
            os.makedirs(model)
 

	
 
        # add `__init__` stub
 
        init = os.path.join(model, '__init__.py')
 
        if not os.path.exists(init):
 
            with open(init, 'wt') as f:
 
                pass
 

	
 
        # make 'db:alembic' folder
 
        alembic = os.path.join(db, 'alembic')
 
        if not os.path.exists(alembic):
 
            os.makedirs(alembic)
 

	
 
        # make 'reports' subpackage
 
        reports = os.path.join(poser, 'reports')
 
        if not os.path.exists(reports):
 
            os.makedirs(reports)
 

	
 
        # add `__init__` stub
 
        init = os.path.join(reports, '__init__.py')
 
        if not os.path.exists(init):
 
            with open(init, 'wt') as f:
 
                pass
 

	
 
        # make .gitignore
 
        gitignore = os.path.join(path, '.gitignore')
 
        # TODO: this should always overwrite a "managed" section of the file
 
        if not os.path.exists(gitignore):
 
            with open(gitignore, 'wt') as f:
 
                f.write('**/__pycache__/\n')
 

	
 
    def get_supported_report_flavors(self, **kwargs):
 
        # TODO: these should come from entry points etc.
 
        flavors = {
 
            'customer_py': {
 
                'description': "Query the Customer table (Python)",
 
                'template': '/reports/customer_py.mako',
 
                'output_fields': ['customer_number', 'customer_name'],
 
            },
 
            'customer_sql': {
 
                'description': "Query the Customer table (SQL)",
 
                'template': '/reports/customer_sql.mako',
 
                'output_fields': ['customer_number', 'customer_name'],
 
            },
 
            'product_py': {
 
                'description': "Query the Product table (Python)",
 
                'template': '/reports/product_py.mako',
 
                'output_fields': ['product_upc', 'product_description'],
 
            },
 
            'product_sql': {
 
                'description': "Query the Product table (SQL)",
 
                'template': '/reports/product_sql.mako',
 
                'output_fields': ['product_upc', 'product_description'],
 
            },
 
        }
 

	
 
        items = sorted(flavors.items(),
 
                       key=lambda itm: itm[1]['description'])
 

	
 
        items.insert(0, ('default', {
 
            'description': "Default (empty)",
 
            'template': '/reports/base.mako',
 
            'output_fields': [],
 
        }))
 

	
 
        return OrderedDict(items)
 

	
 
    def get_all_reports(self, **kwargs):
 
        from poser import reports
 

	
 
        all_reports = []
 

	
 
        path = os.path.dirname(reports.__file__)
 
        for fname in os.listdir(path):
 
            if fname.startswith('_'):
 
                continue
 
            if not fname.endswith('.py'):
 
                continue
 

	
 
            key, ext = os.path.splitext(
 
                os.path.basename(fname))
 

	
 
            report = self.normalize_report(key)
 
            if report:
 
                all_reports.append(report)
 

	
 
        return all_reports
 

	
 
    def normalize_report(self, key, **kwargs):
 
        """
 
        Load the report module for the given key, and normalize the
 
        report it contains.
 
        """
 
        from poser import reports
 

	
 
        filepath = os.path.join(os.path.dirname(reports.__file__),
 
                                '{}.py'.format(key))
 

	
 
        modpath = '.'.join((reports.__name__, key))
 

	
 
        try:
 
            mod = import_module_path(modpath)
 
            import_reload(mod)
 

	
 
        except Exception as error:
 
            log.warning("import failed for %s", modpath, exc_info=True)
 
            return {
 
                'report_key': key,
 
                'module_file_path': filepath,
 
                'error': simple_error(error),
 
                'report': None,
 
                'report_name': '??',
 
                'description': '??',
 
            }
 

	
 
        for name in dir(mod):
 
            if name.startswith('_'):
 
                continue
 

	
 
            obj = getattr(mod, name)
 
            if (isinstance(obj, type)
 
                and issubclass(obj, ExcelReport)
 
                and obj.type_key
 
                and obj.type_key.startswith('poser_')):
 

	
 
                return {
 
                    'report': obj,
 
                    'module_file_path': filepath,
 
                    'report_key': key,
 
                    'report_name': obj.name,
 
                    'description': obj.__doc__,
 
                }
 

	
 
    def normalize_report_path(self, path, **kwargs):
 
        """
 
        Normalize the report module for the given file path.
 
        """
 
        from poser import reports
 

	
 
        # is module in the poser reports package?
 
        dirname = os.path.dirname(path)
 
        if dirname != os.path.dirname(reports.__file__):
 
            return
 

	
 
        # does module have a "normal" name?
 
        fname = os.path.basename(path)
 
        if fname.startswith('_'):
 
            return
 
        if not fname.endswith('.py'):
 
            return
 

	
 
        # okay then normalize per usual
 
        key, ext = os.path.splitext(fname)
 
        return self.normalize_report(key)
 

	
 
    def make_report(self, key, name, description,
 
                    flavor=None, output_fields=[], include_comments=True,
 
                    **kwargs):
 
        """
 
        Generate a new Python module for a Poser Report.
 
        """
 
        from poser import reports
 

	
 
        # TODO: make templates dir configurable
 
        templates = [resource_path('rattail:templates/poser')]
 
        templates = TemplateLookup(directories=templates)
 

	
 
        output_fields = output_fields or []
 
        template = '/reports/base.mako'
 
        if flavor:
 
            flavors = self.get_supported_report_flavors()
 
            if flavor in flavors:
 
                template = flavors[flavor]['template']
 
                output_fields = flavors[flavor]['output_fields']
 

	
 
        template = templates.get_template(template)
 

	
 
        cap_name = ''.join([word.capitalize()
 
                            for word in key.split('_')])
 

	
 
        context = {
 
            'app_title': self.config.app_title(),
 
            'key': key,
 
            'name': name,
 
            'cap_name': cap_name,
 
            'description': description,
 
            'output_fields': output_fields,
 
            'include_comments': include_comments,
 
        }
 

	
 
        path = os.path.join(os.path.dirname(reports.__file__),
 
                            '{}.py'.format(key))
 

	
 
        with open(path, 'wt') as f:
 
            f.write(template.render(**context))
 

	
 
        return self.normalize_report(key)
 

	
 
    def replace_report(self, key, path, **kwargs):
 
        """
 
        Replace the report module from the given file path.
 
        """
 
        from poser import reports
 

	
 
        report = self.normalize_report(key)
 
        oldpath = report['module_file_path']
 
        newpath = os.path.join(os.path.dirname(reports.__file__),
 
                               os.path.basename(path))
 

	
 
        if newpath != oldpath and os.path.exists(newpath):
 
            raise RuntimeError("Report already exists; cannot overwrite: {}".format(newpath))
 

	
 
        if os.path.exists(oldpath):
 
            os.remove(oldpath)
 

	
 
        with open(path, 'rb') as fin:
 
            with open(newpath, 'wb') as fout:
 
                fout.write(fin.read())
 

	
 
        return self.normalize_report_path(newpath)
 

	
 
    def delete_report(self, key, **kwargs):
 
        from poser import reports
 

	
 
        path = os.path.join(os.path.dirname(reports.__file__),
 
                            '{}.py'.format(key))
 
        if os.path.exists(path):
 
            os.remove(path)
rattail/reporting/handlers.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2020 Lance Edgar
 
#  Copyright © 2010-2022 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -46,16 +46,37 @@ class ReportHandler(object):
 

	
 
    def __init__(self, config=None, **kwargs):
 
        self.config = config
 
        self.app = config.get_app()
 
        self.enum = config.get_enum() if config else None
 
        for key, value in kwargs.items():
 
            setattr(self, key, value)
 

	
 
    def get_reports(self):
 
        """
 
        Returns a dict of available reports, which are registered via setuptools
 
        entry points.
 
        Returns a dict of all available reports, keyed by the report
 
        type key.
 

	
 
        By default this includes all reports which are properly
 
        registered via setuptools entry points.  It also will include
 
        any custom Poser reports, although that can be disabled via
 
        config if desired.
 
        """
 
        return load_entry_points(self.entry_point_section)
 
        # properly registered reports
 
        reports = load_entry_points(self.entry_point_section)
 

	
 
        # maybe also include poser reports
 
        if self.config.getbool('rattail.reporting',
 
                               'include_poser_reports',
 
                               default=True):
 

	
 
            # these come back in different format so must normalize
 
            poser_handler = self.app.get_poser_handler()
 
            for report_info in poser_handler.get_all_reports():
 
                if not report_info.get('error'):
 
                    report = report_info['report']
 
                    reports[report.type_key] = report
 

	
 
        return reports
 

	
 
    def get_report(self, key):
 
        """
rattail/templates/poser/reports/base.mako
Show inline comments
 
new file 100644
 
## -*- mode: python; coding: utf-8; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
Poser Report generated by ${app_title}
 
"""
 

	
 
from rattail.reporting import ExcelReport
 

	
 

	
 
% if include_comments:
 
# nb. the "docstring" for the class is used as report description.
 
% endif
 
class ${cap_name}(ExcelReport):
 
    """
 
    ${description}
 
    """
 

	
 
    name = "${name}"
 

	
 
    % if include_comments:
 
    # nb. you can change this if needed, but it should remain unique.
 
    # at the very least never remove the 'poser_' prefix.
 
    % endif
 
    type_key = 'poser_${key}'
 

	
 
    % if include_comments:
 
    # nb. only these columns will be included in excel output, and in
 
    # this particular order.
 
    % endif
 
    output_fields = [
 
        % for field in output_fields:
 
        '${field}',
 
        % endfor
 
    ]
 

	
 
    % if include_comments:
 
    # nb. this method must return all primary data for the report.
 
    # args to this method include a current ${app_title} DB session,
 
    # and dict of report params.  return value must be a list of
 
    # dicts, each of which represents a row in the excel output.
 
    % endif
 
    def make_data(self, session, params, progress=None, **kwargs):
 
${self.render_make_data_body()}
 

	
 
<%def name="render_make_data_body()">\
 

	
 
        # TODO: gather some report data...
 

	
 
        return []
 
</%def>
rattail/templates/poser/reports/customer_py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
<%inherit file="/reports/base.mako" />
 

	
 
<%def name="render_make_data_body()">\
 
        model = self.model
 

	
 
        <%text>##############################</%text>
 
        # example 1
 

	
 
        # looking for all customers
 
        customers = session.query(model.Customer)<%text>\</%text>
 
                           .order_by(model.Customer.name)
 

	
 
        <%text>##############################</%text>
 
        # example 2
 

	
 
        # # all customers, joined with personal contact info
 
        # from sqlalchemy import orm
 
        # customers = session.query(model.Customer)<%text>\</%text>
 
        #                    .order_by(model.Customer.number)<%text>\</%text>
 
        #                    .options(orm.joinedload(model.Customer._people)<%text>\</%text>
 
        #                                .joinedload(model.CustomerPerson.person)<%text>\</%text>
 
        #                                .joinedload(model.Person.emails))
 

	
 
        # final return object will be a list of dicts
 
        results = []
 

	
 
        % if include_comments:
 
        # nb. the next include() function + progress_loop() is really
 
        # just a glorified "for each" loop, but allows the web app to
 
        # show progress
 
        % endif
 

	
 
        def include(customer, i):
 

	
 
            % if include_comments:
 
            # nb. must assign values for all of the report output
 
            # columns; extra non-output values will be ignored
 
            % endif
 
            result = {
 
                'customer_number': customer.number,
 
                'customer_name': customer.name,
 
            }
 

	
 
            # # example 2
 
            # person = customer.first_person()
 
            # result.update{
 
            #     'first_name': person.first_name if person else None,
 
            #     'last_name': person.first_name if person else None,
 
            #     'email_address': person.first_email_address() if person else None,
 
            # })
 

	
 
            results.append(result)
 

	
 
        self.progress_loop(include, customers, progress,
 
                           message="Fetching data for report")
 
        return results
 
</%def>
 

	
 
${parent.body()}
rattail/templates/poser/reports/customer_sql.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
<%inherit file="/reports/base.mako" />
 

	
 
<%def name="render_make_data_body()">\
 

	
 
        % if include_comments:
 
        # nb. your SQL query can return more columns than is needed
 
        # for output; any extras will be ignored.
 
        % endif
 

	
 
        <%text>##############################</%text>
 
        # example 1
 

	
 
        # looking for all customers
 
        sql = """
 
        select
 
                c.uuid as customer_uuid,
 
                c.number as customer_number,
 
                c.name as customer_name
 
        from
 
                customer c
 
        order by
 
                c.name
 
        """
 

	
 
        <%text>##############################</%text>
 
        # example 2
 

	
 
        # # all customers, joined with personal contact info
 
        # sql = """
 
        # select
 
        #         c.uuid as customer_uuid,
 
        #         c.number as customer_number,
 
        #         p.uuid as person_uuid,
 
        #         p.first_name,
 
        #         p.last_name,
 
        #         e.address as email_address
 
        # from
 
        #         customer c
 
        #         left outer join customer_x_person cp on cp.customer_uuid = c.uuid and cp.ordinal = 1
 
        #         left outer join person p on p.uuid = cp.person_uuid
 
        #         left outer join email e on e.parent_type = 'Person' and e.parent_uuid = p.uuid and e.ordinal = 1
 
        # order by
 
        #         c.name
 
        # """
 

	
 
        % if include_comments:
 
        # no need to process results, just return "as-is"...
 
        # nb. this approach only works if your SQL return column names
 
        # match/contain *at least* all of your report output columns.
 
        # but sequence of the result columns does not matter.
 
        % endif
 
        result = session.execute(sql)
 
        return list(result)
 
</%def>
 

	
 
${parent.body()}
rattail/templates/poser/reports/product_py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
<%inherit file="/reports/base.mako" />
 

	
 
<%def name="render_make_data_body()">\
 
        model = self.model
 

	
 
        <%text>##############################</%text>
 
        # example 1
 

	
 
        # looking for all products
 
        products = session.query(model.Product)<%text>\</%text>
 
                          .order_by(model.Product.description)
 

	
 
        <%text>##############################</%text>
 
        # example 2
 

	
 
        # # all products, joined with brand and reg. price
 
        # from sqlalchemy import orm
 
        # products = session.query(model.Product)<%text>\</%text>
 
        #                   .outerjoin(model.Brand)\
 
        #                   .order_by(model.Brand.name,
 
        #                             model.Product.description)<%text>\</%text>
 
        #                   .options(orm.joinedload(model.Product.brand))<%text>\</%text>
 
        #                   .options(orm.joinedload(model.Product.regular_price))
 

	
 
        # final return object will be a list of dicts
 
        results = []
 

	
 
        % if include_comments:
 
        # nb. the next include() function + progress_loop() is really
 
        # just a glorified "for each" loop, but allows the web app to
 
        # show progress
 
        % endif
 

	
 
        def include(product, i):
 

	
 
            % if include_comments:
 
            # nb. must assign values for all of the report output
 
            # columns; extra non-output values will be ignored
 
            % endif
 
            result = {
 
                'product_upc': str(product.upc or ''),
 
                'product_description': product.description,
 
            }
 

	
 
            # # example 2
 
            # brand = product.brand
 
            # regprice = product.regular_price
 
            # result.update{
 
            #     'brand_name': brand.name if brand else None,
 
            #     'regular_price': regprice.price if regprice else None,
 
            # })
 

	
 
            results.append(result)
 

	
 
        self.progress_loop(include, products, progress,
 
                           message="Fetching data for report")
 
        return results
 
</%def>
 

	
 
${parent.body()}
rattail/templates/poser/reports/product_sql.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
<%inherit file="/reports/base.mako" />
 

	
 
<%def name="render_make_data_body()">\
 

	
 
        % if include_comments:
 
        # nb. your SQL query can return more columns than is needed
 
        # for output; any extras will be ignored.
 
        % endif
 

	
 
        <%text>##############################</%text>
 
        # example 1
 

	
 
        # looking for all products
 
        sql = """
 
        select
 
                p.uuid as product_uuid,
 
                p.upc as product_upc,
 
                p.description as product_description
 
        from
 
                product p
 
        order by
 
                p.description
 
        """
 

	
 
        <%text>##############################</%text>
 
        # example 2
 

	
 
        # # all products, joined with brand and reg. price
 
        # sql = """
 
        # select
 
        #         p.uuid as product_uuid,
 
        #         p.upc as product_upc,
 
        #         p.description as product_description,
 
        #         b.name as brand_name,
 
        #         rp.price as regular_price
 
        # from
 
        #         product p
 
        #         left outer join brand b on b.uuid = p.brand_uuid
 
        #         left outer join product_price rp on rp.uuid = p.regular_price_uuid
 
        # order by
 
        #         p.description
 
        # """
 

	
 
        % if include_comments:
 
        # no need to process results, just return "as-is"...
 
        # nb. this approach only works if your SQL return column names
 
        # match/contain *at least* all of your report output columns.
 
        # but sequence of the result columns does not matter.
 
        % endif
 
        result = session.execute(sql)
 
        return list(result)
 
</%def>
 

	
 
${parent.body()}
rattail/util.py
Show inline comments
 
@@ -159,6 +159,26 @@ def import_module_path(module_path):
 
    return last_module(module, module_path)
 

	
 

	
 
def import_reload(module):
 
    """
 
    Reload a module.
 

	
 
    :param module: An already-loaded module.
 

	
 
    :returns: The module.
 
    """
 
    if six.PY3:
 
        try:
 
            import importlib
 
        except ImportError:
 
            pass
 
        else:
 
            return importlib.reload(module)
 

	
 
    # fingers crossed we're on py2
 
    return reload(module)
 

	
 

	
 
def get_object_spec(obj):
 
    """
 
    Returns the "object spec" string for the given object.
0 comments (0 inline, 0 general)