Add rattail_shopfoo project generator

also *remove* the `db/alembic/env.py` script from rattail-adjacent
generator.  it didn't seem necessary..now we'll see if it ever is
This commit is contained in:
Lance Edgar 2023-05-08 21:41:00 -05:00
parent 9834e1276d
commit 4c331e3875
8 changed files with 176 additions and 96 deletions

View file

@ -30,12 +30,11 @@ import re
import shutil import shutil
import string import string
import sys import sys
import unicodedata
import colander import colander
from mako.template import Template 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 from rattail.mako import ResourceTemplateLookup
@ -276,13 +275,6 @@ class PythonProjectGenerator(ProjectGenerator):
return schema 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): def normalize_context(self, context):
context = super(PythonProjectGenerator, self).normalize_context(context) context = super(PythonProjectGenerator, self).normalize_context(context)
@ -296,7 +288,7 @@ class PythonProjectGenerator(ProjectGenerator):
context['egg_name'] = context['pypi_name'].replace('-', '_') context['egg_name'] = context['pypi_name'].replace('-', '_')
if 'studly_prefix' not in context: 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: if 'env_name' not in context:
context['env_name'] = context['folder'] context['env_name'] = context['folder']
@ -478,14 +470,12 @@ class RattailAdjacentProjectGenerator(PythonProjectGenerator):
# model # model
#################### ####################
if context['has_model']: model = os.path.join(db, 'model')
os.makedirs(model)
model = os.path.join(db, 'model') self.generate('package/db/model/__init__.py.mako',
os.makedirs(model) os.path.join(model, '__init__.py'),
context)
self.generate('package/db/model/__init__.py.mako',
os.path.join(model, '__init__.py'),
context)
#################### ####################
# alembic # alembic
@ -496,11 +486,6 @@ class RattailAdjacentProjectGenerator(PythonProjectGenerator):
alembic = os.path.join(db, 'alembic') alembic = os.path.join(db, 'alembic')
os.makedirs(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') versions = os.path.join(alembic, 'versions')
os.makedirs(versions) os.makedirs(versions)

View file

@ -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()

View file

@ -29,6 +29,7 @@ import os
import colander import colander
from rattail.projects import RattailAdjacentProjectGenerator from rattail.projects import RattailAdjacentProjectGenerator
from rattail.util import get_studly_prefix, get_package_name
class RattailIntegrationProjectGenerator(RattailAdjacentProjectGenerator): class RattailIntegrationProjectGenerator(RattailAdjacentProjectGenerator):
@ -61,6 +62,14 @@ class RattailIntegrationProjectGenerator(RattailAdjacentProjectGenerator):
'Topic :: Software Development :: Libraries :: Python Modules', '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: if 'year' not in context:
context['year'] = self.app.today().year context['year'] = self.app.today().year

View file

@ -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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
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)

View file

@ -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

View file

@ -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'

View file

@ -28,6 +28,7 @@ import collections
import importlib import importlib
import re import re
import shlex import shlex
import unicodedata
import datetime import datetime
import decimal import decimal
import subprocess import subprocess
@ -166,6 +167,32 @@ def get_class_hierarchy(klass, topfirst=True):
return hierarchy 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): def import_module_path(module_path):
""" """
Import an arbitrary Python module. Import an arbitrary Python module.

View file

@ -272,6 +272,7 @@ setup(
'fabric = rattail.projects.fabric:FabricProjectGenerator', 'fabric = rattail.projects.fabric:FabricProjectGenerator',
'rattail = rattail.projects.rattail:RattailProjectGenerator', 'rattail = rattail.projects.rattail:RattailProjectGenerator',
'rattail_integration = rattail.projects.rattail_integration:RattailIntegrationProjectGenerator', 'rattail_integration = rattail.projects.rattail_integration:RattailIntegrationProjectGenerator',
'rattail_shopfoo = rattail.projects.rattail_shopfoo:RattailShopfooProjectGenerator',
'tailbone_integration = rattail.projects.tailbone_integration:TailboneIntegrationProjectGenerator', 'tailbone_integration = rattail.projects.tailbone_integration:TailboneIntegrationProjectGenerator',
], ],