diff --git a/rattail/projects/base.py b/rattail/projects/base.py index fb93cbb0..6f39857f 100644 --- a/rattail/projects/base.py +++ b/rattail/projects/base.py @@ -30,12 +30,11 @@ import re import shutil import string import sys -import unicodedata import colander from mako.template import Template -from rattail.util import get_class_hierarchy +from rattail.util import get_class_hierarchy, get_studly_prefix from rattail.mako import ResourceTemplateLookup @@ -276,13 +275,6 @@ class PythonProjectGenerator(ProjectGenerator): 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 = re.split(r'[\- ]', name) - return ''.join([word.capitalize() for word in words]) - def normalize_context(self, context): context = super(PythonProjectGenerator, self).normalize_context(context) @@ -296,7 +288,7 @@ class PythonProjectGenerator(ProjectGenerator): context['egg_name'] = context['pypi_name'].replace('-', '_') if 'studly_prefix' not in context: - context['studly_prefix'] = self.get_studly_prefix(context['name']) + context['studly_prefix'] = get_studly_prefix(context['name']) if 'env_name' not in context: context['env_name'] = context['folder'] @@ -478,14 +470,12 @@ class RattailAdjacentProjectGenerator(PythonProjectGenerator): # model #################### - if context['has_model']: + model = os.path.join(db, 'model') + os.makedirs(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/__init__.py.mako', + os.path.join(model, '__init__.py'), + context) #################### # alembic @@ -496,11 +486,6 @@ class RattailAdjacentProjectGenerator(PythonProjectGenerator): 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) diff --git a/rattail/projects/rattail_adjacent/package/db/alembic/env.py.mako b/rattail/projects/rattail_adjacent/package/db/alembic/env.py.mako deleted file mode 100644 index 4f29e797..00000000 --- a/rattail/projects/rattail_adjacent/package/db/alembic/env.py.mako +++ /dev/null @@ -1,74 +0,0 @@ -# -*- coding: utf-8; mode: python; -*- -""" -Alembic environment script -""" - -from alembic import context -from sqlalchemy.orm import configure_mappers - -from rattail.config import make_config -from rattail.db.util import get_default_engine -from rattail.db.continuum import configure_versioning - - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -alembic_config = context.config - -# use same config file for Rattail -rattail_config = make_config(alembic_config.config_file_name, usedb=False, versioning=False) - -# configure Continuum..this is trickier than we want but it works.. -configure_versioning(rattail_config, force=True) -from ${pkg_name}.db import model -configure_mappers() - -# needed for 'autogenerate' support -target_metadata = model.Base.metadata - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - engine = get_default_engine(rattail_config) - context.configure( - url=engine.url, - target_metadata=target_metadata) - - with context.begin_transaction(): - context.run_migrations() - - -def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - engine = get_default_engine(rattail_config) - connection = engine.connect() - context.configure( - connection=connection, - target_metadata=target_metadata) - - try: - with context.begin_transaction(): - context.run_migrations() - finally: - connection.close() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - run_migrations_online() diff --git a/rattail/projects/rattail_integration.py b/rattail/projects/rattail_integration.py index 334d6199..19bd7ed2 100644 --- a/rattail/projects/rattail_integration.py +++ b/rattail/projects/rattail_integration.py @@ -29,6 +29,7 @@ import os import colander from rattail.projects import RattailAdjacentProjectGenerator +from rattail.util import get_studly_prefix, get_package_name class RattailIntegrationProjectGenerator(RattailAdjacentProjectGenerator): @@ -61,6 +62,14 @@ class RattailIntegrationProjectGenerator(RattailAdjacentProjectGenerator): 'Topic :: Software Development :: Libraries :: Python Modules', ])) + if 'integration_studly_prefix' not in context: + context['integration_studly_prefix'] = get_studly_prefix( + context['integration_name']) + + if 'integration_pkgname' not in context: + context['integration_pkgname'] = get_package_name( + context['integration_name']) + if 'year' not in context: context['year'] = self.app.today().year diff --git a/rattail/projects/rattail_shopfoo.py b/rattail/projects/rattail_shopfoo.py new file mode 100644 index 00000000..d5bf741f --- /dev/null +++ b/rattail/projects/rattail_shopfoo.py @@ -0,0 +1,67 @@ +# -*- 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 . +# +################################################################################ +""" +Generator for 'rattail-shopfoo' integration projects +""" + +import os + +from rattail.projects.rattail_integration import RattailIntegrationProjectGenerator + + +class RattailShopfooProjectGenerator(RattailIntegrationProjectGenerator): + """ + Generator for projects which integrate Rattail with some type of + e-commerce system. This is for generating projects such as + rattail-instacart, rattail-mercato etc. which involve a nightly + export/upload of product data to external server. + """ + key = 'rattail_shopfoo' + + def normalize_context(self, context): + + # nb. auto-set some flags + context['extends_db'] = True + context['has_model'] = True + + # then do normal logic + context = super(RattailShopfooProjectGenerator, self).normalize_context(context) + + return context + + def generate_project(self, output, context, **kwargs): + super(RattailShopfooProjectGenerator, self).generate_project( + output, context, **kwargs) + + package = os.path.join(output, context['pkg_name']) + + ############################## + # db/model + ############################## + + db = os.path.join(package, 'db') + model = os.path.join(db, 'model') + + self.generate('package/db/model/shopfoo.py.mako', + os.path.join(model, '{}.py'.format(context['integration_pkgname'])), + context) diff --git a/rattail/projects/rattail_shopfoo/package/db/model/__init__.py.mako b/rattail/projects/rattail_shopfoo/package/db/model/__init__.py.mako new file mode 100644 index 00000000..dba86106 --- /dev/null +++ b/rattail/projects/rattail_shopfoo/package/db/model/__init__.py.mako @@ -0,0 +1,8 @@ +## -*- coding: utf-8; mode: python; -*- +# -*- coding: utf-8; -*- +""" +${name} data models +""" + +# bring in all models for ${integration_name} integration +from .${integration_pkgname} import ${integration_studly_prefix}Product, ${integration_studly_prefix}ProductExport diff --git a/rattail/projects/rattail_shopfoo/package/db/model/shopfoo.py.mako b/rattail/projects/rattail_shopfoo/package/db/model/shopfoo.py.mako new file mode 100644 index 00000000..afd973d8 --- /dev/null +++ b/rattail/projects/rattail_shopfoo/package/db/model/shopfoo.py.mako @@ -0,0 +1,57 @@ +## -*- coding: utf-8; mode: python; -*- +# -*- coding: utf-8; -*- +""" +Integration data models for ${integration_name} +""" + +import sqlalchemy as sa +from sqlalchemy.ext.declarative import declared_attr + +from rattail.db import model +from rattail.db.model.shopfoo import ShopfooProductBase, ShopfooProductExportBase + + +class ${integration_studly_prefix}Product(ShopfooProductBase, model.Base): + """ + ${integration_name} extensions to :class:`rattail:rattail.db.model.Product`. + """ + __tablename__ = '${integration_pkgname}_product' + + @declared_attr + def __table_args__(cls): + return cls.__product_table_args__() + ( + sa.UniqueConstraint('${integration_pkgname}_id', name='${integration_pkgname}_product_uq_${integration_pkgname}_id'), + ) + + __versioned__ = { + # 'exclude': [ + # 'in_stock', + # 'last_sold', + # 'last_updated', + # 'units_on_hand', + # ], + } + + ${integration_pkgname}_id = sa.Column(sa.String(length=25), nullable=False) + + description = sa.Column(sa.String(length=512), nullable=True) + + # price = sa.Column(sa.Numeric(precision=13, scale=2), nullable=True) + + # in_stock = sa.Column(sa.Boolean(), nullable=True) + + # last_sold = sa.Column(sa.Date(), nullable=True) + + # last_updated = sa.Column(sa.Date(), nullable=True) + + # units_on_hand = sa.Column(sa.Numeric(precision=13, scale=2), nullable=True) + + def __str__(self): + return self.description or "" + + +class ${integration_studly_prefix}ProductExport(ShopfooProductExportBase, model.Base): + """ + History table for product exports which have been submitted to ${integration_name} + """ + __tablename__ = '${integration_pkgname}_product_export' diff --git a/rattail/util.py b/rattail/util.py index faa3ea98..17d10f89 100644 --- a/rattail/util.py +++ b/rattail/util.py @@ -28,6 +28,7 @@ import collections import importlib import re import shlex +import unicodedata import datetime import decimal import subprocess @@ -166,6 +167,32 @@ def get_class_hierarchy(klass, topfirst=True): return hierarchy +def get_package_name(name): + """ + Generic logic to derive a "package name" from the given name. + + E.g. if ``name`` is "Poser Plus" this will return "poser_plus" + """ + # cf. https://stackoverflow.com/a/3194567 + name = unicodedata.normalize('NFD', name) + name = name.encode('ascii', 'ignore').decode('ascii') + words = re.split(r'[\- ]', name) + return '_'.join([word.lower() for word in words]) + + +def get_studly_prefix(name): + """ + Generic logic to derive a "studly prefix" from the given name. + + E.g. if ``name`` is "Poser Plus" this will return "PoserPlus" + """ + # cf. https://stackoverflow.com/a/3194567 + name = unicodedata.normalize('NFD', name) + name = name.encode('ascii', 'ignore').decode('ascii') + words = re.split(r'[\- ]', name) + return ''.join([word.capitalize() for word in words]) + + def import_module_path(module_path): """ Import an arbitrary Python module. diff --git a/setup.py b/setup.py index 66ae1d2e..b8148b93 100644 --- a/setup.py +++ b/setup.py @@ -272,6 +272,7 @@ setup( 'fabric = rattail.projects.fabric:FabricProjectGenerator', 'rattail = rattail.projects.rattail:RattailProjectGenerator', 'rattail_integration = rattail.projects.rattail_integration:RattailIntegrationProjectGenerator', + 'rattail_shopfoo = rattail.projects.rattail_shopfoo:RattailShopfooProjectGenerator', 'tailbone_integration = rattail.projects.tailbone_integration:TailboneIntegrationProjectGenerator', ],