Changeset - 57231236d998
[Not reviewed]
! ! !
Lance Edgar (lance) - 18 months ago 2023-05-05 00:13:44
lance@edbob.org
Massive overhaul of "generate project" feature

previous incarnation was woefully lacking. new feature is much more
extensible. still need to remove old POS integration specifics in
some places.

and a couple of unrelated things that snuck in..

- deprecate `rattail.util.OrderedDict`
- deprecate `rattail.util.import_module_path()`
- deprecate `rattail.util.import_reload()`
105 files changed with 2256 insertions and 1893 deletions:
0 comments (0 inline, 0 general)
docs/api/index.rst
Show inline comments
 
@@ -61,6 +61,7 @@ attributes and method signatures etc.
 
   rattail/people
 
   rattail/problems/index
 
   rattail/products
 
   rattail/projects/index
 
   rattail/tailbone
 
   rattail/time
 
   rattail/trainwreck/index
docs/api/rattail/projects/base.rst
Show inline comments
 
new file 100644
 

	
 
``rattail.projects.base``
 
=========================
 

	
 
.. automodule:: rattail.projects.base
 
   :members:
docs/api/rattail/projects/handler.rst
Show inline comments
 
new file 100644
 

	
 
``rattail.projects.handler``
 
============================
 

	
 
.. automodule:: rattail.projects.handler
 
   :members:
docs/api/rattail/projects/index.rst
Show inline comments
 
new file 100644
 

	
 
``rattail.projects``
 
====================
 

	
 
.. automodule:: rattail.projects
 
   :members:
 

	
 
.. toctree::
 
   :maxdepth: 1
 

	
 
   base
 
   handler
 
   rattail
docs/api/rattail/projects/rattail.rst
Show inline comments
 
new file 100644
 

	
 
``rattail.projects.rattail``
 
============================
 

	
 
.. automodule:: rattail.projects.rattail
 
   :members:
rattail/app.py
Show inline comments
 
@@ -34,12 +34,13 @@ import socket
 
import tempfile
 
import warnings
 
import logging
 
from collections import OrderedDict
 

	
 
import humanize
 
from mako.template import Template
 

	
 
from rattail.util import (load_object, load_entry_points,
 
                          OrderedDict, progress_loop,
 
                          progress_loop,
 
                          pretty_quantity, pretty_hours,
 
                          NOTSET)
 
from rattail.files import temp_path, resource_path
 
@@ -1016,6 +1017,20 @@ class AppHandler(object):
 
                self.config, **kwargs)
 
        return self.problem_report_handler
 

	
 
    def get_project_handler(self, **kwargs):
 
        """
 
        Get the configured "project" handler.
 

	
 
        :returns: The :class:`~rattail.projects.handler.ProjectHandler`
 
           instance for the app.
 
        """
 
        if not hasattr(self, 'project_handler'):
 
            spec = self.config.get('project', 'handler',
 
                                   default='rattail.projects.handler:ProjectHandler')
 
            Handler = self.load_object(spec)
 
            self.project_handler = Handler(self.config)
 
        return self.project_handler
 

	
 
    def get_tailbone_handler(self, **kwargs):
 
        """
 
        Get the configured "tailbone" handler.
rattail/batch/custorder.py
Show inline comments
 
@@ -26,17 +26,14 @@ Customer Order Batch Handler
 
Please note this is different from the Customer Order Handler.
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import, division
 

	
 
import re
 
import decimal
 
from collections import OrderedDict
 

	
 
import six
 
import sqlalchemy as sa
 

	
 
from rattail.db import model
 
from rattail.batch import BatchHandler
 
from rattail.util import OrderedDict
 
from rattail.time import localtime
 

	
 

	
 
@@ -238,11 +235,11 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        """
 
        contact = self.get_contact(batch)
 
        if contact:
 
            return six.text_type(contact)
 
            return str(contact)
 

	
 
        pending = batch.pending_customer
 
        if pending:
 
            return six.text_type(pending)
 
            return str(pending)
 

	
 
    def get_contact_phones(self, batch):
 
        """
 
@@ -515,7 +512,7 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        vendor = product.cost.vendor if product.cost else None
 
        info = {
 
            'uuid': product.uuid,
 
            'upc': six.text_type(product.upc),
 
            'upc': str(product.upc),
 
            'item_id': product.item_id,
 
            'scancode': product.scancode,
 
            'upc_pretty': product.upc.pretty(),
 
@@ -547,7 +544,7 @@ class CustomerOrderBatchHandler(BatchHandler):
 
                sale_ends = sale_price.ends
 
                if sale_ends:
 
                    sale_ends = localtime(self.config, sale_ends, from_utc=True).date()
 
                    info['sale_ends'] = six.text_type(sale_ends)
 
                    info['sale_ends'] = str(sale_ends)
 
                    info['sale_ends_display'] = self.app.render_date(sale_ends)
 

	
 
        case_price = None
 
@@ -559,7 +556,7 @@ class CustomerOrderBatchHandler(BatchHandler):
 
                unit_price = sale_price.price
 
            case_price = (case_size or 1) * unit_price
 
            case_price = case_price.quantize(decimal.Decimal('0.01'))
 
        info['case_price'] = six.text_type(case_price) if case_price is not None else None
 
        info['case_price'] = str(case_price) if case_price is not None else None
 
        info['case_price_display'] = self.app.render_currency(case_price)
 

	
 
        key = self.config.product_key()
 
@@ -757,7 +754,7 @@ class CustomerOrderBatchHandler(BatchHandler):
 
            row.product_upc = product.upc
 
            row.product_item_id = product.item_id
 
            row.product_scancode = product.scancode
 
            row.product_brand = six.text_type(product.brand or "")
 
            row.product_brand = str(product.brand or "")
 
            row.product_description = product.description
 
            row.product_size = product.size
 
            row.product_weighed = product.weighed
 
@@ -823,7 +820,7 @@ class CustomerOrderBatchHandler(BatchHandler):
 
        row.product_upc = pending.upc
 
        row.product_item_id = pending.item_id
 
        row.product_scancode = pending.scancode
 
        row.product_brand = six.text_type(pending.brand or pending.brand_name or '')
 
        row.product_brand = str(pending.brand or pending.brand_name or '')
 
        row.product_description = pending.description
 
        row.product_size = pending.size
 
        # TODO: is this even important?  pending does not have it
rattail/commands/core.py
Show inline comments
 
@@ -24,6 +24,7 @@
 
Console Commands
 
"""
 

	
 
import importlib
 
import os
 
import sys
 
import json
 
@@ -559,9 +560,8 @@ class CloneDatabase(Subcommand):
 

	
 
    def run(self, args):
 
        from sqlalchemy import create_engine, orm
 
        from rattail.util import import_module_path
 

	
 
        model = import_module_path(args.model)
 
        model = importlib.import_module(args.model)
 
        classes = args.classes
 
        assert classes
 

	
rattail/commands/install.py
Show inline comments
 
@@ -24,17 +24,17 @@
 
Installer Commands
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import stat
 
import subprocess
 
import sys
 
import warnings
 

	
 
from mako.lookup import TemplateLookup
 

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

	
 

	
 
class InstallSubcommand(Subcommand):
 
@@ -53,7 +53,7 @@ class InstallSubcommand(Subcommand):
 

	
 
    def run(self, args):
 

	
 
        self.templates = InstallerTemplateLookup(directories=[
 
        self.templates = ResourceTemplateLookup(directories=[
 
            resource_path('{}:templates/installer'.format(self.app_package)),
 
            resource_path('rattail:templates/installer'),
 
        ])
 
@@ -164,14 +164,14 @@ class InstallSubcommand(Subcommand):
 
                       database=dbname)
 

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

	
 
        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')
 
            inspect(engine).has_table('whatever')
 
        except Exception as error:
 
            return str(error)
 

	
 
@@ -314,40 +314,9 @@ class InstallSubcommand(Subcommand):
 
            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)
 
class InstallerTemplateLookup(ResourceTemplateLookup):
 
    def __init__(self, *args, **kwargs):
 
        warnings.warn("InstallerTemplateLookup is deprecated; "
 
                      "please use ResourceTemplateLookup instead",
 
                      DeprecationWarning, stacklevel=2)
 
        super(InstallerTemplateLookup, self).__init__(*args, **kwargs)
rattail/config.py
Show inline comments
 
@@ -24,6 +24,7 @@
 
Application Configuration
 
"""
 

	
 
import importlib
 
import os
 
import re
 
import sys
 
@@ -34,7 +35,7 @@ import warnings
 
import logging
 
import logging.config
 

	
 
from rattail.util import load_entry_points, import_module_path, load_object
 
from rattail.util import load_entry_points, load_object
 
from rattail.exceptions import WindowsExtensionsNotInstalled, ConfigurationError
 
from rattail.files import temp_path
 
from rattail.logging import TimeConverter
 
@@ -666,7 +667,7 @@ class RattailConfig(object):
 
        """
 
        kwargs.setdefault('usedb', False)
 
        spec = self.get('rattail', 'enum', default='rattail.enum', **kwargs)
 
        return import_module_path(spec)
 
        return importlib.import_module(spec)
 

	
 
    def get_model(self):
 
        """
 
@@ -674,7 +675,7 @@ class RattailConfig(object):
 
        :mod:`rattail.db.model`.
 
        """
 
        spec = self.get('rattail', 'model', default='rattail.db.model', usedb=False)
 
        return import_module_path(spec)
 
        return importlib.import_module(spec)
 

	
 
    def get_trainwreck_model(self):
 
        """
 
@@ -683,7 +684,7 @@ class RattailConfig(object):
 
        be configured.
 
        """
 
        spec = self.require('rattail.trainwreck', 'model', usedb=False)
 
        return import_module_path(spec)
 
        return importlib.import_module(spec)
 

	
 
    def product_key(self, default='upc'):
 
        """
rattail/db/cache.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2021 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -24,16 +24,13 @@
 
Cache Helpers
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import logging
 
from collections import OrderedDict
 

	
 
import six
 
from sqlalchemy.orm import joinedload
 

	
 
from rattail.core import Object
 
from rattail.db import model
 
from rattail.util import OrderedDict
 

	
 

	
 
log = logging.getLogger(__name__)
 
@@ -121,7 +118,7 @@ class ModelCacher(object):
 
    def get_key(self, instance, normalized):
 
        if callable(self.key):
 
            return self.key(instance, normalized)
 
        if isinstance(self.key, six.string_types):
 
        if isinstance(self.key, str):
 
            return getattr(instance, self.key)
 
        return tuple(getattr(instance, k) for k in self.key)
 

	
rattail/db/config.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.
 
#
 
@@ -24,11 +24,10 @@
 
Database Configuration
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import logging
 
from collections import OrderedDict
 

	
 
from rattail.util import load_object, OrderedDict
 
from rattail.util import load_object
 
from rattail.exceptions import SQLAlchemyNotInstalled
 
from rattail.config import parse_list, parse_bool
 

	
rattail/db/continuum.py
Show inline comments
 
@@ -26,14 +26,13 @@ SQLAlchemy-Continuum integration
 

	
 
import socket
 
import logging
 
from collections import OrderedDict
 

	
 
import sqlalchemy as sa
 
import sqlalchemy_continuum as continuum
 
from sqlalchemy_continuum.plugins import Plugin
 
from sqlalchemy_utils.functions import get_primary_keys
 

	
 
from rattail.util import OrderedDict
 

	
 
# TODO: Deprecate/remove this import.
 
from rattail.db.config import configure_versioning
 

	
rattail/enum.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.
 
#
 
@@ -54,9 +54,7 @@ The following enumerations are provided:
 
   from the SIL specification.
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
from rattail.util import OrderedDict
 
from collections import OrderedDict
 

	
 

	
 
BATCH_ACTION_ADD                = 'ADD'
rattail/importing/csv.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2021 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -24,13 +24,11 @@
 
CSV -> Rattail data import
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import csv
 
import datetime
 
from collections import OrderedDict
 

	
 
import six
 
import sqlalchemy as sa
 
from sqlalchemy import orm
 
from sqlalchemy_utils.functions import get_primary_keys
 
@@ -39,7 +37,6 @@ from rattail import importing, csvutil
 
from rattail.importing.handlers import FromFileHandler
 
from rattail.importing.files import FromFile
 
from rattail.db.util import make_topo_sortkey
 
from rattail.util import OrderedDict
 
from rattail.config import parse_bool
 

	
 

	
 
@@ -80,8 +77,6 @@ class FromCSVToSQLAlchemyMixin(object):
 
        fields = list(mapper.columns.keys())
 
        pkeys = get_primary_keys(cls)
 
        name = '{}Importer'.format(name)
 
        if six.PY2:
 
            name = name.encode('utf_8')
 
        return type(name, (FromCSV, self.ToParent), {
 
            'model_class': cls,
 
            'supported_fields': fields,
 
@@ -213,12 +208,8 @@ class FromCSV(FromFile):
 
        return '{}.csv'.format(self.model_name)
 

	
 
    def open_input_file(self):
 
        if six.PY2:
 
            self.input_file = open(self.input_file_path, 'rb')
 
            self.input_reader = csvutil.UnicodeDictReader(self.input_file, encoding=self.csv_encoding)
 
        else: # PY3
 
            self.input_file = open(self.input_file_path, 'rt', encoding=self.csv_encoding)
 
            self.input_reader = csv.DictReader(self.input_file)
 
        self.input_file = open(self.input_file_path, 'rt', encoding=self.csv_encoding)
 
        self.input_reader = csv.DictReader(self.input_file)
 

	
 
    def close_input_file(self):
 
        self.input_file.close()
rattail/importing/exporters.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.
 
#
 
@@ -24,17 +24,13 @@
 
Rattail Data Exporters
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import csv
 
from collections import OrderedDict
 

	
 
import six
 
from sqlalchemy import orm
 
from sqlalchemy_utils.functions import get_primary_keys
 

	
 
from rattail import csvutil
 
from rattail.util import OrderedDict
 
from rattail.db.util import make_topo_sortkey
 
from rattail.importing import Importer
 
from rattail.importing.rattail import FromRattailHandler, FromRattail
 
@@ -81,8 +77,6 @@ class FromSQLAlchemyToCSVMixin(object):
 
        fields = list(mapper.columns.keys())
 
        pkeys = get_primary_keys(cls)
 
        name = '{}Importer'.format(name)
 
        if six.PY2:
 
            name = name.encode('utf_8')
 
        return type(name, (self.FromParent, ToCSV), {
 
            'model_class': cls,
 
            'supported_fields': fields,
 
@@ -173,12 +167,8 @@ class ToCSV(ToFile):
 
    empty_local_data = True
 

	
 
    def open_output_file(self):
 
        if six.PY2:
 
            self.output_file = open(self.output_file_path, 'wb')
 
            self.output_writer = csvutil.UnicodeDictWriter(self.output_file, self.fields, encoding='utf_8')
 
        else: # PY3
 
            self.output_file = open(self.output_file_path, 'wt', encoding='utf_8')
 
            self.output_writer = csv.DictWriter(self.output_file, self.fields)
 
        self.output_file = open(self.output_file_path, 'wt', encoding='utf_8')
 
        self.output_writer = csv.DictWriter(self.output_file, self.fields)
 
        self.write_output_header()
 

	
 
    def write_output_header(self):
 
@@ -208,7 +198,7 @@ class ToCSV(ToFile):
 
                value = ''
 

	
 
            else:
 
                value = six.text_type(value)
 
                value = str(value)
 

	
 
            coerced[field] = value
 
        return coerced
rattail/importing/handlers.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.
 
#
 
@@ -24,21 +24,19 @@
 
Import Handlers
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import sys
 
import shutil
 
import tempfile
 
import logging
 
from collections import OrderedDict
 

	
 
import six
 
import humanize
 
import sqlalchemy as sa
 

	
 
from rattail.core import get_uuid
 
from rattail.time import make_utc
 
from rattail.util import OrderedDict, get_object_spec, progress_loop
 
from rattail.util import get_object_spec, progress_loop
 
from rattail.mail import send_email
 

	
 

	
 
@@ -319,7 +317,7 @@ class ImportHandler(object):
 
        def delete(key, i):
 
            cached = importer.cached_local_data.pop(key)
 
            local_data = cached['data']
 
            local_data['_object_str'] = six.text_type(cached['object'])
 
            local_data['_object_str'] = str(cached['object'])
 
            sequence = batch.rowcount + 1
 
            self.make_batch_row(session, importer, row_table, sequence, None, local_data,
 
                                status_code=self.enum.IMPORTER_BATCH_ROW_STATUS_DELETE)
 
@@ -376,13 +374,13 @@ class ImportHandler(object):
 
            if '_object_str' in host_data:
 
                values['object_str'] = host_data['_object_str']
 
            elif '_host_object' in host_data:
 
                values['object_str'] = six.text_type(host_data['_host_object'])
 
            values['object_key'] = ','.join([six.text_type(host_data[f]) for f in importer.key])
 
                values['object_str'] = str(host_data['_host_object'])
 
            values['object_key'] = ','.join([str(host_data[f]) for f in importer.key])
 
        elif local_data:
 
            if '_object_str' in local_data:
 
                values['object_str'] = local_data['_object_str']
 
            elif '_object' in local_data:
 
                values['object_str'] = six.text_type(local_data['_object'])
 
                values['object_str'] = str(local_data['_object'])
 
            values['object_key'] = ','.join([local_data[f] for f in importer.key])
 

	
 
        for field in importer.fields:
 
@@ -769,7 +767,7 @@ class RecordRenderer(object):
 
        return label(record)
 

	
 
    def label(self, record):
 
        return six.text_type(record)
 
        return str(record)
 

	
 
    def get_url(self, record):
 
        """
