Source code for rattail.commands.base

# -*- coding: utf-8; -*-
################################################################################
#
#  Rattail -- Retail Software Framework
#  Copyright © 2010-2024 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/>.
#
################################################################################
"""
Console Commands - base classes and core commands
"""

import os
import sys
import json
import argparse
import datetime
import warnings
import logging

from wuttjamaican.cmd.base import (Command as WuttaCommand,
                                   CommandArgumentParser,
                                   Subcommand as WuttaSubcommand)
from wuttjamaican.util import parse_list

from rattail import __version__
from rattail.progress import ConsoleProgress, SocketProgress
from rattail.config import make_config
from rattail.db.config import configure_versioning
from rattail.commands.typer import make_typer


rattail_typer = make_typer(
    name='rattail',
    help="Rattail Software Framework"
)


[docs] class ArgumentParser(CommandArgumentParser): """ Custom argument parser. This is a compatibility wrapper around upstream :class:`wuttjamaican:wuttjamaican.commands.base.CommandArgumentParser`. New code should use that instead; this will eventually be removed. """ def __init__(self, *args, **kwargs): warnings.warn("the custom ArgumentParser in rattail is deprecated; " "please use the one from wuttjamaican instead", DeprecationWarning, stacklevel=2) super().__init__(*args, **kwargs)
[docs] def date_argument(string): """ Validate and coerce a date argument. This function is designed be used as the ``type`` parameter when calling ``ArgumentParser.add_argument()``, e.g.:: parser = ArgumentParser() parser.add_argument('--date', type=date_argument) """ try: date = datetime.datetime.strptime(string, '%Y-%m-%d').date() except ValueError: raise argparse.ArgumentTypeError("Date must be in YYYY-MM-DD format") return date
[docs] def dict_argument(string): """ Coerce the given string to a Python dictionary. The string is assumed to be JSON-encoded; this function merely invokes ``json.loads()`` on it. This function is designed be used as the ``type`` parameter when calling ``ArgumentParser.add_argument()``, e.g.:: parser = ArgumentParser() parser.add_argument('--date', type=dict_argument) """ try: return json.loads(string) except json.decoder.JSONDecodeError: raise argparse.ArgumentTypeError("Argument must be valid JSON-encoded string")
[docs] def list_argument(string): """ Coerce the given string to a list of strings, splitting on whitespace and/or commas. This function is designed be used as the ``type`` parameter when calling ``ArgumentParser.add_argument()``, e.g.:: parser = ArgumentParser() parser.add_argument('--things', type=list_argument) """ return parse_list(string)
[docs] class RattailCommand(WuttaCommand): """ The primary command for Rattail. This inherits from :class:`wuttjamaican:wuttjamaican.commands.base.Command` so see those docs for more info. Custom apps based on Rattail will probably want to make and register their own ``Command`` class derived from this one. Again see upstream docs for more details. Rattail extends the upstream class by adding the following: """ name = 'rattail' version = __version__ description = "Rattail Software Framework" @property def db_config_section(self): """ Name of section in config file which should have database connection info. This defaults to ``'rattail.db'`` but may be overridden so the command framework can sit in front of a non-Rattail database if needed. This is used to auto-configure a "default" database engine for the app, when any command is invoked. """ # TODO: surely this can be more dynamic? or is it really being used? return 'rattail.db' @property def db_session_factory(self): """ Returns a reference to the configured session factory. This is a compatibility wrapper around :meth:`rattail.app.AppHandler.make_session()`. New code should use that instead; this may eventually be removed. """ return self.config.get_app().make_session @property def db_model(self): """ Returns a reference to configured model module. This is a compatibility wrapper around :meth:`rattail.config.RattailConfig.get_model()`. New code should use that instead; this may eventually be removed. """ return self.config.get_model()
[docs] def iter_subcommands(self): """ Returns a generator for the subcommands, sorted by name. This should probably not be used; instead see upstream :meth:`wuttjamaican:wuttjamaican.commands.base.Command.sorted_subcommands()`. """ for subcmd in self.sorted_subcommands(): yield subcmd
[docs] def add_args(self): """ Configure args for the main command arg parser. Rattail extends the upstream :meth:`~wuttjamaican:wuttjamaican.commands.base.Command.add_args()` by adding various command line args which have traditionally been available for it. Some of these may disappear some day but no changes are planned just yet. """ super().add_args() parser = self.parser # TODO: i think these aren't really being used in practice..? parser.add_argument('-n', '--no-init', action='store_true', default=False) parser.add_argument('--no-extend-config', dest='extend_config', action='store_false') parser.add_argument('--verbose', action='store_true') parser.add_argument('--progress-socket', help="Optional socket (e.g. localhost:8487) to which progress info should be written.") parser.add_argument('--runas', '-R', metavar='USERNAME', help="Optional username to impersonate when running the command. " "This is only relevant for / used by certain commands.") # data versioning parser.add_argument('--versioning', action='store_true', help="Force *enable* of data versioning. If set, then --no-versioning " "cannot also be set. If neither is set, config will determine whether " "or not data versioning should be enabled.") parser.add_argument('--no-versioning', action='store_true', help="Force *disable* of data versioning. If set, then --versioning " "cannot also be set. If neither is set, config will determine whether " "or not data versioning should be enabled.")
[docs] def make_config(self, args): """ Make the config object in preparation for running a subcommand. See also upstream :meth:`~wuttjamaican:wuttjamaican.commands.base.Command.make_config()` but for now, Rattail overrides this completely, mostly for the sake of versioning setup. """ # TODO: can we make better args so this is handled by argparse somehow? if args.versioning and args.no_versioning: self.stderr.write("Cannot pass both --versioning and --no-versioning\n") sys.exit(1) # if args say not to "init" then we make a sort of empty config if args.no_init: config = make_config([], extend=False, versioning=False) else: # otherwise we make a proper config, and maybe turn on versioning logging.basicConfig() config = make_config(args.config_paths, plus_files=args.plus_config_paths, extend=args.extend_config, versioning=False) if args.versioning: configure_versioning(config, force=True) elif not args.no_versioning: configure_versioning(config) # import our primary data model now, just in case it hasn't fully been # imported yet. this it to be sure association proxies and the like # are fully wired up in the case of extensions # TODO: what about web apps etc.? i guess i was only having the problem # for some importers, e.g. basic CORE API -> Rattail w/ the schema # extensions in place from rattail-corepos try: config.get_model() except ImportError: pass return config
[docs] def prep_subcommand(self, subcmd, args): """ Rattail overrides this method to apply some of the global args directly to the subcommand object. See also upstream :meth:`~wuttjamaican:wuttjamaican.commands.base.Command.prep_subcommand()`. """ # figure out if/how subcommand should show progress subcmd.show_progress = args.progress subcmd.progress = None if subcmd.show_progress: if args.progress_socket: host, port = args.progress_socket.split(':') subcmd.progress = SocketProgress(host, int(port)) else: subcmd.progress = ConsoleProgress # maybe should be verbose subcmd.verbose = args.verbose # TODO: make this default to something from config? subcmd.runas_username = args.runas or None
# TODO: deprecate / remove this? Command = RattailCommand
[docs] class RattailSubcommand(WuttaSubcommand): """ Base class for subcommands. This inherits from :class:`wuttjamaican.commands.base.Subcommand` so see those docs for more info. Rattail extends the subcommand to include: .. attribute:: runas_username Username (:attr:`~rattail.db.model.users.User.username`) corresponding to the :class:`~rattail.db.model.users.User` which the command should "run as" - i.e. for sake of version history etc. .. attribute:: show_progress Boolean indicating whether progress should be shown for the subcommand. .. attribute:: progress Optional factory to be used when creating progress objects. This is ``None`` by default but if :attr:`show_progress` is enabled, then :class:`~rattail.progress.ConsoleProgress` is the default factory. .. attribute:: verbose Flag indicating the subcommand should be free to print arbitrary messages to :attr:`~wuttjamaican:wuttjamaican.commands.base.Subcommand.stdout`. """ runas_username = None show_progress = False progress = None verbose = False def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # TODO: deprecate / remove this self.parent = self.command
[docs] def add_args(self): """ This is the "same" as upstream :meth:`wuttjamaican:wuttjamaican.commands.base.Subcommand.add_args()` except Rattail must customize this to also invoke its deprecated method, :meth:`add_parser_args()`. """ super().add_args() self.add_parser_args(self.parser)
[docs] def add_parser_args(self, parser): """ This method is deprecated and will eventually be removed; please define :meth:`add_args()` instead. """
@property def model(self): return self.parent.db_model def make_session(self): session = self.parent.db_session_factory() user = self.get_runas_user(session=session) if user: session.set_continuum_user(user) return session
[docs] def finalize_session(self, *args, **kwargs): """ Wrap up the given session, per the given arguments. This is meant to provide a simple convenience, for commands which must do work within a DB session, but would like to support a "dry run" mode. """ from rattail.db.util import finalize_session return finalize_session(*args, **kwargs)
[docs] def get_runas_user(self, session=None, username=None): """ Convenience method to get the "runas" User object for the current command. Uses :meth:`rattail.app.AppHandler.get_runas_user()` under the hood, but the ``--runas`` command line param provides the default username. """ if not username: username = getattr(self, 'runas_username', None) return self.app.get_runas_user(session=session, username=username)
def progress_loop(self, func, items, factory=None, **kwargs): return self.app.progress_loop(func, items, factory or self.progress, **kwargs) # TODO: deprecate / remove this def get_pip_path(self): return os.path.join(sys.prefix, 'bin', 'pip') def require_prompt_toolkit(self): from rattail.commands.util import require_prompt_toolkit return require_prompt_toolkit(self.config) def require_rich(self): from rattail.commands.util import require_rich return require_rich(self.config) def rprint(self, *args, **kwargs): from rattail.commands.util import rprint return rprint(*args, **kwargs) def basic_prompt(self, *args, **kwargs): from rattail.commands.util import basic_prompt return basic_prompt(*args, **kwargs)
# TODO: deprecate / remove this? Subcommand = RattailSubcommand
[docs] def main(*args): """ The primary entry point for the Rattail command system. """ if args: args = list(args) else: args = sys.argv[1:] cmd = Command() cmd.run(*args)