rattail/importing/ifps.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2021 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -24,17 +24,15 @@
 
IFPS -> Rattail data import
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import datetime
 
import logging
 
from collections import OrderedDict
 

	
 
import sqlalchemy as sa
 

	
 
from rattail import importing
 
from rattail.importing.handlers import FromFileHandler
 
from rattail.importing.files import FromExcelFile
 
from rattail.util import OrderedDict
 
from rattail.time import localtime, make_utc
 
from rattail.db.util import maxlen
 

	
rattail/importing/importers.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.
 
#
 
@@ -24,17 +24,14 @@
 
Data Importers
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import datetime
 
import logging
 

	
 
import six
 
from collections import OrderedDict
 

	
 
from rattail.db import cache
 
from rattail.db.util import QuerySequence
 
from rattail.time import make_utc
 
from rattail.util import data_diffs, progress_loop, OrderedDict
 
from rattail.util import data_diffs, progress_loop
 
from rattail.csvutil import UnicodeDictReader
 

	
 

	
 
@@ -140,7 +137,7 @@ class Importer(object):
 
            self.exclude_fields(*exclude_fields)
 
        self.fuzzy_fields = fuzzy_fields or []
 
        self.fuzz_factor = fuzz_factor
 
        if isinstance(self.key, six.string_types):
 
        if isinstance(self.key, str):
 
            self.key = (self.key,)
 
        if self.key:
 
            for field in self.key:
 
@@ -783,13 +780,8 @@ class FromCSV(Importer):
 
                self.source_data_path = self.args.source_csv
 

	
 
    def get_host_objects(self):
 
        if six.PY3:
 
            source_csv_file = open(self.source_data_path, 'rt', encoding='latin_1')
 
            reader = UnicodeDictReader(source_csv_file)
 
        else:
 
            source_csv_file = open(self.source_data_path, 'rb')
 
            reader = UnicodeDictReader(source_csv_file, 'latin_1')
 

	
 
        source_csv_file = open(self.source_data_path, 'rt', encoding='latin_1')
 
        reader = UnicodeDictReader(source_csv_file)
 
        objects = list(reader)
 
        source_csv_file.close()
 
        return objects
rattail/importing/rattail.py
Show inline comments
 
@@ -25,6 +25,7 @@ Rattail -> Rattail data import
 
"""
 

	
 
import logging
 
from collections import OrderedDict
 

	
 
import sqlalchemy as sa
 

	
 
@@ -32,7 +33,6 @@ from rattail.db import Session
 
from rattail.importing import model
 
from rattail.importing.handlers import FromSQLAlchemyHandler, ToSQLAlchemyHandler
 
from rattail.importing.sqlalchemy import FromSQLAlchemySameToSame
 
from rattail.util import OrderedDict
 

	
 

	
 
log = logging.getLogger(__name__)
rattail/importing/rattail_bulk.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2019 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -24,10 +24,9 @@
 
Rattail -> Rattail bulk data import
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 
from collections import OrderedDict
 

	
 
from rattail import importing
 
from rattail.util import OrderedDict
 
from rattail.importing.rattail import FromRattailToRattailImport, FromRattail
 

	
 

	
rattail/importing/sample.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.
 
#
 
@@ -24,19 +24,14 @@
 
Sample -> Rattail data import
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import csv
 
import os
 
import datetime
 
import decimal
 
import logging
 

	
 
import six
 
from collections import OrderedDict
 

	
 
from rattail import importing
 
from rattail.util import OrderedDict
 
from rattail.csvutil import UnicodeDictReader
 
from rattail.files import resource_path
 
from rattail.config import parse_bool
 

	
 
@@ -97,11 +92,6 @@ class FromSample(importing.Importer):
 
        """
 
        Return all data rows from CSV file, as returned by CSV parser.
 
        """
 
        if six.PY2:
 
            with open(self.data_path, 'rb') as csv_file:
 
                reader = UnicodeDictReader(csv_file)
 
                return list(reader)
 

	
 
        with open(self.data_path, 'rt') as csv_file:
 
            reader = csv.DictReader(csv_file)
 
            return list(reader)
rattail/importing/versions.py
Show inline comments
 
@@ -2,7 +2,7 @@
 
################################################################################
 
#
 
#  Rattail -- Retail Software Framework
 
#  Copyright © 2010-2021 Lance Edgar
 
#  Copyright © 2010-2023 Lance Edgar
 
#
 
#  This file is part of Rattail.
 
#
 
@@ -24,7 +24,7 @@
 
Rattail -> Rattail "versions" data import
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 
from collections import OrderedDict
 

	
 
import sqlalchemy_continuum as continuum
 

	
 
@@ -32,7 +32,6 @@ from rattail.db import model
 
from rattail.db.continuum import versioning_manager
 
from rattail.importing.rattail import FromRattailHandler, ToRattailHandler, FromRattail
 
from rattail.importing.model import ToRattail
 
from rattail.util import OrderedDict
 

	
 

	
 
class FromRattailToRattailVersions(FromRattailHandler, ToRattailHandler):
rattail/labels.py
Show inline comments
 
@@ -29,13 +29,13 @@ import os
 
import os.path
 
import socket
 
import shutil
 
from collections import OrderedDict
 

	
 
from rattail.app import GenericHandler
 
from rattail.core import Object
 
from rattail.files import temp_path
 
from rattail.exceptions import LabelPrintingError
 
from rattail.time import localtime
 
from rattail.util import OrderedDict
 

	
 

	
 
class LabelHandler(GenericHandler):
rattail/mail.py
Show inline comments
 
@@ -24,6 +24,7 @@
 
Email Framework
 
"""
 

	
 
import importlib
 
import os
 
import smtplib
 
import logging
 
@@ -43,7 +44,7 @@ from mako.exceptions import TopLevelLookupException
 
from rattail import exceptions
 
from rattail.core import UNSPECIFIED
 
from rattail.files import resource_path
 
from rattail.util import import_module_path, load_entry_points
 
from rattail.util import load_entry_points
 
from rattail.time import localtime, make_utc
 

	
 

	
 
@@ -143,7 +144,7 @@ class EmailHandler(object):
 

	
 
        # figure out which modules we should actually use
 
        if app_modules:
 
            modules = [import_module_path(m)
 
            modules = [importlib.import_module(m)
 
                       for m in app_modules]
 
        else:
 
            modules = all_modules
 
@@ -188,7 +189,7 @@ class EmailHandler(object):
 
                          DeprecationWarning, stacklevel=2)
 

	
 
        for module in self.config.getlist('rattail.mail', 'emails', default=['rattail.emails']):
 
            module = import_module_path(module)
 
            module = importlib.import_module(module)
 
            for email in self.get_emails_from_module(module):
 
                yield email
 

	
rattail/mako.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/>.
 
#
 
################################################################################
 
"""
 
Mako utility logic
 
"""
 

	
 
import os
 

	
 
from mako.lookup import TemplateLookup
 

	
 
from rattail.files import resource_path
 

	
 

	
 
class ResourceTemplateLookup(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(ResourceTemplateLookup, 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(ResourceTemplateLookup, self).get_template(uri)
rattail/poser/handler.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.
 
#
 
@@ -24,19 +24,18 @@
 
Poser Handler
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import importlib
 
import os
 
import sys
 
import subprocess
 
import logging
 
from collections import OrderedDict
 

	
 
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.util import simple_error
 
from rattail.reporting import ExcelReport
 
from rattail.config import ConfigExtension
 

	
 
@@ -249,8 +248,8 @@ class PoserHandler(GenericHandler):
 
        modpath = '.'.join((reports.__name__, key))
 

	
 
        try:
 
            mod = import_module_path(modpath)
 
            import_reload(mod)
 
            mod = importlib.import_module(modpath)
 
            importlib.reload(mod)
 

	
 
        except Exception as error:
 
            log.warning("import failed for %s", modpath, exc_info=True)
 
@@ -412,8 +411,8 @@ class PoserHandler(GenericHandler):
 
        modpath = '.'.join((views.__name__, key))
 

	
 
        try:
 
            mod = import_module_path(modpath)
 
            import_reload(mod)
 
            mod = importlib.import_module(modpath)
 
            importlib.reload(mod)
 

	
 
        except Exception as error:
 
            log.warning("import failed for %s", modpath, exc_info=True)
rattail/problems/handlers.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.
 
#
 
@@ -24,15 +24,14 @@
 
Problem Report Handlers
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import calendar
 
import importlib
 
import logging
 

	
 
from rattail.db import Session
 
from rattail.mail import send_email
 
from rattail.time import localtime
 
from rattail.util import load_object, import_module_path, progress_loop
 
from rattail.util import load_object, progress_loop
 
from rattail.problems import ProblemReport, RattailProblemReport
 

	
 

	
 
@@ -64,7 +63,7 @@ class ProblemReportHandler(object):
 
        problem_modules = self.config.getlist('rattail', 'problems',
 
                                              default=['rattail.problems.rattail'])
 
        for module_path in problem_modules:
 
            module = import_module_path(module_path)
 
            module = importlib.import_module(module_path)
 
            for name in dir(module):
 
                obj = getattr(module, name)
 

	
rattail/projects/__init__.py
Show inline comments
 
# -*- 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/>.
 
#
 
################################################################################
 
"""
 
Project Generators
 
"""
 

	
 
from .base import ProjectGenerator, PythonProjectGenerator, PoserProjectGenerator
rattail/projects/base.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/>.
 
#
 
################################################################################
 
"""
 
Project Generators
 
"""
 

	
 
import os
 
import random
 
import re
 
import shutil
 
import string
 
import sys
 
import unicodedata
 

	
 
import colander
 
from alembic.config import Config as AlembicConfig
 
from alembic.command import revision as alembic_revision
 
from mako.template import Template
 

	
 
from rattail.util import get_class_hierarchy
 
from rattail.mako import ResourceTemplateLookup
 

	
 

	
 
class ProjectGenerator(object):
 
    """
 
    Base class for project generators.
 

	
 
    .. attribute:: key
 

	
 
       Unique key for the project type.
 
    """
 

	
 
    def __init__(self, config, **kwargs):
 
        self.config = config
 
        self.app = self.config.get_app()
 

	
 
        self.template_lookup = ResourceTemplateLookup()
 

	
 
    @property
 
    def key(self):
 
        raise NotImplementedError("Must define {}.key".format(
 
            self.__class__.__name__))
 

	
 
    @classmethod
 
    def get_templates_path(cls):
 
        """
 
        Return the path to templates folder for this generator.
 

	
 
        This is a class method for sake of inheritance, so templates
 
        can more easily be shared by generator subclasses.
 
        """
 
        basedir = os.path.dirname(sys.modules[cls.__module__].__file__)
 
        return os.path.join(basedir, cls.key)
 

	
 
    def make_schema(self, **kwargs):
 
        return colander.Schema()
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        """
 
        Generate a new project to the given output folder, with the
 
        given context data.
 

	
 
        :param output: Path to output folder.  This path should
 
           already exist.
 

	
 
        :param context: Dictionary of template context data.
 
        """
 
        raise NotImplementedError("Must define {}.generate_project()".format(
 
            self.__class__.__name__))
 

	
 
    def get_template_path(self, template):
 
        """
 
        Return the full path to a template file.
 

	
 
        :param template: Filename of the template, e.g. ``'setup.py'``.
 
        """
 
        for cls in get_class_hierarchy(self.__class__, topfirst=False):
 
            templates = cls.get_templates_path()
 
            path = os.path.join(templates, template)
 
            if os.path.exists(path):
 
                return path
 

	
 
        raise RuntimeError("template not found: {}".format(template))
 

	
 
    def normalize_context(self, context):
 
        return context
 

	
 
    def generate(self, template, output, context=None, **kwargs):
 
        """
 
        Generate a file from the given template, and save the result
 
        to the given output path.
 

	
 
        This will do one of 3 things based on the specified template:
 

	
 
        * if filename ends with ``.mako`` then call
 
          :meth:`generate_mako()`
 
        * if filename ends with ``.mako_tmpl`` then call
 
          :meth:`generate_mako_tmpl()`
 
        * otherwise copy file as-is (do not "generate" output)
 

	
 
        :param template: Path to template file.
 

	
 
        :param output: Path to output file.
 

	
 
        :param context: Data dictionary with template context.
 
        """
 
        template = self.get_template_path(template)
 
        context = context or {}
 

	
 
        # maybe run it through our simplistic, hand-rolled template
 
        # engine (note, this is only for the sake of *avoiding* mako
 
        # logic, when generating "actual" mako templates, so we avoid
 
        # a mako-within-mako situation.)
 
        if template.endswith('.mako_tmpl'):
 
            return self.generate_mako_tmpl(template, output, context)
 

	
 
        # maybe run it through Mako template engine
 
        if template.endswith('.mako'):
 
            return self.generate_mako(template, output, context)
 

	
 
        # or, just copy the file as-is
 
        shutil.copyfile(template, output)
 

	
 
    def generate_mako(self, template, output, context):
 
        """
 
        Generate output from a Mako template.
 
        """
 
        template = Template(filename=template,
 
                            lookup=self.template_lookup)
 
        text = template.render(**context)
 
        with open(output, 'wt') as f:
 
            f.write(text)
 

	
 
    def generate_mako_tmpl(self, template, output, context):
 
        """
 
        Generate output (which is itself a Mako template) from a
 
        "simple" original template.
 

	
 
        Sometimes you want the final output to be a Mako template, but
 
        your original template also must be dynamic, based on context.
 
        It's problematic (confusing at the very least) for an original
 
        Mako template to produce output which is also a Mako template.
 
        So instead..
 

	
 
        If you give your original template file a ``.mako_tmpl``
 
        extension, then the output will still be dynamic, but instead
 
        of running the original template through the Mako engine, we
 
        leverage Python strings' printf-style formatting.
 

	
 
        A small example template might be ``rattail.conf.mako_tmpl``:
 

	
 
        .. code-block:: ini
 

	
 
           <%%text>##############################</%%text>
 
           # example config
 
           <%%text>##############################</%%text>
 

	
 
           [rattail]
 
           app_title = ${app_title}
 

	
 
           [alembic]
 
           script_location = rattail.db:alembic
 
           version_locations = %(alembic_version_locations)s
 

	
 
           # -- LOGGING SECTION --
 

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

	
 
        Note the Mako syntax which is *ignored* (passed through as-is)
 
        by this method when generating output.
 

	
 
        Note also this template expects ``alembic_version_locations``
 
        to be provided via the context.
 

	
 
        Finally also note the doubled-up ``%`` chars, both in
 
        ``<%%text>`` as well as in the logging section.  Since
 
        printf-style formatting uses the ``%`` char, we must escape
 
        those by doubling-up; the formatter will convert each back to
 
        single chars for the output.
 

	
 
        For more info on the formatting specifics see
 
        :ref:`python:old-string-formatting`.
 

	
 
        So with context like::
 

	
 
           {'alembic_version_locations': 'rattail.db:alembic/versions'}
 

	
 
        The above example would produce output ``rattail.conf.mako``:
 

	
 
        .. code-block:: ini
 

	
 
           <%text>##############################</%text>
 
           # example config
 
           <%text>##############################</%text>
 

	
 
           [rattail]
 
           app_title = ${app_title}
 

	
 
           [alembic]
 
           script_location = rattail.db:alembic
 
           version_locations = rattail.db:alembic/versions
 

	
 
           # -- LOGGING SECTION --
 

	
 
           [formatter_generic]
 
           format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(funcName)s: %(message)s
 
           datefmt = %Y-%m-%d %H:%M:%S
 
        """
 
        with open(template, 'rt') as f:
 
            template_lines = f.readlines()
 

	
 
        output_lines = []
 
        for line in template_lines:
 
            line = line.rstrip('\n')
 
            line = line % context
 
            output_lines.append(line)
 

	
 
        with open(output, 'wt') as f:
 
            f.write('\n'.join(output_lines))
 

	
 
    def random_string(self, size=20, chars=string.ascii_letters + string.digits):
 
        # per https://stackoverflow.com/a/2257449
 
        return ''.join(random.SystemRandom().choice(chars) for _ in range(size))
 

	
 

	
 
class PythonProjectGenerator(ProjectGenerator):
 
    """
 
    Base class for Python project generators.
 

	
 
    All projects generated are assumed to have the following context:
 

	
 
    * ``name`` - human-friendly name for the project, e.g. ``"Poser Plus"``
 
    * ``description`` - brief (one-line) description of the project
 
    * ``folder`` - folder name for the project, e.g. ``"poser-plus"``
 
    * ``pkg_name`` - package name for use in Python, e.g. ``"poser_plus"``
 
    * ``pypi_name`` - package name for use with PyPI, e.g. ``"Poser-Plus"``
 
    * ``egg_name`` - package name used with egg files, e.g. ``"Poser_Plus"``
 
    * ``studly_prefix`` - prefix for class names, e.g. ``PoserPlus``
 
    * ``env_name`` - name of the Python virtual environment
 
    * ``requires`` - dict of required dependencies
 
    * ``classifiers`` - set of Python trove classifiers for project
 
    * ``entry_points`` - dict of setuptools entry points for project
 
    """
 
    # nb. subclass must override this!
 
    key = 'python'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(PythonProjectGenerator, self).make_schema(**kwargs)
 

	
 
        schema.add(colander.SchemaNode(name='name',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='pkg_name',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='pypi_name',
 
                                       typ=colander.String()))
 

	
 
        return schema
 

	
 
    def get_studly_prefix(self, name):
 
        # cf. https://stackoverflow.com/a/3194567
 
        name = unicodedata.normalize('NFD', name)
 
        name = name.encode('ascii', 'ignore').decode('ascii')
 
        words = string.capwords(name)
 
        return ''.join(words)
 

	
 
    def normalize_context(self, context):
 
        context = super(PythonProjectGenerator, self).normalize_context(context)
 

	
 
        if 'description' not in context:
 
            context['description'] = ""
 

	
 
        if 'folder' not in context:
 
            context['folder'] = context['pkg_name'].replace('_', '-')
 

	
 
        if 'egg_name' not in context:
 
            context['egg_name'] = context['pypi_name'].replace('-', '_')
 

	
 
        if 'studly_prefix' not in context:
 
            context['studly_prefix'] = self.get_studly_prefix(context['name'])
 

	
 
        if 'env_name' not in context:
 
            context['env_name'] = context['folder']
 

	
 
        if 'requires' not in context:
 
            context['requires'] = {}
 

	
 
        if 'classifiers' not in context:
 
            context['classifiers'] = set([
 
                'Development Status :: 3 - Alpha',
 
                'Intended Audience :: Developers',
 
                'Natural Language :: English',
 
                'Programming Language :: Python',
 
                'Programming Language :: Python :: 3',
 
            ])
 

	
 
        if 'entry_points' not in context:
 
            context['entry_points'] = {}
 

	
 
        return context
 

	
 
    def generate_project(self, output, context, **kwargs):
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('gitignore.mako',
 
                      os.path.join(output, '.gitignore'),
 
                      context)
 

	
 
        self.generate('MANIFEST.in.mako',
 
                      os.path.join(output, 'MANIFEST.in'),
 
                      context)
 

	
 
        self.generate('README.md.mako',
 
                      os.path.join(output, 'README.md'),
 
                      context)
 

	
 
        self.generate('CHANGELOG.md.mako',
 
                      os.path.join(output, 'CHANGELOG.md'),
 
                      context)
 

	
 
        self.generate('setup.py.mako',
 
                      os.path.join(output, 'setup.py'),
 
                      context)
 

	
 
        self.generate('tasks.py.mako',
 
                      os.path.join(output, 'tasks.py'),
 
                      context)
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        package = os.path.join(output, context['pkg_name'])
 
        os.makedirs(package)
 

	
 
        self.generate('package/__init__.py.mako',
 
                      os.path.join(package, '__init__.py'),
 
                      context)
 

	
 
        self.generate('package/_version.py',
 
                      os.path.join(package, '_version.py'))
 

	
 

	
 
class PoserProjectGenerator(PythonProjectGenerator):
 
    """
 
    Base class for Poser project generators.
 

	
 
    In addition to normal context for Python projects, all Poser
 
    projects are assumed to have the following context:
 

	
 
    * ``organization`` - human-friendly name for the organization
 
    * ``extends_config`` - whether the app extends Rattail config
 
    * ``has_cli`` - whether the app has its own command interface
 
    * ``has_db`` - whether the app has a Rattail DB
 
    * ``extends_db`` - whether the app has custom DB schema
 
    * ``has_batch_schema`` - whether the DB needs dynamic 'batch' schema
 
    * ``db_name`` - name of the Rattail DB
 
    * ``has_datasync`` - whether the app needs a datasync service
 
    * ``has_web`` - whether the app has tailbone web UI
 
    * ``has_web_api`` - whether the app has tailbone web API
 
    * ``beaker_session_secret`` - secret for Beaker session storage
 
    * ``uses_fabric`` - whether the app is deployed via fabric
 
    """
 
    # nb. subclass must override this!
 
    key = 'poser'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(PoserProjectGenerator, self).make_schema(**kwargs)
 

	
 
        schema.add(colander.SchemaNode(name='organization',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='extends_config',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_cli',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_db',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='extends_db',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_batch_schema',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_web',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_web_api',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='has_datasync',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='uses_fabric',
 
                                       typ=colander.Boolean()))
 

	
 
        return schema
 

	
 
    def normalize_context(self, context):
 
        context = super(PoserProjectGenerator, self).normalize_context(context)
 

	
 
        pkg_name = context['pkg_name']
 

	
 
        if not context.get('description'):
 
            context['description'] = "Rattail/Poser project for {}".format(
 
                context['organization'])
 

	
 
        context['classifiers'].update(set([
 
            'Environment :: Console',
 
            'Operating System :: POSIX :: Linux',
 
            'Topic :: Office/Business',
 
        ]))
 
        if context['has_web'] or context['has_web_api']:
 
            context['classifiers'].update(set([
 
                'Environment :: Web Environment',
 
                'Framework :: Pyramid',
 
            ]))
 

	
 
        # nb. auto-set some flags as needed
 
        if context['extends_db']:
 
            context['extends_config'] = True
 

	
 
        if context['extends_config']:
 
            context['entry_points'].setdefault('rattail.config.extensions', []).extend([
 
                "{0} = {0}.config:{1}Config".format(pkg_name, context['studly_prefix']),
 
            ])
 

	
 
        if context['has_cli']:
 
            context['entry_points'].setdefault('console_scripts', []).extend([
 
                "{0} = {0}.commands:main".format(pkg_name),
 
            ])
 
            context['entry_points'].setdefault('{}.commands'.format(pkg_name), []).extend([
 
                "hello = {}.commands:HelloWorld".format(pkg_name),
 
                "install = {}.commands:Install".format(pkg_name),
 
            ])
 

	
 
        if context['has_web']:
 
            context['entry_points'].setdefault('paste.app_factory', []).extend([
 
                "main = {}.web.app:main".format(pkg_name),
 
            ])
 

	
 
        if 'db_name' not in context:
 
            context['db_name'] = context['pkg_name']
 

	
 
        if 'beaker_session_secret' not in context:
 
            context['beaker_session_secret'] = self.random_string()
 

	
 
        if context['has_db']:
 
            context['requires'].setdefault('psycopg2', True)
 

	
 
        if context['has_web']:
 
            context['requires'].setdefault('Tailbone', True)
 
        elif context['has_db']:
 
            context['requires'].setdefault('rattail', 'rattail[db]')
 
        else:
 
            context['requires'].setdefault('rattail', True)
 

	
 
        if context['uses_fabric']:
 
            context['requires'].setdefault('rattail-fabric2', True)
 

	
 
        if 'alembic_script_location' not in context:
 
            context['alembic_script_location'] = 'rattail.db:alembic'
 

	
 
        if 'alembic_version_locations' not in context:
 
            context['alembic_version_locations'] = ['rattail.db:alembic/versions']
 
        if context['extends_db']:
 
            context['alembic_version_locations'].append(
 
                '{}.db:alembic/versions'.format(context['pkg_name']))
 

	
 
        if 'mako_directories' not in context:
 
            context['mako_directories'] = [
 
                '{}.web:templates'.format(context['pkg_name']),
 
                'tailbone:templates',
 
            ]
 

	
 
        return context
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        super(PoserProjectGenerator, self).generate_project(
 
            output, context, **kwargs)
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        package = os.path.join(output, context['pkg_name'])
 

	
 
        if context['extends_config']:
 
            self.generate('package/config.py.mako',
 
                          os.path.join(package, 'config.py'),
 
                          context)
 

	
 
        if context['has_cli']:
 
            self.generate('package/commands.py.mako',
 
                          os.path.join(package, 'commands.py'),
 
                          context)
 

	
 
        ##############################
 
        # db package dir
 
        ##############################
 

	
 
        db = os.path.join(package, 'db')
 
        os.makedirs(db)
 

	
 
        self.generate('package/db/__init__.py',
 
                      os.path.join(db, '__init__.py'))
 

	
 
        ####################
 
        # model
 
        ####################
 

	
 
        model = os.path.join(db, 'model')
 
        os.makedirs(model)
 

	
 
        self.generate('package/db/model/__init__.py.mako',
 
                      os.path.join(model, '__init__.py'),
 
                      context)
 

	
 
        if context['extends_db']:
 

	
 
            ####################
 
            # alembic
 
            ####################
 

	
 
            alembic = os.path.join(db, 'alembic')
 
            os.makedirs(alembic)
 

	
 
            # TODO: can we get rid of this? why not?
 
            self.generate('package/db/alembic/env.py.mako',
 
                          os.path.join(alembic, 'env.py'),
 
                          context)
 

	
 
            versions = os.path.join(alembic, 'versions')
 
            os.makedirs(versions)
 

	
 
            # make alembic config, aware of new project versions folder
 
            alembic_config = AlembicConfig()
 
            alembic_config.set_main_option('script_location',
 
                                        'rattail.db:alembic')
 
            alembic_config.set_main_option('version_locations',
 
                                        '{} rattail.db:alembic/versions'.format(
 
                                            versions))
 

	
 
            # generate first revision script for new project
 
            script = alembic_revision(alembic_config,
 
                                      version_path=versions,
 
                                      head='rattail@head',
 
                                      splice=True,
 
                                      branch_label=context['pkg_name'],
 
                                      message="add {} branch".format(context['pkg_name']))
 

	
 
            # declare `down_revision = None` ..no way to tell alembic
 
            # to do that apparently, so we must rewrite file
 
            with open(script.path, 'rt') as f:
 
                old_contents = f.read()
 
            new_contents = []
 
            for line in old_contents.split('\n'):
 
                if line.startswith('down_revision ='):
 
                    line = re.sub(r"'\w+'", 'None', line)
 
                new_contents.append(line)
 
            with open(script.path, 'wt') as f:
 
                f.write('\n'.join(new_contents))
 

	
 
        ##############################
 
        # templates
 
        ##############################
 

	
 
        templates = os.path.join(package, 'templates')
 
        os.makedirs(templates)
 

	
 
        installer = os.path.join(templates, 'installer')
 
        os.makedirs(installer)
 

	
 
        self.generate('package/templates/installer/rattail.conf.mako',
 
                      os.path.join(installer, 'rattail.conf.mako'),
 
                      context)
 

	
 
        self.generate('package/templates/installer/upgrade.sh.mako_',
 
                      os.path.join(installer, 'upgrade.sh.mako'))
 

	
 
        ##############################
 
        # web package dir
 
        ##############################
 

	
 
        if context['has_web']:
 

	
 
            web = os.path.join(package, 'web')
 
            os.makedirs(web)
 

	
 
            self.generate('package/web/__init__.py',
 
                          os.path.join(web, '__init__.py'))
 

	
 
            self.generate('package/web/app.py.mako',
 
                          os.path.join(web, 'app.py'),
 
                          context)
 

	
 
            self.generate('package/web/menus.py.mako', os.path.join(web, 'menus.py'),
 
                          context)
 

	
 
            self.generate('package/web/subscribers.py.mako',
 
                          os.path.join(web, 'subscribers.py'),
 
                          context)
 

	
 
            static = os.path.join(web, 'static')
 
            os.makedirs(static)
 

	
 
            self.generate('package/web/static/__init__.py.mako',
 
                          os.path.join(static, '__init__.py'),
 
                          context)
 

	
 
            web_templates = os.path.join(web, 'templates')
 
            os.makedirs(web_templates)
 

	
 
            self.generate('package/web/templates/base_meta.mako_tmpl',
 
                          os.path.join(web_templates, 'base_meta.mako'),
 
                          context)
 

	
 
            views = os.path.join(web, 'views')
 
            os.makedirs(views)
 

	
 
            self.generate('package/web/views/__init__.py.mako',
 
                          os.path.join(views, '__init__.py'),
 
                          context)
rattail/projects/byjove.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/>.
 
#
 
################################################################################
 
"""
 
Generator for 'byjove' projects
 
"""
 

	
 
import os
 

	
 
import colander
 

	
 
from rattail.projects import ProjectGenerator
 

	
 

	
 
class ByjoveProjectGenerator(ProjectGenerator):
 
    """
 
    Generator for Byjove app projects.
 
    """
 
    key = 'byjove'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = colander.Schema()
 

	
 
        schema.add(colander.SchemaNode(name='name',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='slug',
 
                                       typ=colander.String()))
 

	
 
        return schema
 

	
 
    def generate_project(self, output, context, **kwargs):
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('CHANGELOG.md.mako',
 
                      os.path.join(output, 'CHANGELOG.md'),
 
                      context)
 

	
 
        self.generate('gitignore',
 
                      os.path.join(output, '.gitignore'))
 

	
 
        self.generate('README.md.mako',
 
                      os.path.join(output, 'README.md'),
 
                      context)
 

	
 
        self.generate('vue.config.js.dist.mako',
 
                      os.path.join(output, 'vue.config.js.dist'),
 
                      context)
rattail/projects/byjove/README.md.mako
Show inline comments
 
@@ -4,6 +4,8 @@
 

	
 
This is a Vue.js mobile frontend app, meant for use with a Tailbone API backend.
 

	
 
**TODO: this project template is not complete! needs more files etc.**
 

	
 
## Project setup
 
```
 
npm install
rattail/projects/fabric.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/>.
 
#
 
################################################################################
 
"""
 
Generator for 'fabric' projects
 
"""
 

	
 
import os
 

	
 
import colander
 

	
 
from rattail.projects import PythonProjectGenerator
 

	
 

	
 
class FabricProjectGenerator(PythonProjectGenerator):
 
    """
 
    Generator for projects meant only to manage Fabric machine
 
    deployment logic, i.e. no Rattail app.
 
    """
 
    key = 'fabric'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(FabricProjectGenerator, self).make_schema(**kwargs)
 

	
 
        schema.add(colander.SchemaNode(name='organization',
 
                                       typ=colander.String()))
 

	
 
        # TODO: add validation for this?
 
        schema.add(colander.SchemaNode(name='integrates_with',
 
                                       typ=colander.String(),
 
                                       missing=''))
 

	
 
        return schema
 

	
 
    def normalize_context(self, context):
 
        context = super(FabricProjectGenerator, self).normalize_context(context)
 

	
 
        if not context.get('description'):
 
            context['description'] = "Fabric project for {}".format(
 
                context['organization'])
 

	
 
        context['requires']['rattail-fabric2'] = True
 

	
 
        if context['integrates_with'] == 'catapult':
 
            context['requires']['tailbone-onager'] = True
 

	
 
        return context
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        super(FabricProjectGenerator, self).generate_project(
 
            output, context, **kwargs)
 

	
 
        package = os.path.join(output, context['pkg_name'])
 

	
 
        ##############################
 
        # machines
 
        ##############################
 

	
 
        machines = os.path.join(output, 'machines')
 
        os.makedirs(machines)
 

	
 
        ##############################
 
        # generic-server
 
        ##############################
 

	
 
        generic_server = os.path.join(machines, 'generic-server')
 
        os.makedirs(generic_server)
 

	
 
        self.generate('machines/generic-server/README.md.mako',
 
                      os.path.join(generic_server, 'README.md'),
 
                      context)
 

	
 
        self.generate('machines/generic-server/Vagrantfile.mako',
 
                      os.path.join(generic_server, 'Vagrantfile'),
 
                      context)
 

	
 
        self.generate('machines/generic-server/fabenv.py.dist.mako',
 
                      os.path.join(generic_server, 'fabenv.py.dist'),
 
                      context)
 

	
 
        self.generate('machines/generic-server/fabric.yaml.dist',
 
                      os.path.join(generic_server, 'fabric.yaml.dist'))
 

	
 
        self.generate('machines/generic-server/fabfile.py.mako',
 
                      os.path.join(generic_server, 'fabfile.py'),
 
                      context)
 

	
 
        ##############################
 
        # theo-server
 
        ##############################
 

	
 
        theo_server = os.path.join(machines, 'theo-server')
 
        os.makedirs(theo_server)
 

	
 
        self.generate('machines/theo-server/README.md',
 
                      os.path.join(theo_server, 'README.md'))
 

	
 
        self.generate('machines/theo-server/Vagrantfile',
 
                      os.path.join(theo_server, 'Vagrantfile'))
 

	
 
        self.generate('machines/theo-server/fabenv.py.dist.mako',
 
                      os.path.join(theo_server, 'fabenv.py.dist'),
 
                      context)
 

	
 
        self.generate('machines/theo-server/fabric.yaml.dist',
 
                      os.path.join(theo_server, 'fabric.yaml.dist'))
 

	
 
        self.generate('machines/theo-server/fabfile.py.mako',
 
                      os.path.join(theo_server, 'fabfile.py'),
 
                      context)
 

	
 
        theo_deploy = os.path.join(theo_server, 'deploy')
 
        os.makedirs(theo_deploy)
 

	
 
        theo_python = os.path.join(theo_deploy, 'python')
 
        os.makedirs(theo_python)
 

	
 
        self.generate('machines/theo-server/deploy/python/pip.conf.mako',
 
                      os.path.join(theo_python, 'pip.conf.mako'),
 
                      context)
 

	
 
        theo_rattail = os.path.join(theo_deploy, 'rattail')
 
        os.makedirs(theo_rattail)
 

	
 
        self.generate('machines/theo-server/deploy/rattail/rattail.conf.mako',
 
                      os.path.join(theo_rattail, 'rattail.conf.mako'),
 
                      context)
 

	
 
        self.generate('machines/theo-server/deploy/rattail/freetds.conf.mako_',
 
                      os.path.join(theo_rattail, 'freetds.conf.mako'))
 

	
 
        self.generate('machines/theo-server/deploy/rattail/odbc.ini',
 
                      os.path.join(theo_rattail, 'odbc.ini'))
 

	
 
        theo_theo_common = os.path.join(theo_deploy, 'theo-common')
 
        os.makedirs(theo_theo_common)
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/rattail.conf.mako',
 
                      os.path.join(theo_theo_common, 'rattail.conf.mako'),
 
                      context)
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/web.conf.mako',
 
                      os.path.join(theo_theo_common, 'web.conf.mako'),
 
                      context)
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/upgrade.sh.mako',
 
                      os.path.join(theo_theo_common, 'upgrade.sh.mako'),
 
                      context)
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/tasks.py.mako_',
 
                      os.path.join(theo_theo_common, 'tasks.py.mako'))
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/upgrade-wrapper.sh.mako_',
 
                      os.path.join(theo_theo_common, 'upgrade-wrapper.sh.mako'))
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/supervisor.conf.mako_',
 
                      os.path.join(theo_theo_common, 'supervisor.conf.mako'))
 

	
 
        self.generate('machines/theo-server/deploy/theo-common/sudoers.mako_',
 
                      os.path.join(theo_theo_common, 'sudoers.mako'))
rattail/projects/fabric/README.md.mako
Show inline comments
 
@@ -2,6 +2,4 @@
 

	
 
# ${name}
 

	
 
This is a starter Rattail project.  See the
 
[Rattail website](https://rattailproject.org/)
 
for more info.
 
This is a starter Fabric project.
rattail/projects/fabric/machines/generic-server/fabfile.py.mako
Show inline comments
 
@@ -11,7 +11,7 @@ from fabric2 import task
 
from rattail.core import Object
 
from rattail_fabric2 import apt, postfix #, exists, make_system_user, mkdir
 

	
 
from ${python_name} import make_deploy
 
from ${pkg_name} import make_deploy
 

	
 

	
 
env = Object()
rattail/projects/fabric/machines/theo-server/fabfile.py.mako
Show inline comments
 
@@ -11,7 +11,7 @@ from fabric2 import task
 
from rattail.core import Object
 
from rattail_fabric2 import apt, postfix, postgresql, exists, make_system_user, mkdir
 

	
 
from ${python_name} import make_deploy
 
from ${pkg_name} import make_deploy
 

	
 

	
 
env = Object()
rattail/projects/fabric/setup.py.mako
Show inline comments
 
deleted file
rattail/projects/handler.py
Show inline comments
 
@@ -24,17 +24,13 @@
 
Handler for Generating Projects
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import os
 
import random
 
import re
 
import shutil
 
import string
 
import subprocess
 
import sys
 
import warnings
 
import zipfile
 
from collections import OrderedDict
 

	
 
from mako.template import Template
 
from rattail.util import load_entry_points
 

	
 

	
 
class ProjectHandler(object):
 
@@ -46,813 +42,133 @@ class ProjectHandler(object):
 
        self.config = config
 
        self.app = self.config.get_app()
 

	
 
    def get_all_project_types(self):
 
    def get_all_project_generators(self):
 
        """
 
        Returns the list of *all* possible project types.
 
        Returns an ``OrderedDict`` with all available project
 
        generators.
 
        """
 
        return [
 
            'rattail',
 
            'rattail_integration',
 
            'tailbone_integration',
 
            # 'byjove',
 
            'fabric',
 
        ]
 
        generators = load_entry_points('rattail.projects')
 
        generators = sorted(generators.items(),
 
                            key=lambda itm: itm[0])
 
        return OrderedDict(generators)
 

	
 
    def get_supported_project_types(self):
 
    def get_all_project_types(self):
 
        """
 
        Returns the list of "supported" project types.
 
        Returns the list of keys for *all* possible project types.
 
        """
 
        return self.get_all_project_types()
 
        warnings.warn("get_all_project_types() is deprecated; "
 
                      "please use get_all_project_generators() instead",
 
                      DeprecationWarning, stacklevel=2)
 

	
 
    def get_storage_dir(self):
 
        """
 
        Returns the path to root storage (output) dir for all generated
 
        projects.
 
        """
 
        path = self.config.get('rattail', 'generated_projects.storage_dir')
 
        if path:
 
            return path
 
        return os.path.join(self.config.workdir(require=True),
 
                            'generated-projects')
 
        return list(self.get_all_project_generators())
 

	
 
    def get_project_dir(self, slug):
 
    def get_supported_project_generators(self):
 
        """
 
        Returns the storage/output path for generated project with the given
 
        slug name.
 
        Returns the list of "supported" project generators.
 
        """
 
        return os.path.join(self.get_storage_dir(), slug)
 
        return self.get_all_project_generators()
 

	
 
    def generate_project(self, project_type, slug, options, path=None):
 
    def get_supported_project_types(self):
 
        """
 
        Generate source code for a new project.
 

	
 
        Note that this method is *not* meant to be overridden by custom
 
        handlers.  It sticks to the housekeeping chores, and all "interesting"
 
        logic should be found in :meth:`do_generate()` - which you're free to
 
        override.
 

	
 
        :param slug: Canonical "slug" (machine-friendly name) for the project.
 

	
 
        :param options: Dictionary(-like) object which contains whatever
 
           "options" should direct the code generator logic.
 

	
 
        :param path: Path to folder in which project source code should be
 
           generated.  It will be created if it doesn't exist.  If not
 
           specified, will use the path returned by :meth:`get_project_dir()`.
 
        Returns the list of keys for "supported" project types.
 
        """
 
        if not path:
 
            path = self.get_project_dir(slug)
 

	
 
        if os.path.exists(path):
 
            shutil.rmtree(path)
 
        os.makedirs(path)
 

	
 
        if project_type == 'rattail_integration':
 

	
 
            if 'name' not in options:
 
                options['name'] = options['python_project_name']
 

	
 
            if 'slug' not in options:
 
                options['slug'] = options['name']
 

	
 
            if 'year' not in options:
 
                options['year'] = self.app.localtime().year
 

	
 
            if 'integration_studly' not in options:
 
                words = options['integration_name'].split()
 
                options['integration_studly'] = ''.join([word.capitalize()
 
                                                         for word in words])
 

	
 
            if 'integration_prefix' not in options:
 
                words = options['integration_name'].split()
 
                options['integration_prefix'] = '_'.join([word.lower()
 
                                                          for word in words])
 

	
 
        if project_type == 'tailbone_integration':
 

	
 
            if 'name' not in options:
 
                options['name'] = options['python_project_name']
 
        warnings.warn("get_supported_project_types() is deprecated; "
 
                      "please use get_supported_project_generators() instead",
 
                      DeprecationWarning, stacklevel=2)
 

	
 
            if 'year' not in options:
 
                options['year'] = self.app.localtime().year
 
        return list(self.get_supported_project_generators())
 

	
 
        # TODO: all of this logic really belongs elsewhere...
 
        if project_type in ('rattail', 'rattail_integration', 'tailbone_integration', 'fabric'):
 

	
 
            if 'egg_name' not in options:
 
                options['egg_name'] = options['python_project_name'].replace('-', '_')
 

	
 
            if 'studly_name' not in options:
 
                words = options['name'].split('-')
 
                options['studly_name'] = ''.join([word.capitalize()
 
                                                  for word in words])
 

	
 
            if 'app_class_prefix' not in options:
 
                options['app_class_prefix'] = options['studly_name']
 

	
 
            if 'env_name' not in options:
 
                options['env_name'] = slug
 

	
 
        if project_type == 'rattail':
 

	
 
            if 'db_name' not in options:
 
                options['db_name'] = slug
 

	
 
            if 'runas_username' not in options:
 
                options['runas_username'] = slug
 

	
 
            if 'alembic_script_location' not in options:
 
                if options['extends_db']:
 
                    location = '{}.db:alembic'.format(options['python_name'])
 
                else:
 
                    location = 'rattail.db:alembic'
 
                options['alembic_script_location'] = location
 

	
 
            if 'alembic_version_locations' not in options:
 
                locations = ['rattail.db:alembic/versions']
 
                if options['integrates_catapult']:
 
                    locations.append('rattail_onager.db:alembic/versions')
 
                if options['extends_db']:
 
                    locations.append('{}.db:alembic/versions'.format(options['python_name']))
 
                options['alembic_version_locations'] = ' '.join(reversed(locations))
 

	
 
            if 'beaker_session_secret' not in options:
 
                options['beaker_session_secret'] = self.random_string()
 

	
 
        self.do_generate(project_type, slug, options, path)
 
        return path
 

	
 
    def random_string(self, size=20, chars=string.ascii_letters + string.digits):
 
        # per https://stackoverflow.com/a/2257449
 
        return ''.join(random.SystemRandom().choice(chars) for _ in range(size))
 

	
 
    def do_generate(self, project_type, slug, options, path):
 
        """
 
        This method supplies the "true" logic for generating new project code.
 
        Custom handlers may need to override it.  Arguments are essentially the
 
        same as for :meth:`generate_project()`; however note that the ``path``
 
        will already exist when this method is invoked.
 

	
 
        Default logic for this method simply runs the ``pcreate`` command with
 
        the 'rattail' scaffold.  But that is quite limiting, i.e. the only
 
        "option" it accepts is the project name.  (Hopefully improved logic is
 
        coming soon though..!)
 
    def get_project_generator(self, key, require=False):
 
        """
 
        pcreate = os.path.join(os.path.dirname(sys.executable), 'pcreate')
 
        current = os.getcwd()
 
        os.chdir(path)
 
        try:
 
            subprocess.check_call([pcreate, '-s', 'rattail', options['name']])
 
        finally:
 
            os.chdir(current)
 

	
 
    def resolve(self, path):
 
        """
 
        Returns an absolute path, based on the given path.  So, if that's
 
        already an absolute path, it's returned as-is; however if not then the
 
        path is assumed to be relative to this python module.
 
        """
 
        abspath = os.path.abspath(path)
 
        if abspath == path:
 
            return path
 
        return os.path.join(os.path.dirname(__file__), path)
 
        Returns a ``ProjectGenerator`` instance for the given key.
 

	
 
    def generate(self, template, output, **context):
 
        """
 
        Generate a file from the given template, and save the result to the
 
        given output path.
 
        If the key is not valid, returns ``None`` unless
 
        ``require=True`` in which case an error is raised.
 
        """
 
        # maybe run it through our simplistic, hand-rolled template engine
 
        # (note, this is only for the sake of *avoiding* mako logic, when
 
        # generating "actual" mako templates, so we avoid a mako-within-mako
 
        # situation.)
 
        if template.endswith('.mako_tmpl'):
 
            return self.generate_mako_tmpl(template, output, **context)
 

	
 
        # maybe run it through Mako template engine
 
        if template.endswith('.mako'):
 
            return self.generate_mako(template, output, **context)
 

	
 
        # or, just copy the file as-is
 
        template = self.resolve(template)
 
        shutil.copyfile(template, output)
 

	
 
    def generate_mako(self, template, output, **context):
 
        """
 
        Generate a file from the given template, and save the result to the
 
        given output path.
 
        """
 
        template = self.resolve(template)
 
        template = Template(filename=template)
 
        text = template.render(**context)
 
        with open(output, 'wt') as f:
 
            f.write(text)
 
        generators = self.get_all_project_generators()
 
        if key in generators:
 
            return generators[key](self.config)
 
        if require:
 
            raise RuntimeError("Project generator not found for: {}".format(key))
 

	
 
    def generate_mako_tmpl(self, template, output, **context):
 
    def get_storage_dir(self):
 
        """
 
        Generate a file from the given template, and save the result to the
 
        given output path.
 
        Returns the path to root storage (output) dir for all generated
 
        projects.
 
        """
 
        template = os.path.join(os.path.dirname(__file__), template)
 
        with open(template, 'rt') as f:
 
            template_lines = f.readlines()
 

	
 
        output_lines = []
 
        for line in template_lines:
 
            line = line.rstrip('\n')
 
            line = line % context
 
            output_lines.append(line)
 

	
 
        with open(output, 'wt') as f:
 
            f.write('\n'.join(output_lines))
 

	
 

	
 
class RattailProjectHandler(ProjectHandler):
 
    """
 
    Project handler for Rattail
 
    """
 
        path = self.config.get('rattail', 'generated_projects.storage_dir')
 
        if path:
 
            return path
 
        return os.path.join(self.config.workdir(require=True),
 
                            'generated-projects')
 

	
 
    def do_generate(self, project_type, slug, options, path):
 
    def make_project_schema(self, key):
 
        """
 
        And here we do some experimentation...
 
        Make and return a colander schema representing the context
 
        needed for generating a project for the given key.
 
        """
 
        if project_type == 'byjove':
 
            return self.generate_byjove_project(slug, options, path)
 
        elif project_type == 'fabric':
 
            return self.generate_fabric_project(slug, options, path)
 
        elif project_type == 'rattail_integration':
 
            return self.generate_rattail_integration_project(slug, options, path)
 
        elif project_type == 'tailbone_integration':
 
            return self.generate_tailbone_integration_project(slug, options, path)
 
        else:
 
            return self.generate_rattail_project(slug, options, path)
 

	
 
    def generate_rattail_project(self, slug, options, path):
 
        """
 
        And here we do some experimentation...
 
        """
 
        from alembic.config import Config as AlembicConfig
 
        from alembic.command import revision as alembic_revision
 

	
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('rattail/gitignore.mako', os.path.join(path, '.gitignore'),
 
                      **context)
 

	
 
        self.generate('rattail/MANIFEST.in.mako', os.path.join(path, 'MANIFEST.in'),
 
                      **context)
 

	
 
        self.generate('rattail/README.md.mako', os.path.join(path, 'README.md'),
 
                      **context)
 

	
 
        self.generate('rattail/setup.py.mako', os.path.join(path, 'setup.py'),
 
                      **context)
 

	
 
        self.generate('rattail/tasks.py.mako', os.path.join(path, 'tasks.py'),
 
                      **context)
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        package = os.path.join(path, options['python_name'])
 
        os.makedirs(package)
 

	
 
        self.generate('rattail/package/__init__.py.mako', os.path.join(package, '__init__.py'),
 
                      **context)
 

	
 
        self.generate('rattail/package/_version.py', os.path.join(package, '_version.py'))
 

	
 
        self.generate('rattail/package/config.py.mako', os.path.join(package, 'config.py'),
 
                      **context)
 

	
 
        self.generate('rattail/package/commands.py.mako', os.path.join(package, 'commands.py'),
 
                      **context)
 

	
 
        ##############################
 
        # db package dir
 
        ##############################
 

	
 
        if context['extends_db']:
 

	
 
            db = os.path.join(package, 'db')
 
            os.makedirs(db)
 

	
 
            self.generate('rattail/package/db/__init__.py', os.path.join(db, '__init__.py'))
 

	
 
            ####################
 
            # model
 
            ####################
 

	
 
            model = os.path.join(db, 'model')
 
            os.makedirs(model)
 

	
 
            self.generate('rattail/package/db/model/__init__.py.mako', os.path.join(model, '__init__.py'),
 
                          **context)
 

	
 
            ####################
 
            # alembic
 
            ####################
 

	
 
            alembic = os.path.join(db, 'alembic')
 
            os.makedirs(alembic)
 

	
 
            # TODO: can we get rid of this? why not?
 
            self.generate('rattail/package/db/alembic/env.py.mako', os.path.join(alembic, 'env.py'),
 
                          **context)
 

	
 
            versions = os.path.join(alembic, 'versions')
 
            os.makedirs(versions)
 

	
 
            # make alembic config, aware of new project versions folder
 
            alembic_config = AlembicConfig()
 
            alembic_config.set_main_option('script_location',
 
                                        'rattail.db:alembic')
 
            alembic_config.set_main_option('version_locations',
 
                                        '{} rattail.db:alembic/versions'.format(
 
                                            versions))
 

	
 
            # generate first revision script for new project
 
            script = alembic_revision(alembic_config,
 
                                      version_path=versions,
 
                                      head='rattail@head',
 
                                      splice=True,
 
                                      branch_label=context['python_name'],
 
                                      message="add {} branch".format(context['python_name']))
 

	
 
            # declare `down_revision = None` ..no way to tell alembic
 
            # to do that apparently, so we must rewrite file
 
            with open(script.path, 'rt') as f:
 
                old_contents = f.read()
 
            new_contents = []
 
            for line in old_contents.split('\n'):
 
                if line.startswith('down_revision ='):
 
                    line = re.sub(r"'\w+'", 'None', line)
 
                new_contents.append(line)
 
            with open(script.path, 'wt') as f:
 
                f.write('\n'.join(new_contents))
 

	
 
        ##############################
 
        # templates
 
        ##############################
 

	
 
        templates = os.path.join(package, 'templates')
 
        os.makedirs(templates)
 

	
 
        installer = os.path.join(templates, 'installer')
 
        os.makedirs(installer)
 

	
 
        self.generate('rattail/package/templates/installer/rattail.conf.mako_tmpl',
 
                      os.path.join(installer, 'rattail.conf.mako'),
 
                      **context)
 

	
 
        self.generate('rattail/package/templates/installer/upgrade.sh.mako_',
 
                      os.path.join(installer, 'upgrade.sh.mako'))
 

	
 
        ##############################
 
        # web package dir
 
        ##############################
 

	
 
        if context['has_web']:
 

	
 
            web = os.path.join(package, 'web')
 
            os.makedirs(web)
 

	
 
            self.generate('rattail/package/web/__init__.py', os.path.join(web, '__init__.py'))
 

	
 
            self.generate('rattail/package/web/app.py.mako', os.path.join(web, 'app.py'),
 
                          **context)
 

	
 
            self.generate('rattail/package/web/menus.py.mako', os.path.join(web, 'menus.py'),
 
                          **context)
 

	
 
            self.generate('rattail/package/web/subscribers.py.mako', os.path.join(web, 'subscribers.py'),
 
                          **context)
 

	
 
            static = os.path.join(web, 'static')
 
            os.makedirs(static)
 

	
 
            self.generate('rattail/package/web/static/__init__.py.mako', os.path.join(static, '__init__.py'),
 
                          **context)
 

	
 
            web_templates = os.path.join(web, 'templates')
 
            os.makedirs(web_templates)
 

	
 
            self.generate('rattail/package/web/templates/base_meta.mako_tmpl',
 
                          os.path.join(web_templates, 'base_meta.mako'),
 
                          **context)
 

	
 
            views = os.path.join(web, 'views')
 
            os.makedirs(views)
 

	
 
            self.generate('rattail/package/web/views/__init__.py.mako', os.path.join(views, '__init__.py'),
 
                          **context)
 

	
 
            self.generate('rattail/package/web/views/common.py.mako', os.path.join(views, 'common.py'),
 
                          **context)
 
        generator = self.get_project_generator(key, require=True)
 
        return generator.make_schema()
 

	
 
        ##############################
 
        # fablib / machines
 
        ##############################
 

	
 
        if context['uses_fabric']:
 

	
 
            fablib = os.path.join(package, 'fablib')
 
            os.makedirs(fablib)
 

	
 
            self.generate('rattail/package/fablib/__init__.py.mako', os.path.join(fablib, '__init__.py'),
 
                          **context)
 

	
 
            self.generate('rattail/package/fablib/python.py.mako', os.path.join(fablib, 'python.py'),
 
                          **context)
 

	
 
            deploy = os.path.join(fablib, 'deploy')
 
            os.makedirs(deploy)
 

	
 
            python = os.path.join(deploy, 'python')
 
            os.makedirs(python)
 

	
 
            self.generate('rattail/package/fablib/deploy/python/premkvirtualenv.mako', os.path.join(python, 'premkvirtualenv.mako'),
 
                          **context)
 

	
 
            machines = os.path.join(path, 'machines')
 
            os.makedirs(machines)
 

	
 
            server = os.path.join(machines, 'server')
 
            os.makedirs(server)
 

	
 
            self.generate('rattail/machines/server/README.md.mako', os.path.join(server, 'README.md'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/Vagrantfile.mako', os.path.join(server, 'Vagrantfile'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/fabenv.py.dist.mako', os.path.join(server, 'fabenv.py.dist'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/fabric.yaml.dist', os.path.join(server, 'fabric.yaml.dist'))
 

	
 
            self.generate('rattail/machines/server/fabfile.py.mako', os.path.join(server, 'fabfile.py'),
 
                          **context)
 

	
 
            deploy = os.path.join(server, 'deploy')
 
            os.makedirs(deploy)
 

	
 
            poser = os.path.join(deploy, slug)
 
            os.makedirs(poser)
 

	
 
            if options['integrates_catapult']:
 
                self.generate('rattail/machines/server/deploy/poser/freetds.conf.mako_', os.path.join(poser, 'freetds.conf.mako'))
 
                self.generate('rattail/machines/server/deploy/poser/odbc.ini', os.path.join(poser, 'odbc.ini'))
 

	
 
            self.generate('rattail/machines/server/deploy/poser/rattail.conf.mako', os.path.join(poser, 'rattail.conf.mako'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/cron.conf.mako', os.path.join(poser, 'cron.conf'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/web.conf.mako', os.path.join(poser, 'web.conf.mako'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/supervisor.conf.mako', os.path.join(poser, 'supervisor.conf'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/overnight.sh.mako', os.path.join(poser, 'overnight.sh'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/overnight-wrapper.sh.mako', os.path.join(poser, 'overnight-wrapper.sh'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/crontab.mako', os.path.join(poser, 'crontab.mako'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/upgrade.sh.mako', os.path.join(poser, 'upgrade.sh'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/tasks.py.mako', os.path.join(poser, 'tasks.py'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/upgrade-wrapper.sh.mako', os.path.join(poser, 'upgrade-wrapper.sh'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/sudoers.mako', os.path.join(poser, 'sudoers'),
 
                          **context)
 

	
 
            self.generate('rattail/machines/server/deploy/poser/logrotate.conf.mako', os.path.join(poser, 'logrotate.conf'),
 
                          **context)
 

	
 
    def generate_rattail_integration_project(self, slug, options, path):
 
    def generate_project(self, key, output=None, context=None, **kwargs):
 
        """
 
        And here we do some experimentation...
 
        """
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('rattail_integration/gitignore.mako',
 
                      os.path.join(path, '.gitignore'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/MANIFEST.in.mako',
 
                      os.path.join(path, 'MANIFEST.in'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/README.rst.mako',
 
                      os.path.join(path, 'README.rst'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/CHANGELOG.md.mako',
 
                      os.path.join(path, 'CHANGELOG.md'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/setup.py.mako',
 
                      os.path.join(path, 'setup.py'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/tasks.py.mako',
 
                      os.path.join(path, 'tasks.py'),
 
                      **context)
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        package = os.path.join(path, options['python_name'])
 
        os.makedirs(package)
 

	
 
        self.generate('rattail_integration/package/__init__.py.mako',
 
                      os.path.join(package, '__init__.py'),
 
                      **context)
 

	
 
        self.generate('rattail_integration/package/_version.py',
 
                      os.path.join(package, '_version.py'))
 
        Generate source code for a new project, and return the path to
 
        the output folder.
 

	
 
        if context['extends_config']:
 
            self.generate('rattail_integration/package/config.py.mako',
 
                          os.path.join(package, 'config.py'),
 
                          **context)
 
        :param key: Key identifying which type of project to generate.
 

	
 
        ##############################
 
        # db package dir
 
        ##############################
 
        :param output: Optional path to the output folder.  If not
 
           specified, one will be determined automatically.
 

	
 
        if context['extends_db']:
 

	
 
            db = os.path.join(package, 'db')
 
            os.makedirs(db)
 

	
 
            self.generate('rattail_integration/package/db/__init__.py',
 
                          os.path.join(db, '__init__.py'))
 

	
 
            ####################
 
            # model
 
            ####################
 

	
 
            model = os.path.join(db, 'model')
 
            os.makedirs(model)
 

	
 
            self.generate('rattail_integration/package/db/model/__init__.py.mako',
 
                          os.path.join(model, '__init__.py'),
 
                          **context)
 

	
 
            self.generate('rattail_integration/package/db/model/customers.py.mako',
 
                          os.path.join(model, 'customers.py'),
 
                          **context)
 

	
 
            ####################
 
            # alembic
 
            ####################
 

	
 
            alembic = os.path.join(db, 'alembic')
 
            os.makedirs(alembic)
 

	
 
            versions = os.path.join(alembic, 'versions')
 
            os.makedirs(versions)
 

	
 
            self.generate('rattail_integration/package/db/alembic/versions/.keepme',
 
                          os.path.join(versions, '.keepme'))
 

	
 
    def generate_tailbone_integration_project(self, slug, options, path):
 
        :param context: Data dictionary with template context,
 
           appropriate for the project type.
 
        """
 
        And here we do some experimentation...
 
        """
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('tailbone_integration/gitignore.mako',
 
                      os.path.join(path, '.gitignore'),
 
                      **context)
 
        generator = self.get_project_generator(key)
 
        context = generator.normalize_context(context or {})
 

	
 
        self.generate('tailbone_integration/MANIFEST.in.mako',
 
                      os.path.join(path, 'MANIFEST.in'),
 
                      **context)
 
        if not output:
 
            folder = context.get('folder', key)
 
            output = os.path.join(self.get_storage_dir(), folder)
 

	
 
        self.generate('tailbone_integration/README.rst.mako',
 
                      os.path.join(path, 'README.rst'),
 
                      **context)
 
        if os.path.exists(output):
 
            shutil.rmtree(output)
 
        os.makedirs(output)
 

	
 
        self.generate('tailbone_integration/CHANGELOG.md.mako',
 
                      os.path.join(path, 'CHANGELOG.md'),
 
                      **context)
 
        generator.generate_project(output, context)
 
        return output
 

	
 
        self.generate('tailbone_integration/setup.py.mako',
 
                      os.path.join(path, 'setup.py'),
 
                      **context)
 

	
 
        self.generate('tailbone_integration/tasks.py.mako',
 
                      os.path.join(path, 'tasks.py'),
 
                      **context)
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        package = os.path.join(path, options['python_name'])
 
        os.makedirs(package)
 

	
 
        self.generate('tailbone_integration/package/__init__.py.mako',
 
                      os.path.join(package, '__init__.py'),
 
                      **context)
 

	
 
        self.generate('tailbone_integration/package/_version.py',
 
                      os.path.join(package, '_version.py'))
 

	
 
        ##############################
 
        # views
 
        ##############################
 

	
 
        views = os.path.join(package, 'views')
 
        os.makedirs(views)
 

	
 
        self.generate('tailbone_integration/package/views/__init__.py',
 
                      os.path.join(views, '__init__.py'))
 

	
 
        ##############################
 
        # static
 
        ##############################
 

	
 
        if options['has_static_files']:
 

	
 
            static = os.path.join(package, 'static')
 
            os.makedirs(static)
 

	
 
            self.generate('tailbone_integration/package/static/__init__.py.mako',
 
                          os.path.join(static, '__init__.py'),
 
                          **context)
 

	
 
        ##############################
 
        # templates
 
        ##############################
 

	
 
        templates = os.path.join(package, 'templates')
 
        os.makedirs(templates)
 

	
 
        self.generate('tailbone_integration/package/templates/.keepme',
 
                      os.path.join(templates, '.keepme'))
 

	
 
    def generate_byjove_project(self, slug, options, path):
 
    def zip_output(self, output, zipped=None):
 
        """
 
        Generate a new 'byjove' project per the given arguments.
 
        """
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('byjove/CHANGELOG.md.mako', os.path.join(path, 'CHANGELOG.md'),
 
                      **context)
 

	
 
        self.generate('byjove/gitignore', os.path.join(path, '.gitignore'))
 

	
 
        self.generate('byjove/README.md.mako', os.path.join(path, 'README.md'),
 
                      **context)
 
        Compress the given output folder and save as ZIP file.
 

	
 
        self.generate('byjove/vue.config.js.dist.mako', os.path.join(path, 'vue.config.js.dist'),
 
                      **context)
 
        :param output: Path to the output folder.
 

	
 
    def generate_fabric_project(self, slug, options, path):
 
        :param zipped: Optional path to the final ZIP file.  If not
 
           specified, it will be the same path as ``output`` but with
 
           a ``.zip`` file extension.
 
        """
 
        Generate a new 'fabric' project per the given arguments.
 
        """
 
        context = options
 

	
 
        ##############################
 
        # root project dir
 
        ##############################
 

	
 
        self.generate('fabric/gitignore.mako', os.path.join(path, '.gitignore'),
 
                      **context)
 

	
 
        self.generate('fabric/README.md.mako', os.path.join(path, 'README.md'),
 
                      **context)
 

	
 
        self.generate('fabric/setup.py.mako', os.path.join(path, 'setup.py'),
 
                      **context)
 

	
 
        ##############################
 
        # package dir
 
        ##############################
 

	
 
        package = os.path.join(path, options['python_name'])
 
        os.makedirs(package)
 
        if not zipped:
 
            zipped = '{}.zip'.format(output)
 

	
 
        self.generate('fabric/package/__init__.py.mako', os.path.join(package, '__init__.py'),
 
                      **context)
 
        folder = os.path.basename(output)
 

	
 
        self.generate('fabric/package/_version.py', os.path.join(package, '_version.py'))
 
        with zipfile.ZipFile(zipped, 'w', zipfile.ZIP_DEFLATED) as z:
 
            self.zipdir(z, output, folder)
 

	
 
        ##############################
 
        # machines
 
        ##############################
 
        return zipped
 

	
 
        machines = os.path.join(path, 'machines')
 
        os.makedirs(machines)
 
    def zipdir(self, zipf, path, folder):
 
        for root, dirs, files in os.walk(path):
 
            relative_root = os.path.join(folder, root[len(path)+1:])
 
            for fname in files:
 
                zipf.write(os.path.join(root, fname),
 
                           arcname=os.path.join(relative_root, fname))
 

	
 
        ##############################
 
        # generic-server
 
        ##############################
 

	
 
        generic_server = os.path.join(machines, 'generic-server')
 
        os.makedirs(generic_server)
 

	
 
        self.generate('fabric/machines/generic-server/README.md.mako', os.path.join(generic_server, 'README.md'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/generic-server/Vagrantfile.mako', os.path.join(generic_server, 'Vagrantfile'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/generic-server/fabenv.py.dist.mako', os.path.join(generic_server, 'fabenv.py.dist'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/generic-server/fabric.yaml.dist', os.path.join(generic_server, 'fabric.yaml.dist'))
 

	
 
        self.generate('fabric/machines/generic-server/fabfile.py.mako', os.path.join(generic_server, 'fabfile.py'),
 
                      **context)
 

	
 
        ##############################
 
        # theo-server
 
        ##############################
 

	
 
        theo_server = os.path.join(machines, 'theo-server')
 
        os.makedirs(theo_server)
 

	
 
        self.generate('fabric/machines/theo-server/README.md', os.path.join(theo_server, 'README.md'))
 

	
 
        self.generate('fabric/machines/theo-server/Vagrantfile', os.path.join(theo_server, 'Vagrantfile'))
 

	
 
        self.generate('fabric/machines/theo-server/fabenv.py.dist.mako', os.path.join(theo_server, 'fabenv.py.dist'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/theo-server/fabric.yaml.dist', os.path.join(theo_server, 'fabric.yaml.dist'))
 

	
 
        self.generate('fabric/machines/theo-server/fabfile.py.mako', os.path.join(theo_server, 'fabfile.py'),
 
                      **context)
 

	
 
        theo_deploy = os.path.join(theo_server, 'deploy')
 
        os.makedirs(theo_deploy)
 

	
 
        theo_python = os.path.join(theo_deploy, 'python')
 
        os.makedirs(theo_python)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/python/pip.conf.mako', os.path.join(theo_python, 'pip.conf.mako'),
 
                      **context)
 

	
 
        theo_rattail = os.path.join(theo_deploy, 'rattail')
 
        os.makedirs(theo_rattail)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/rattail/rattail.conf.mako', os.path.join(theo_rattail, 'rattail.conf.mako'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/rattail/freetds.conf.mako_', os.path.join(theo_rattail, 'freetds.conf.mako'))
 

	
 
        self.generate('fabric/machines/theo-server/deploy/rattail/odbc.ini', os.path.join(theo_rattail, 'odbc.ini'))
 

	
 
        theo_theo_common = os.path.join(theo_deploy, 'theo-common')
 
        os.makedirs(theo_theo_common)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/rattail.conf.mako', os.path.join(theo_theo_common, 'rattail.conf.mako'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/web.conf.mako', os.path.join(theo_theo_common, 'web.conf.mako'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/upgrade.sh.mako', os.path.join(theo_theo_common, 'upgrade.sh.mako'),
 
                      **context)
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/tasks.py.mako_', os.path.join(theo_theo_common, 'tasks.py.mako'))
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/upgrade-wrapper.sh.mako_', os.path.join(theo_theo_common, 'upgrade-wrapper.sh.mako'))
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/supervisor.conf.mako_', os.path.join(theo_theo_common, 'supervisor.conf.mako'))
 
class RattailProjectHandler(ProjectHandler):
 

	
 
        self.generate('fabric/machines/theo-server/deploy/theo-common/sudoers.mako_', os.path.join(theo_theo_common, 'sudoers.mako'))
 
    def __init__(self, *args, **kwargs):
 
        warnings.warn("RattailProjectHandler is deprecated; "
 
                      "please just use ProjectHandler instead",
 
                      DeprecationWarning, stacklevel=2)
 
        super(RattailProjectHandler, self).__init__(*args, **kwargs)
rattail/projects/poser/MANIFEST.in.mako
Show inline comments
 
new file 100644
 
## -*- mode: conf; -*-
 

	
 
include *.md
 
include *.rst
 

	
 
% if extends_db:
 
recursive-include ${pkg_name}/db/alembic *.py
 
recursive-include ${pkg_name}/db/alembic *.mako
 
% endif
 

	
 
% if has_web:
 
recursive-include ${pkg_name}/web/static *.css
 
recursive-include ${pkg_name}/web/static *.js
 
recursive-include ${pkg_name}/web/templates *.mako
 
% endif
rattail/projects/poser/README.md.mako
Show inline comments
 
new file 100644
 
## -*- mode: markdown; -*-
 

	
 
# ${name}
 

	
 
This is a custom Rattail/Poser project.  See the
 
[Rattail website](https://rattailproject.org/) for more info.
 

	
 
<%text>##</%text> Quick Start
 

	
 
Make a virtual environment:
 

	
 
    cd /path/to/envs
 
    python3 -m venv ./${env_name}
 

	
 
Enter and activate it:
 

	
 
    cd ${env_name}
 
    source bin/activate
 

	
 
Install the ${name} package:
 

	
 
    pip install ${pypi_name}
 

	
 
Run the ${name} app installer:
 

	
 
    ${pkg_name} install
 

	
 
<%text>##</%text> Running from Source
 

	
 
The above shows how to run from a package release.
 

	
 
To run from local source folder instead, substitute the `pip install`
 
command above with:
 

	
 
    pip install -e /path/to/src/${folder}
rattail/projects/poser/gitignore.mako
Show inline comments
 
file renamed from rattail/projects/rattail/gitignore.mako to rattail/projects/poser/gitignore.mako
rattail/projects/poser/package/commands.py.mako
Show inline comments
 
file renamed from rattail/projects/rattail/package/commands.py.mako to rattail/projects/poser/package/commands.py.mako
 
@@ -7,7 +7,7 @@ import sys
 

	
 
from rattail import commands
 

	
 
from ${python_name} import __version__
 
from ${pkg_name} import __version__
 

	
 

	
 
def main(*args):
 
@@ -23,7 +23,7 @@ class Command(commands.Command):
 
    """
 
    Main command for ${name}
 
    """
 
    name = '${python_name}'
 
    name = '${pkg_name}'
 
    version = __version__
 
    description = "${name} (custom Rattail system)"
 
    long_description = ''
 
@@ -48,8 +48,8 @@ class Install(commands.InstallSubcommand):
 
    description = __doc__.strip()
 

	
 
    # nb. these must be explicitly set b/c config is not available
 
    # when running normally, e.g. `${python_name} -n install`
 
    # when running normally, e.g. `${pkg_name} -n install`
 
    app_title = "${name}"
 
    app_package = '${python_name}'
 
    app_package = '${pkg_name}'
 
    app_eggname = '${egg_name}'
 
    app_pypiname = '${python_project_name}'
 
    app_pypiname = '${pypi_name}'
rattail/projects/poser/package/config.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} config extensions
 
"""
 

	
 
from rattail.config import ConfigExtension
 

	
 

	
 
class ${studly_prefix}Config(ConfigExtension):
 
    """
 
    Rattail config extension for ${name}
 
    """
 
    key = '${pkg_name}'
 

	
 
    def configure(self, config):
 

	
 
        # default app title
 
        config.setdefault('rattail', 'app_title', "${name.replace('"', '\\"')}")
 

	
 
        # primary data model
 
        config.setdefault('rattail', 'model', '${pkg_name}.db.model')
 

	
 
        # menu handler
 
        config.setdefault('tailbone.menus', 'handler', '${pkg_name}.web.menus:${studly_prefix}MenuHandler')
rattail/projects/poser/package/db/__init__.py
Show inline comments
 
file renamed from rattail/projects/rattail/package/db/__init__.py to rattail/projects/poser/package/db/__init__.py
rattail/projects/poser/package/db/alembic/env.py.mako
Show inline comments
 
file renamed from rattail/projects/rattail/package/db/alembic/env.py.mako to rattail/projects/poser/package/db/alembic/env.py.mako
 
@@ -20,7 +20,7 @@ rattail_config = make_config(alembic_config.config_file_name, usedb=False, versi
 

	
 
# configure Continuum..this is trickier than we want but it works..
 
configure_versioning(rattail_config, force=True)
 
from ${python_name}.db import model
 
from ${pkg_name}.db import model
 
configure_mappers()
 

	
 
# needed for 'autogenerate' support
rattail/projects/poser/package/db/model/__init__.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} data models
 
"""
 

	
 
# bring in all of Rattail
 
from rattail.db.model import *
 

	
 
# TODO: import other/custom models here...
rattail/projects/poser/package/templates/installer/rattail.conf.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: conf; -*-
 
${'## -*- coding: utf-8; mode: conf; -*-'}
 

	
 
## NOTE: this is a Mako template, which generates a Mako template!
 

	
 
${'<%text>############################################################</%text>'}
 
#
 
${'# ${app_title} core config'}
 
#
 
${'<%text>############################################################</%text>'}
 

	
 
####################
 
## main body
 
####################
 

	
 
${self.render_group_preamble()}
 

	
 
${self.render_group_rattail()}
 

	
 
${self.render_group_alembic()}
 

	
 
${self.render_group_logging()}
 

	
 
####################
 
## preamble
 
####################
 

	
 
<%def name="render_group_preamble()"></%def>
 

	
 
####################
 
## rattail
 
####################
 

	
 
<%def name="render_group_rattail()">
 
${self.render_heading('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')}"}
 

	
 
[rattail.config]
 
# require = /etc/rattail/rattail.conf
 
configure_logging = true
 
usedb = true
 
preferdb = true
 

	
 
[rattail.db]
 
${'default.url = ${db_url}'}
 
versioning.enabled = true
 

	
 
[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
 

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

	
 
####################
 
## alembic
 
####################
 

	
 
<%def name="render_group_alembic()">
 
${self.render_heading('alembic')}
 

	
 
[alembic]
 
script_location = rattail.db:alembic
 
## nb. this line is *not* escaped, is part of 1st mako pass
 
version_locations = ${' '.join(reversed(alembic_version_locations))}
 
</%def>
 

	
 
####################
 
## logging
 
####################
 

	
 
<%def name="render_group_logging()">
 
${self.render_heading('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>
 

	
 
####################
 
## other
 
####################
 

	
 
<%def name="render_heading(label)">
 
${'<%text>##############################</%text>'}
 
# ${label}
 
${'<%text>##############################</%text>'}
 
</%def>
rattail/projects/poser/package/templates/installer/upgrade.sh.mako_
Show inline comments
 
file renamed from rattail/projects/rattail/package/templates/installer/upgrade.sh.mako_ to rattail/projects/poser/package/templates/installer/upgrade.sh.mako_
rattail/projects/poser/package/web/__init__.py
Show inline comments
 
file renamed from rattail/projects/rattail/package/web/__init__.py to rattail/projects/poser/package/web/__init__.py
rattail/projects/poser/package/web/app.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} web app
 
"""
 

	
 
from tailbone import app
 

	
 

	
 
def main(global_config, **settings):
 
    """
 
    This function returns a Pyramid WSGI application.
 
    """
 
    # prefer ${name} templates over Tailbone
 
    settings.setdefault('mako.directories', [
 
        % for path in mako_directories:
 
        '${path}',
 
        % endfor
 
    ])
 

	
 
    # make config objects
 
    rattail_config = app.make_rattail_config(settings)
 
    pyramid_config = app.make_pyramid_config(settings)
 

	
 
    # bring in the rest of ${name}
 
    pyramid_config.include('${pkg_name}.web.static')
 
    pyramid_config.include('${pkg_name}.web.subscribers')
 
    pyramid_config.include('${pkg_name}.web.views')
 

	
 
    return pyramid_config.make_wsgi_app()
 

	
 

	
 
def asgi_main():
 
    """
 
    This function returns an ASGI application.
 
    """
 
    from tailbone.asgi import make_asgi_app
 

	
 
    return make_asgi_app(main)
rattail/projects/poser/package/web/menus.py.mako
Show inline comments
 
file renamed from rattail/projects/rattail/package/web/menus.py.mako to rattail/projects/poser/package/web/menus.py.mako
 
@@ -7,7 +7,7 @@ ${name} Menu
 
from tailbone import menus as base
 

	
 

	
 
class ${app_class_prefix}MenuHandler(base.MenuHandler):
 
class ${studly_prefix}MenuHandler(base.MenuHandler):
 
    """
 
    ${name} menu handler
 
    """
 
@@ -22,6 +22,6 @@ class ${app_class_prefix}MenuHandler(base.MenuHandler):
 
        # ]
 

	
 
        # ...but for now this uses default menus
 
        menus = super(${app_class_prefix}MenuHandler, self).make_menus(request, **kwargs)
 
        menus = super(${studly_prefix}MenuHandler, self).make_menus(request, **kwargs)
 

	
 
        return menus
rattail/projects/poser/package/web/static/__init__.py.mako
Show inline comments
 
file renamed from rattail/projects/rattail/package/web/static/__init__.py.mako to rattail/projects/poser/package/web/static/__init__.py.mako
 
@@ -7,4 +7,4 @@ Static assets
 

	
 
def includeme(config):
 
    config.include('tailbone.static')
 
    config.add_static_view('${python_name}', '${python_name}.web:static', cache_max_age=3600)
 
    config.add_static_view('${pkg_name}', '${pkg_name}.web:static', cache_max_age=3600)
rattail/projects/poser/package/web/subscribers.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
Pyramid event subscribers
 
"""
 

	
 
import ${pkg_name}
 

	
 

	
 
def add_${pkg_name}_to_context(event):
 
    renderer_globals = event
 
    renderer_globals['${pkg_name}'] = ${pkg_name}
 

	
 

	
 
def includeme(config):
 
    config.include('tailbone.subscribers')
 
    config.add_subscriber(add_${pkg_name}_to_context, 'pyramid.events.BeforeRender')
rattail/projects/poser/package/web/templates/base_meta.mako_tmpl
Show inline comments
 
file renamed from rattail/projects/rattail/package/web/templates/base_meta.mako_tmpl to rattail/projects/poser/package/web/templates/base_meta.mako_tmpl
 
@@ -2,16 +2,17 @@
 
<%%inherit file="tailbone:templates/base_meta.mako" />
 

	
 
<%%def name="favicon()">
 
  ## <link rel="icon" type="image/x-icon" href="${request.static_url('%(python_name)s.web:static/favicon.ico')}" />
 
  ## <link rel="icon" type="image/x-icon" href="${request.static_url('%(pkg_name)s.web:static/favicon.ico')}" />
 
  <link rel="icon" type="image/x-icon" href="${request.static_url('tailbone:static/img/rattail.ico')}" />
 
</%%def>
 

	
 
<%%def name="header_logo()">
 
  ## ${h.image(request.static_url('%(pkg_name)s.web:static/img/logo.png'), "Header Logo", style="height: 49px;")}
 
  ${h.image(request.static_url('tailbone:static/img/rattail.ico'), "Header Logo", style="height: 49px;")}
 
</%%def>
 

	
 
<%%def name="footer()">
 
  <p class="has-text-centered">
 
    ${h.link_to("%(name)s {}{}".format(%(python_name)s.__version__, '' if request.rattail_config.production() else '+dev'), url('about'))}
 
    ${h.link_to("%(name)s {}{}".format(%(pkg_name)s.__version__, '' if request.rattail_config.production() else '+dev'), url('about'))}
 
  </p>
 
</%%def>
rattail/projects/poser/package/web/views/__init__.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} Views
 
"""
 

	
 
from tailbone.views import essentials
 

	
 

	
 
def includeme(config):
 

	
 
    essentials.defaults(config)
 

	
 
    # main table views
 
    config.include('tailbone.views.brands')
 
    config.include('tailbone.views.customergroups')
 
    config.include('tailbone.views.customers')
 
    config.include('tailbone.views.departments')
 
    config.include('tailbone.views.employees')
 
    config.include('tailbone.views.families')
 
    config.include('tailbone.views.members')
 
    config.include('tailbone.views.messages')
 
    config.include('tailbone.views.products')
 
    config.include('tailbone.views.reportcodes')
 
    config.include('tailbone.views.shifts')
 
    config.include('tailbone.views.stores')
 
    config.include('tailbone.views.subdepartments')
 
    config.include('tailbone.views.taxes')
 
    config.include('tailbone.views.vendors')
 

	
 
    # purchasing / receiving
 
    config.include('tailbone.views.purchases')
 
    config.include('tailbone.views.purchasing')
 

	
 
    # batch views
 
    config.include('tailbone.views.batch.handheld')
 
    config.include('tailbone.views.batch.inventory')
rattail/projects/python/CHANGELOG.md.mako
Show inline comments
 
file renamed from rattail/projects/rattail_integration/CHANGELOG.md.mako to rattail/projects/python/CHANGELOG.md.mako
rattail/projects/python/MANIFEST.in.mako
Show inline comments
 
new file 100644
 
## -*- mode: conf; -*-
 
include *.md
rattail/projects/python/README.md.mako
Show inline comments
 
new file 100644
 
## -*- mode: markdown; -*-
 

	
 
# ${name}
 

	
 
This is a starter Python project.
rattail/projects/python/gitignore.mako
Show inline comments
 
file renamed from rattail/projects/fabric/gitignore.mako to rattail/projects/python/gitignore.mako
rattail/projects/python/package/__init__.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} package root
 
"""
 

	
 
from ._version import __version__
rattail/projects/python/package/_version.py
Show inline comments
 
file renamed from rattail/projects/rattail/package/_version.py to rattail/projects/python/package/_version.py
rattail/projects/python/setup.py.mako
Show inline comments
 
new file 100644
 
## -*- coding: utf-8; mode: python; -*-
 
# -*- coding: utf-8; -*-
 
"""
 
${name} setup script
 
"""
 

	
 
import os
 
from setuptools import setup, find_packages
 

	
 

	
 
here = os.path.abspath(os.path.dirname(__file__))
 
exec(open(os.path.join(here, '${pkg_name}', '_version.py')).read())
 
README = open(os.path.join(here, 'README.md')).read()
 

	
 

	
 
setup(
 
    name = "${pypi_name}",
 
    version = __version__,
 
    author = "Your Name",
 
    author_email = "you@example.com",
 
    # url = "https://example.com/",
 
    description = "${description}",
 
    long_description = README,
 

	
 
    classifiers = [
 

	
 
        # TODO: remove this if you intend to publish your project
 
        # (it's here by default, to prevent accidental publishing)
 
        'Private :: Do Not Upload',
 

	
 
        % for classifier in sorted(classifiers):
 
        '${classifier}',
 
        % endfor
 
    ],
 

	
 
    install_requires = [
 
        % for pkg, spec in requires.items():
 
        % if spec:
 
        '${pkg if spec is True else spec}',
 
        % endif
 
        % endfor
 

	
 
        # TODO: these may be needed to build/release package
 
        #'build',
 
        #'invoke',
 
        #'twine',
 
    ],
 
    packages = find_packages(),
 
    include_package_data = True,
 

	
 
    entry_points = {
 
        % for key, values in entry_points.items():
 
        '${key}': [
 
            % for value in values:
 
            '${value}',
 
            % endfor
 
        ],
 
        % endfor
 
    },
 
)
rattail/projects/python/tasks.py.mako
Show inline comments
 
file renamed from rattail/projects/rattail/tasks.py.mako to rattail/projects/python/tasks.py.mako
 
@@ -11,7 +11,7 @@ from invoke import task
 

	
 

	
 
here = os.path.abspath(os.path.dirname(__file__))
 
exec(open(os.path.join(here, '${python_name}', '_version.py')).read())
 
exec(open(os.path.join(here, '${pkg_name}', '_version.py')).read())
 

	
 

	
 
@task
 
@@ -25,10 +25,10 @@ def release(c):
 
    c.run('python -m build --sdist')
 

	
 
    # filename of built package
 
    filename = '${python_project_name}-{}.tar.gz'.format(__version__)
 
    filename = '${pypi_name}-{}.tar.gz'.format(__version__)
 

	
 
    # TODO: uncomment and update these details, to upload to private PyPI
 
    #c.run('scp dist/{} rattail@pypi.example.com:/srv/pypi/${slug}/'.format(filename))
 
    #c.run('scp dist/{} rattail@pypi.example.com:/srv/pypi/${folder}/'.format(filename))
 

	
 
    # TODO: or, uncomment this to upload to *public* PyPI
 
    #c.run('twine upload dist/{}'.format(filename))
rattail/projects/rattail.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/>.
 
#
 
################################################################################
 
"""
 
Generator for 'rattail' projects
 
"""
 

	
 
import os
 

	
 
import colander
 

	
 

	
 
from rattail.projects import PoserProjectGenerator
 

	
 

	
 
class RattailProjectGenerator(PoserProjectGenerator):
 
    """
 
    Generator for "generic" Rattail projects
 
    """
 
    key = 'rattail'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(RattailProjectGenerator, self).make_schema(**kwargs)
 

	
 
        # TODO: get rid of these after templates are updated
 

	
 
        schema.add(colander.SchemaNode(name='integrates_catapult',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='integrates_corepos',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='integrates_locsms',
 
                                       typ=colander.Boolean()))
 

	
 
        return schema
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        super(RattailProjectGenerator, self).generate_project(
 
            output, context, **kwargs)
 

	
 
        package = os.path.join(output, context['pkg_name'])
 

	
 
        ##############################
 
        # fablib / machines
 
        ##############################
 

	
 
        if context['uses_fabric']:
 

	
 
            fablib = os.path.join(package, 'fablib')
 
            os.makedirs(fablib)
 

	
 
            self.generate('package/fablib/__init__.py.mako',
 
                          os.path.join(fablib, '__init__.py'),
 
                          context)
 

	
 
            self.generate('package/fablib/python.py.mako',
 
                          os.path.join(fablib, 'python.py'),
 
                          context)
 

	
 
            deploy = os.path.join(fablib, 'deploy')
 
            os.makedirs(deploy)
 

	
 
            python = os.path.join(deploy, 'python')
 
            os.makedirs(python)
 

	
 
            self.generate('package/fablib/deploy/python/premkvirtualenv.mako',
 
                          os.path.join(python, 'premkvirtualenv.mako'),
 
                          context)
 

	
 
            machines = os.path.join(output, 'machines')
 
            os.makedirs(machines)
 

	
 
            server = os.path.join(machines, 'server')
 
            os.makedirs(server)
 

	
 
            self.generate('machines/server/README.md.mako',
 
                          os.path.join(server, 'README.md'),
 
                          context)
 

	
 
            self.generate('machines/server/Vagrantfile.mako',
 
                          os.path.join(server, 'Vagrantfile'),
 
                          context)
 

	
 
            self.generate('machines/server/fabenv.py.dist.mako',
 
                          os.path.join(server, 'fabenv.py.dist'),
 
                          context)
 

	
 
            self.generate('machines/server/fabric.yaml.dist',
 
                          os.path.join(server, 'fabric.yaml.dist'))
 

	
 
            self.generate('machines/server/fabfile.py.mako',
 
                          os.path.join(server, 'fabfile.py'),
 
                          context)
 

	
 
            deploy = os.path.join(server, 'deploy')
 
            os.makedirs(deploy)
 

	
 
            poser = os.path.join(deploy, context['folder'])
 
            os.makedirs(poser)
 

	
 
            if context['integrates_catapult']:
 
                self.generate('machines/server/deploy/poser/freetds.conf.mako_',
 
                              os.path.join(poser, 'freetds.conf.mako'))
 
                self.generate('machines/server/deploy/poser/odbc.ini',
 
                              os.path.join(poser, 'odbc.ini'))
 

	
 
            self.generate('machines/server/deploy/poser/rattail.conf.mako',
 
                          os.path.join(poser, 'rattail.conf.mako'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/cron.conf.mako',
 
                          os.path.join(poser, 'cron.conf'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/web.conf.mako',
 
                          os.path.join(poser, 'web.conf.mako'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/supervisor.conf.mako',
 
                          os.path.join(poser, 'supervisor.conf'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/overnight.sh.mako',
 
                          os.path.join(poser, 'overnight.sh'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/overnight-wrapper.sh.mako',
 
                          os.path.join(poser, 'overnight-wrapper.sh'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/crontab.mako',
 
                          os.path.join(poser, 'crontab.mako'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/upgrade.sh.mako',
 
                          os.path.join(poser, 'upgrade.sh'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/tasks.py.mako',
 
                          os.path.join(poser, 'tasks.py'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/upgrade-wrapper.sh.mako',
 
                          os.path.join(poser, 'upgrade-wrapper.sh'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/sudoers.mako',
 
                          os.path.join(poser, 'sudoers'),
 
                          context)
 

	
 
            self.generate('machines/server/deploy/poser/logrotate.conf.mako',
 
                          os.path.join(poser, 'logrotate.conf'),
 
                          context)
rattail/projects/rattail/MANIFEST.in.mako
Show inline comments
 
deleted file
rattail/projects/rattail/README.md.mako
Show inline comments
 
deleted file
rattail/projects/rattail/machines/server/deploy/poser/rattail.conf.mako
Show inline comments
 
@@ -25,7 +25,7 @@ production = <%text>${'true' if env.machine_is_live else 'false'}</%text>
 
timezone.default = America/Chicago
 

	
 
# TODO: set this to a valid user within your DB
 
#runas.default = ${runas_username}
 
#runas.default = ${pkg_name}
 

	
 
appdir = /srv/envs/${env_name}/app
 
datadir = /srv/envs/${env_name}/app/data
 
@@ -50,7 +50,7 @@ send_emails = true
 
smtp.server = localhost
 

	
 
templates =
 
    ${python_name}:templates/mail
 
    ${pkg_name}:templates/mail
 
    rattail:templates/mail
 

	
 
default.prefix = [${name}]
rattail/projects/rattail/machines/server/deploy/poser/supervisor.conf.mako
Show inline comments
 
## -*- mode: conf; -*-
 

	
 
[group:${python_name}]
 
programs=${python_name}_webmain
 
[group:${pkg_name}]
 
programs=${pkg_name}_webmain
 

	
 
[program:${python_name}_webmain]
 
[program:${pkg_name}_webmain]
 
command=/srv/envs/${env_name}/bin/pserve pastedeploy+ini:/srv/envs/${env_name}/app/web.conf
 
directory=/srv/envs/${env_name}/app/work
 
user=rattail
rattail/projects/rattail/machines/server/deploy/poser/upgrade-wrapper.sh.mako
Show inline comments
 
@@ -16,4 +16,4 @@ INVOKE="sudo -H -u rattail bin/invoke --collection=app/tasks $INVOKE_ARGS"
 
$INVOKE upgrade
 

	
 
# restart web app
 
sh -c 'sleep 10; supervisorctl restart ${python_name}:${python_name}_webmain' &
 
sh -c 'sleep 10; supervisorctl restart ${pkg_name}:${pkg_name}_webmain' &
rattail/projects/rattail/machines/server/deploy/poser/upgrade.sh.mako
Show inline comments
 
@@ -24,14 +24,14 @@ $PIP install $QUIET --disable-pip-version-check --upgrade pip
 
# packages "from source" as opposed to only using built/released packages
 

	
 
# if running ${name} from source, you should first fetch/install latest code:
 
#cd $SRC/${slug}
 
#cd $SRC/${folder}
 
#git pull $QUIET
 
#find . -name '*.pyc' -delete
 
#$PIP install $QUIET --editable .
 

	
 
# in any case the last step is always the same.  note that this will ensure the
 
# "latest" ${name} is used, but also will upgrade any dependencies
 
$PIP install $QUIET --upgrade --upgrade-strategy eager '${python_project_name}'
 
$PIP install $QUIET --upgrade --upgrade-strategy eager '${pypi_name}'
 

	
 
# migrate database schema
 
cd /srv/envs/${env_name}
rattail/projects/rattail/machines/server/deploy/poser/web.conf.mako
Show inline comments
 
@@ -32,7 +32,7 @@ beaker.session.type = file
 
beaker.session.data_dir = %(here)s/sessions/data
 
beaker.session.lock_dir = %(here)s/sessions/lock
 
beaker.session.secret = <%text>${env.tailbone_beaker_secret}</%text>
 
beaker.session.key = ${slug}
 
beaker.session.key = ${folder}
 

	
 
pyramid_deform.tempdir = %(here)s/data/uploads
 

	
rattail/projects/rattail/machines/server/fabfile.py.mako
Show inline comments
 
@@ -14,8 +14,8 @@ from rattail_fabric2 import apt, postfix, postgresql, python, exists, make_syste
 
from rattail_fabric2 import freetds
 
% endif
 

	
 
from ${python_name}.fablib import make_deploy
 
from ${python_name}.fablib.python import bootstrap_python
 
from ${pkg_name}.fablib import make_deploy
 
from ${pkg_name}.fablib.python import bootstrap_python
 

	
 

	
 
env = Object()
 
@@ -32,7 +32,7 @@ def bootstrap_all(c):
 
    Bootstrap all aspects of the machine
 
    """
 
    bootstrap_base(c)
 
    bootstrap_${python_name}(c)
 
    bootstrap_${pkg_name}(c)
 

	
 

	
 
@task
 
@@ -65,9 +65,9 @@ def bootstrap_base(c):
 
    # is too old.  however the *latest* source seems to have some issue(s)
 
    # which cause it to use too much memory, so we use a more stable branch
 
    freetds.install_from_source(c, user='rattail', branch='Branch-1_2')
 
    deploy(c, '${slug}/freetds.conf.mako', '/usr/local/etc/freetds.conf',
 
    deploy(c, '${folder}/freetds.conf.mako', '/usr/local/etc/freetds.conf',
 
           use_sudo=True, context={'env': env})
 
    deploy(c, '${slug}/odbc.ini', '/etc/odbc.ini', use_sudo=True)
 
    deploy(c, '${folder}/odbc.ini', '/etc/odbc.ini', use_sudo=True)
 
    % endif
 

	
 
    # misc.
 
@@ -83,13 +83,13 @@ def bootstrap_base(c):
 

	
 

	
 
@task
 
def bootstrap_${python_name}(c):
 
def bootstrap_${pkg_name}(c):
 
    """
 
    Bootstrap the ${name} app
 
    """
 
    user = 'rattail'
 

	
 
    c.sudo('supervisorctl stop ${python_name}:', warn=True)
 
    c.sudo('supervisorctl stop ${pkg_name}:', warn=True)
 

	
 
    # virtualenv
 
    if not exists(c, '/srv/envs/${env_name}'):
 
@@ -99,11 +99,11 @@ def bootstrap_${python_name}(c):
 

	
 
    # ${name}
 
    if env.machine_is_live:
 
        c.sudo("bash -lc 'workon ${env_name} && pip install ${python_project_name}'",
 
        c.sudo("bash -lc 'workon ${env_name} && pip install ${pypi_name}'",
 
               user=user)
 
    else:
 
        # TODO: this really only works for vagrant
 
        c.sudo("bash -lc 'workon ${env_name} && pip install /vagrant/${python_project_name}-*.tar.gz'",
 
        c.sudo("bash -lc 'workon ${env_name} && pip install /vagrant/${pypi_name}-*.tar.gz'",
 
               user=user)
 

	
 
    # app dir
 
@@ -113,24 +113,24 @@ def bootstrap_${python_name}(c):
 
    mkdir(c, '/srv/envs/${env_name}/app/data', use_sudo=True, owner=user)
 

	
 
    # config / scripts
 
    deploy(c, '${slug}/rattail.conf.mako', '/srv/envs/${env_name}/app/rattail.conf',
 
    deploy(c, '${folder}/rattail.conf.mako', '/srv/envs/${env_name}/app/rattail.conf',
 
           owner=user, mode='0600', use_sudo=True, context={'env': env})
 
    if not exists(c, '/srv/envs/${env_name}/app/quiet.conf'):
 
        c.sudo("bash -lc 'workon ${env_name} && cdvirtualenv app && rattail make-config -T quiet'",
 
               user=user)
 
    deploy(c, '${slug}/cron.conf', '/srv/envs/${env_name}/app/cron.conf',
 
    deploy(c, '${folder}/cron.conf', '/srv/envs/${env_name}/app/cron.conf',
 
           owner=user, use_sudo=True)
 
    deploy(c, '${slug}/web.conf.mako', '/srv/envs/${env_name}/app/web.conf',
 
    deploy(c, '${folder}/web.conf.mako', '/srv/envs/${env_name}/app/web.conf',
 
           owner=user, mode='0600', use_sudo=True, context={'env': env})
 
    deploy(c, '${slug}/upgrade.sh', '/srv/envs/${env_name}/app/upgrade.sh',
 
    deploy(c, '${folder}/upgrade.sh', '/srv/envs/${env_name}/app/upgrade.sh',
 
           owner=user, mode='0755', use_sudo=True)
 
    deploy(c, '${slug}/tasks.py', '/srv/envs/${env_name}/app/tasks.py',
 
    deploy(c, '${folder}/tasks.py', '/srv/envs/${env_name}/app/tasks.py',
 
           owner=user, use_sudo=True)
 
    deploy(c, '${slug}/upgrade-wrapper.sh', '/srv/envs/${env_name}/app/upgrade-wrapper.sh',
 
    deploy(c, '${folder}/upgrade-wrapper.sh', '/srv/envs/${env_name}/app/upgrade-wrapper.sh',
 
           owner=user, mode='0755', use_sudo=True)
 
    deploy(c, '${slug}/overnight.sh', '/srv/envs/${env_name}/app/overnight.sh',
 
    deploy(c, '${folder}/overnight.sh', '/srv/envs/${env_name}/app/overnight.sh',
 
           owner=user, mode='0755', use_sudo=True)
 
    deploy(c, '${slug}/overnight-wrapper.sh', '/srv/envs/${env_name}/app/overnight-wrapper.sh',
 
    deploy(c, '${folder}/overnight-wrapper.sh', '/srv/envs/${env_name}/app/overnight-wrapper.sh',
 
           owner=user, mode='0755', use_sudo=True)
 

	
 
    # database
 
@@ -146,16 +146,16 @@ def bootstrap_${python_name}(c):
 
                       database='${db_name}')
 

	
 
    # supervisor
 
    deploy(c, '${slug}/supervisor.conf', '/etc/supervisor/conf.d/${python_name}.conf',
 
    deploy(c, '${folder}/supervisor.conf', '/etc/supervisor/conf.d/${pkg_name}.conf',
 
           use_sudo=True)
 
    c.sudo('supervisorctl update')
 
    c.sudo('supervisorctl start ${python_name}:')
 
    c.sudo('supervisorctl start ${pkg_name}:')
 

	
 
    # cron etc.
 
    deploy.sudoers(c, '${slug}/sudoers', '/etc/sudoers.d/${python_name}')
 
    deploy(c, '${slug}/crontab.mako', '/etc/cron.d/${python_name}',
 
    deploy.sudoers(c, '${folder}/sudoers', '/etc/sudoers.d/${pkg_name}')
 
    deploy(c, '${folder}/crontab.mako', '/etc/cron.d/${pkg_name}',
 
           use_sudo=True, context={'env': env})
 
    deploy(c, '${slug}/logrotate.conf', '/etc/logrotate.d/${python_name}',
 
    deploy(c, '${folder}/logrotate.conf', '/etc/logrotate.d/${pkg_name}',
 
           use_sudo=True)
 

	
 

	
rattail/projects/rattail/package/__init__.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail/package/config.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail/package/db/model/__init__.py.mako
Show inline comments
 
@@ -12,4 +12,4 @@ from rattail.db.model import *
 
from rattail_onager.db.model import *
 
% endif
 

	
 
# TODO: import your custom models here...
 
# TODO: import other/custom models here...
rattail/projects/rattail/package/fablib/python.py.mako
Show inline comments
 
@@ -6,7 +6,7 @@ Fabric library for Python
 

	
 
from rattail_fabric2 import python as base
 

	
 
from ${python_name}.fablib import make_deploy
 
from ${pkg_name}.fablib import make_deploy
 

	
 

	
 
deploy = make_deploy(__file__)
rattail/projects/rattail/package/templates/installer/rattail.conf.mako_tmpl
Show inline comments
 
deleted file
rattail/projects/rattail/package/web/app.py.mako
Show inline comments
 
@@ -12,7 +12,7 @@ def main(global_config, **settings):
 
    This function returns a Pyramid WSGI application.
 
    """
 
    # prefer ${name} templates over Tailbone
 
    settings.setdefault('mako.directories', ['${python_name}.web:templates',
 
    settings.setdefault('mako.directories', ['${pkg_name}.web:templates',
 
                                             'tailbone:templates'])
 

	
 
    # make config objects
 
@@ -29,8 +29,8 @@ def main(global_config, **settings):
 
    % endif
 

	
 
    # bring in the rest of ${name}
 
    pyramid_config.include('${python_name}.web.static')
 
    pyramid_config.include('${python_name}.web.subscribers')
 
    pyramid_config.include('${python_name}.web.views')
 
    pyramid_config.include('${pkg_name}.web.static')
 
    pyramid_config.include('${pkg_name}.web.subscribers')
 
    pyramid_config.include('${pkg_name}.web.views')
 

	
 
    return pyramid_config.make_wsgi_app()
rattail/projects/rattail/package/web/subscribers.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail/package/web/views/__init__.py.mako
Show inline comments
 
@@ -7,8 +7,10 @@ ${name} Views
 

	
 
def includeme(config):
 

	
 
    # TODO: should use 'essential' views here
 

	
 
    # core views
 
    config.include('${python_name}.web.views.common')
 
    config.include('tailbone.web.views.common')
 
    config.include('tailbone.views.auth')
 
    config.include('tailbone.views.importing')
 
    config.include('tailbone.views.luigi')
rattail/projects/rattail/package/web/views/common.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail/setup.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail_integration.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/>.
 
#
 
################################################################################
 
"""
 
Generator for 'rattail-integration' projects
 
"""
 

	
 
import os
 

	
 
import colander
 

	
 
from rattail.projects import PythonProjectGenerator
 

	
 

	
 
class RattailIntegrationProjectGenerator(PythonProjectGenerator):
 
    """
 
    Generator for projects which integrate Rattail with some other
 
    system.  This is for generating projects such as rattail-corepos
 
    and rattail-mailchimp etc.
 
    """
 
    key = 'rattail_integration'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(RattailIntegrationProjectGenerator, self).make_schema(**kwargs)
 

	
 
        schema.add(colander.SchemaNode(name='integration_name',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='integration_url',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='extends_config',
 
                                       typ=colander.Boolean()))
 

	
 
        schema.add(colander.SchemaNode(name='extends_db',
 
                                       typ=colander.Boolean()))
 

	
 
        return schema
 

	
 
    def normalize_context(self, context):
 
        context = super(RattailIntegrationProjectGenerator, self).normalize_context(context)
 

	
 
        if not context.get('description'):
 
            context['description'] = "Rattail integration package for {}".format(
 
                context['integration_name'])
 

	
 
        context['classifiers'].update(set([
 
            'Topic :: Office/Business',
 
        ]))
 

	
 
        context['entry_points'].setdefault('rattail.config.extensions', []).extend([
 
            "{0} = {0}.config:{1}Config".format(context['pkg_name'], context['studly_prefix']),
 
        ])
 

	
 
        context['requires']['rattail'] = True
 

	
 
        if 'year' not in context:
 
            context['year'] = self.app.today().year
 

	
 
        return context
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        super(RattailIntegrationProjectGenerator, self).generate_project(
 
            output, context, **kwargs)
 

	
 
        package = os.path.join(output, context['pkg_name'])
 

	
 
        ##############################
 
        # root package dir
 
        ##############################
 

	
 
        if context['extends_config']:
 
            self.generate('package/config.py.mako',
 
                          os.path.join(package, 'config.py'),
 
                          context)
 

	
 
        ##############################
 
        # db package dir
 
        ##############################
 

	
 
        if context['extends_db']:
 

	
 
            db = os.path.join(package, 'db')
 
            os.makedirs(db)
 

	
 
            self.generate('package/db/__init__.py',
 
                          os.path.join(db, '__init__.py'))
 

	
 
            ####################
 
            # model
 
            ####################
 

	
 
            model = os.path.join(db, 'model')
 
            os.makedirs(model)
 

	
 
            self.generate('package/db/model/__init__.py.mako',
 
                          os.path.join(model, '__init__.py'),
 
                          context)
 

	
 
            self.generate('package/db/model/customers.py.mako',
 
                          os.path.join(model, 'customers.py'),
 
                          context)
 

	
 
            ####################
 
            # alembic
 
            ####################
 

	
 
            alembic = os.path.join(db, 'alembic')
 
            os.makedirs(alembic)
 

	
 
            versions = os.path.join(alembic, 'versions')
 
            os.makedirs(versions)
 

	
 
            self.generate('package/db/alembic/versions/.keepme',
 
                          os.path.join(versions, '.keepme'))
rattail/projects/rattail_integration/MANIFEST.in.mako
Show inline comments
 
include *.md
 
include *.rst
 
% if extends_db:
 
recursive-include ${python_name}/db/alembic *.mako
 
recursive-include ${pkg_name}/db/alembic *.mako
 
% endif
rattail/projects/rattail_integration/README.md.mako
Show inline comments
 
new file 100644
 
## -*- mode: markdown; -*-
 

	
 
# ${name}
 

	
 
Rattail is a retail software framework, released under the GNU General
 
Public License.
 

	
 
This package contains software interfaces for
 
[${integration_name}](${integration_url}).
 

	
 
Please see the [Rattail Project](https://rattailproject.org/)
 
for more information.
rattail/projects/rattail_integration/README.rst.mako
Show inline comments
 
deleted file
rattail/projects/rattail_integration/gitignore.mako
Show inline comments
 
deleted file
rattail/projects/rattail_integration/package/config.py.mako
Show inline comments
 
@@ -28,11 +28,11 @@ Custom config
 
from rattail.config import ConfigExtension
 

	
 

	
 
class ${integration_studly}ConfigExtension(ConfigExtension):
 
class ${studly_prefix}ConfigExtension(ConfigExtension):
 
    """
 
    Rattail config extension for ${integration_name}
 
    """
 
    key = '${python_name}'
 
    key = '${pkg_name}'
 

	
 
    def configure(self, config):
 

	
rattail/projects/rattail_integration/package/db/model/__init__.py.mako
Show inline comments
 
@@ -25,4 +25,4 @@
 
${integration_name} integration data models
 
"""
 

	
 
from .customers import ${integration_studly}Customer
 
from .customers import ${studly_prefix}Customer
rattail/projects/rattail_integration/package/db/model/customers.py.mako
Show inline comments
 
@@ -31,14 +31,14 @@ from sqlalchemy import orm
 
from rattail.db import model
 

	
 

	
 
class ${integration_studly}Customer(model.Base):
 
class ${studly_prefix}Customer(model.Base):
 
    """
 
    ${integration_name} extensions to core Customer model
 
    """
 
    __tablename__ = '${integration_prefix}_customer'
 
    __tablename__ = '${pkg_name}_customer'
 
    __table_args__ = (
 
        sa.ForeignKeyConstraint(['uuid'], ['customer.uuid'],
 
                                name='${integration_prefix}_customer_fk_customer'),
 
                                name='${pkg_name}_customer_fk_customer'),
 
    )
 
    __versioned__ = {}
 

	
 
@@ -50,7 +50,7 @@ class ${integration_studly}Customer(model.Base):
 
        Customer to which this extension record pertains.
 
        """,
 
        backref=orm.backref(
 
            '_${integration_prefix}',
 
            '_${pkg_name}',
 
            uselist=False,
 
            cascade='all, delete-orphan',
 
            doc="""
rattail/projects/rattail_integration/setup.py.mako
Show inline comments
 
deleted file
rattail/projects/rattail_integration/tasks.py.mako
Show inline comments
 
deleted file
rattail/projects/tailbone_integration.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/>.
 
#
 
################################################################################
 
"""
 
Generator for 'tailbone-integration' projects
 
"""
 

	
 
import os
 

	
 
import colander
 

	
 
from rattail.projects import PythonProjectGenerator
 

	
 

	
 
class TailboneIntegrationProjectGenerator(PythonProjectGenerator):
 
    """
 
    Generator for projects which integrate Tailbone with some other
 
    system.  This is for generating projects such as tailbone-corepos
 
    and tailbone-mailchimp etc.
 
    """
 
    key = 'tailbone_integration'
 

	
 
    def make_schema(self, **kwargs):
 
        schema = super(TailboneIntegrationProjectGenerator, self).make_schema(**kwargs)
 

	
 
        schema.add(colander.SchemaNode(name='integration_name',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='integration_url',
 
                                       typ=colander.String()))
 

	
 
        schema.add(colander.SchemaNode(name='has_static_files',
 
                                       typ=colander.Boolean()))
 

	
 
        return schema
 

	
 
    def normalize_context(self, context):
 
        context = super(TailboneIntegrationProjectGenerator, self).normalize_context(context)
 

	
 
        if not context.get('description'):
 
            context['description'] = "Tailbone integration package for {}".format(
 
                context['integration_name'])
 

	
 
        context['classifiers'].update(set([
 
            'Environment :: Web Environment',
 
            'Framework :: Pyramid',
 
            'Operating System :: POSIX :: Linux',
 
            'Topic :: Office/Business',
 
        ]))
 

	
 
        context['requires']['Tailbone'] = True
 

	
 
        if 'year' not in context:
 
            context['year'] = self.app.today().year
 

	
 
        return context
 

	
 
    def generate_project(self, output, context, **kwargs):
 
        super(TailboneIntegrationProjectGenerator, self).generate_project(
 
            output, context, **kwargs)
 

	
 
        package = os.path.join(output, context['pkg_name'])
 

	
 
        ##############################
 
        # views
 
        ##############################
 

	
 
        views = os.path.join(package, 'views')
 
        os.makedirs(views)
 

	
 
        self.generate('package/views/__init__.py',
 
                      os.path.join(views, '__init__.py'))
 

	
 
        ##############################
 
        # static
 
        ##############################
 

	
 
        if context['has_static_files']:
 

	
 
            static = os.path.join(package, 'static')
 
            os.makedirs(static)
 

	
 
            self.generate('package/static/__init__.py.mako',
 
                          os.path.join(static, '__init__.py'),
 
                          context)
 

	
 
        ##############################
 
        # templates
 
        ##############################
 

	
 
        templates = os.path.join(package, 'templates')
 
        os.makedirs(templates)
 

	
 
        self.generate('package/templates/.keepme',
 
                      os.path.join(templates, '.keepme'))
rattail/projects/tailbone_integration/CHANGELOG.md.mako
Show inline comments
 
deleted file
rattail/projects/tailbone_integration/MANIFEST.in.mako
Show inline comments
 
include *.md
 
include *.rst
 
recursive-include ${python_name}/templates *.mako
 
recursive-include ${pkg_name}/templates *.mako
rattail/projects/tailbone_integration/README.md.mako
Show inline comments
 
new file 100644
 
## -*- mode: markdown; -*-
 

	
 
# ${name}
 

	
 
Rattail is a retail software framework, released under the GNU General
 
Public License.
 

	
 
This package contains software interfaces for
 
[${integration_name}](${integration_url}).
 

	
 
Please see the [Rattail Project](https://rattailproject.org/)
 
for more information.
rattail/projects/tailbone_integration/README.rst.mako
Show inline comments
 
deleted file
rattail/projects/tailbone_integration/gitignore.mako
Show inline comments
 
deleted file
rattail/projects/tailbone_integration/package/static/__init__.py.mako
Show inline comments
 
@@ -27,5 +27,5 @@ Static assets for ${name}
 

	
 

	
 
def includeme(config):
 
    config.add_static_view('${python_name}', '${python_name}:static',
 
    config.add_static_view('${pkg_name}', '${pkg_name}:static',
 
                           cache_max_age=3600)
rattail/projects/tailbone_integration/setup.py.mako
Show inline comments
 
deleted file
rattail/projects/tailbone_integration/tasks.py.mako
Show inline comments
 
deleted file
rattail/trainwreck/handler.py
Show inline comments
 
@@ -24,12 +24,10 @@
 
Trainwreck Handler
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import warnings
 
from collections import OrderedDict
 

	
 
from rattail.app import GenericHandler
 
from rattail.util import OrderedDict
 
from rattail.trainwreck.db import Session as TrainwreckSession
 

	
 

	
rattail/trainwreck/importing/trainwreck.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.
 
#
 
@@ -24,15 +24,13 @@
 
TrainWreck -> Trainwreck data importing
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 

	
 
import datetime
 
from collections import OrderedDict
 

	
 
from rattail.importing.handlers import FromSQLAlchemyHandler, ToSQLAlchemyHandler
 
from rattail.importing.sqlalchemy import FromSQLAlchemySameToSame
 
from rattail.trainwreck.db import Session as TrainwreckSession
 
from rattail.time import localtime, make_utc
 
from rattail.util import OrderedDict
 
from rattail.trainwreck.importing import model
 
from rattail.trainwreck.importing.util import ToOrFromTrainwreck
 

	
rattail/util.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.
 
#
 
@@ -24,25 +24,18 @@
 
Utilities
 
"""
 

	
 
from __future__ import unicode_literals, absolute_import
 
from __future__ import division
 

	
 
import collections
 
import importlib
 
import re
 
import shlex
 
import sys
 
import datetime
 
import decimal
 
import subprocess
 
import logging
 
import warnings
 

	
 
import six
 
from pkg_resources import iter_entry_points
 

	
 
try:
 
    from collections import OrderedDict
 
except ImportError: # pragma no cover
 
    from ordereddict import OrderedDict
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 
@@ -50,6 +43,14 @@ log = logging.getLogger(__name__)
 
NOTSET = object()
 

	
 

	
 
class OrderedDict(collections.OrderedDict):
 
    def __init__(self, *args, **kwargs):
 
        warnings.warn("rattail.util.OrderedDict is deprecated; "
 
                      "please use collections.OrderedDict instead",
 
                      DeprecationWarning, stacklevel=2)
 
        super(OrderedDict, self).__init__(*args, **kwargs)
 

	
 

	
 
def capture_output(*args, **kwargs):
 
    """
 
    Runs ``command`` and returns any output it produces.
 
@@ -130,55 +131,79 @@ def data_diffs(local_data, host_data, fields=None,
 
    return diffs
 

	
 

	
 
def get_class_hierarchy(klass, topfirst=True):
 
    """
 
    Returns a list of all classes in the inheritance chain for the
 
    given class.
 

	
 
    For instance::
 

	
 
       class A(object):
 
          pass
 

	
 
       class B(A):
 
          pass
 

	
 
       class C(B):
 
          pass
 

	
 
       get_class_hierarchy(C)
 
       # -> [A, B, C]
 

	
 
    Specify ``topfirst=False`` to get ``[C, B, A]`` instead.
 
    """
 
    hierarchy = []
 

	
 
    def traverse(cls):
 
        if cls is not object:
 
            hierarchy.append(cls)
 
            for parent in cls.__bases__:
 
                traverse(parent)
 

	
 
    traverse(klass)
 
    if topfirst:
 
        hierarchy.reverse()
 
    return hierarchy
 

	
 

	
 
def import_module_path(module_path):
 
    """
 
    Import an arbitrary Python module.
 

	
 
    .. warning::
 

	
 
       This function is deprecated; please use
 
       :func:`python:importlib.import_module()` instead.
 

	
 
    :param module_path: String referencing a module by its "dotted path".
 

	
 
    :returns: The referenced module.
 
    """
 
    if six.PY3:
 
        try:
 
            import importlib
 
        except ImportError:
 
            pass
 
        else:
 
            return importlib.import_module(module_path)
 
    warnings.warn("rattail.util.import_module_path() is deprecated; "
 
                  "please use importlib.import_module() instead",
 
                  DeprecationWarning, stacklevel=2)
 

	
 
    if module_path in sys.modules:
 
        return sys.modules[module_path]
 
    module = __import__(module_path)
 

	
 
    def last_module(module, module_path):
 
        parts = module_path.split('.')
 
        parts.pop(0)
 
        child = getattr(module, parts[0])
 
        if len(parts) == 1:
 
            return child
 
        return last_module(child, '.'.join(parts))
 

	
 
    return last_module(module, module_path)
 
    return importlib.import_module(module_path)
 

	
 

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

	
 
    .. warning::
 

	
 
       This function is deprecated; please use
 
       :func:`python:importlib.reload()` instead.
 

	
 
    :param module: An already-loaded module.
 

	
 
    :returns: The module.
 
    """
 
    if six.PY3:
 
        try:
 
            import importlib
 
        except ImportError:
 
            pass
 
        else:
 
            return importlib.reload(module)
 
    warnings.warn("rattail.util.import_reload() is deprecated; "
 
                  "please use importlib.reload() instead",
 
                  DeprecationWarning, stacklevel=2)
 

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

	
 

	
 
def get_object_spec(obj):
 
@@ -212,7 +237,7 @@ def load_object(specifier):
 
    :returns: The specified object.
 
    """
 
    module_path, name = specifier.split(':')
 
    module = import_module_path(module_path)
 
    module = importlib.import_module(module_path)
 
    return getattr(module, name)
 

	
 

	
 
@@ -312,8 +337,8 @@ def pretty_quantity(value, empty_zero=False):
 
        value = int(value)
 
        if empty_zero and value == 0:
 
            return ''
 
        return six.text_type(value)
 
    return six.text_type(value).rstrip('0')
 
        return str(value)
 
    return str(value).rstrip('0')
 

	
 

	
 
def progress_loop(func, items, factory, *args, **kwargs):
 
@@ -361,8 +386,8 @@ def simple_error(error):
 

	
 
       "ErrorClass"
 
    """
 
    cls = six.text_type(type(error).__name__)
 
    msg = six.text_type(error)
 
    cls = str(type(error).__name__)
 
    msg = str(error)
 
    if msg:
 
        return "{}: {}".format(cls, msg)
 
    return cls
setup.py
Show inline comments
 
@@ -267,6 +267,14 @@ setup(
 
            'to_trainwreck.from_trainwreck.import = rattail.trainwreck.importing.trainwreck:FromTrainwreckToTrainwreckImport',
 
        ],
 

	
 
        'rattail.projects': [
 
            'byjove = rattail.projects.byjove:ByjoveProjectGenerator',
 
            'fabric = rattail.projects.fabric:FabricProjectGenerator',
 
            'rattail = rattail.projects.rattail:RattailProjectGenerator',
 
            'rattail_integration = rattail.projects.rattail_integration:RattailIntegrationProjectGenerator',
 
            'tailbone_integration = rattail.projects.tailbone_integration:TailboneIntegrationProjectGenerator',
 
        ],
 

	
 
        'rattail.reports': [
 
            'customer_mailing = rattail.reporting.customer_mailing:CustomerMailing',
 
        ],
0 comments (0 inline, 0 general)