3
0
Fork 0

feat: add wutta top-level command with make-uuid subcommand

i think it only makes sense to have an "opinion" for command line
interface in this project, and we probably need more `wutta`
subcommands too but we'll see.

main motivation for this currently is to allow poser apps to define
their own CLI, in particular e.g. `poser install`
This commit is contained in:
Lance Edgar 2024-11-23 11:48:28 -06:00
parent cb147c203d
commit 2deba45588
20 changed files with 446 additions and 47 deletions

View file

@ -0,0 +1,6 @@
``wuttjamaican.cli.base``
=========================
.. automodule:: wuttjamaican.cli.base
:members:

View file

@ -0,0 +1,6 @@
``wuttjamaican.cli.make_uuid``
==============================
.. automodule:: wuttjamaican.cli.make_uuid
:members:

View file

@ -0,0 +1,6 @@
``wuttjamaican.cli``
====================
.. automodule:: wuttjamaican.cli
:members:

View file

@ -9,6 +9,9 @@
app
auth
cli
cli.base
cli.make_uuid
conf
db
db.conf

View file

@ -163,8 +163,8 @@ Glossary
subcommand
A top-level :term:`command` may expose one or more subcommands,
for the overall command line interface. Subcommands are usually
the real workhorse; each can perform a different function. See
also :doc:`narr/cli/index`.
the real workhorse; each can perform a different function with a
custom arg set. See also :doc:`narr/cli/index`.
virtual environment
This term comes from the broader Python world and refers to an

View file

@ -22,8 +22,10 @@ Features
* flexible configuration, using config files and/or DB settings table
* flexible architecture, abstracting various portions of the overall app
* flexible command line interface, using `Typer`_
* flexible database support, using `SQLAlchemy`_
.. _Typer: https://typer.tiangolo.com
.. _SQLAlchemy: https://www.sqlalchemy.org
See also these projects which build on WuttJamaican:

41
docs/narr/cli/builtin.rst Normal file
View file

@ -0,0 +1,41 @@
Built-in Commands
=================
WuttJamaican comes with one top-level :term:`command`, and some
:term:`subcommands<subcommand>`.
It uses `Typer`_ for the underlying CLI framework.
.. _Typer: https://typer.tiangolo.com/
``wutta``
---------
This is the top-level command. Its purpose is to expose subcommands
pertaining to WuttJamaican.
It is installed to the virtual environment in the ``bin`` folder (or
``Scripts`` on Windows):
.. code-block:: sh
cd /path/to/venv
bin/wutta --help
Defined in: :mod:`wuttjamaican.cli`
.. program-output:: wutta --help
.. _wutta-make-uuid:
``wutta make-uuid``
-------------------
Print a new universally-unique identifier to standard output.
Defined in: :mod:`wuttjamaican.cli.make_uuid`
.. program-output:: wutta make-uuid --help

View file

@ -1,23 +0,0 @@
Commands
========
WuttJamaican in fact does not directly provide a way to define a
command line interface for your app.
The reason is that several good frameworks exist already. You are
encouraged to use one of the following to define
:term:`commands<command>` and :term:`subcommands<subcommand>` as
needed:
* `Typer <https://typer.tiangolo.com/>`_
* `Click <https://click.palletsprojects.com/en/latest/>`_
* :mod:`python:argparse`
For even more options see:
* `awesome-cli-framework <https://github.com/shadawck/awesome-cli-frameworks/blob/master/README.md#python>`_
* `Hitchhikers Guide to Python <https://docs.python-guide.org/scenarios/cli/>`_
* `Python Wiki <https://wiki.python.org/moin/CommandlineTools>`_
Or if that is overkill you can always just use :doc:`scripts`.

105
docs/narr/cli/custom.rst Normal file
View file

@ -0,0 +1,105 @@
Custom Commands
===============
WuttJamaican comes with :doc:`/narr/cli/builtin`.
Using the same framework, each :term:`package` can define additional
top-level :term:`command(s)<command>` and
:term:`subcommands<subcommand>` as needed.
Top-Level Command
-----------------
You must "define" *and* "register" your top-level command. Assuming a
basic Poser example:
.. code-block:: none
poser-project
├── poser
│ ├── __init__.py
│ └── cli.py
└── pyproject.toml
Add the command definition to the ``poser.cli`` module::
from wuttjamaican.cli import make_typer
poser_typer = make_typer(
name='poser',
help="Poser - the killer app"
)
Then register the command as script in ``pyproject.toml``:
.. code-block:: toml
[project.scripts]
poser = "poser.cli:poser_typer"
Then reinstall your project:
.. code-block:: sh
pip install -e ~/src/poser
And now you can run your command:
.. code-block:: sh
poser --help
But it won't really do anything until you add subcommands.
Subcommands
-----------
You must "define" the subcommand of course, but do not need to
"register" it. (That happens via function decorator; see below.)
However you *do* need to ensure all modules containing subcommands are
"eagerly imported" so the runtime discovery process finds everything.
Here we'll define the ``poser hello`` subcommand, by adding it to our
``poser.cli`` module (from example above)::
import sys
import typer
from wuttjamaican.cli import make_typer
# top-level command
poser_typer = make_typer(
name='poser',
help="Poser - the killer app"
)
# nb. function decorator will auto-register the subcommand
@poser_typer.command()
def hello(
ctx: typer.Context,
):
"""
Hello world example
"""
config = ctx.parent.wutta_config
app = config.get_app()
name = config.get('hello.name', default="WhoAreYou")
sys.stdout.write(f'hello {name}\n')
title = app.get_title()
sys.stdout.write(f'from {title}\n')
# TODO: you may need to import other modules here, if they contain
# subcommands and would not be automatically imported otherwise.
# nb. *this* current module *is* automatically imported, only
# because of the top-level command registration in pyproject.toml
No need to re-install, you can now use the subcommand:
.. code-block:: sh
poser hello --help

View file

@ -2,9 +2,24 @@
Command Line Interface
======================
Most apps will need some sort of command line usage, via cron or
otherwise. There are two main aspects to it:
There is a proper CLI framework based on `Typer`_, with top-level
:term:`commands<command>` and :term:`subcommands<subcommand>`. The
``wutta`` command is built-in and includes some subcommands, but each
app can define more of either as needed. Such (sub)commands are
installed as part of a :term:`package`.
.. _Typer: https://typer.tiangolo.com
But sometimes you just need an :term:`ad hoc script` which is a single
file and can be placed anywhere, usually *not* installed as part of a
package.
.. toctree::
:maxdepth: 2
overview
commands
builtin
custom
scripts

View file

@ -1,15 +0,0 @@
Overview
========
Many apps will need some sort of command line usage, via cron or
otherwise. There are two main aspects to it:
First there is the :term:`ad hoc script` which is a single file and
can be placed anywhere, but is not installed as part of a
:term:`package`. See :doc:`scripts`.
But a "true" command line interface may define
:term:`commands<command>` and :term:`subcommands<subcommand>`, which
are then installed as part of a package. See :doc:`commands` for more
about that.

View file

@ -33,7 +33,7 @@ Run that like so:
Better Standards
----------------
~~~~~~~~~~~~~~~~
Keeping it simple, but improving that script per recommended patterns::
@ -69,7 +69,7 @@ that this also gives you access to the :term:`app handler`::
def hello(config):
app = config.get_app()
print('hello', config.get('hello.name'))
print('from', app.appname)
print('from', app.get_title())
if __name__ == '__main__':
config = make_config('my.conf')
@ -81,7 +81,7 @@ Output should now be different:
$ python hello.py
hello George
from wutta
from WuttJamaican
You are likely to need more imports; it is generally wise to do those
*within the function* as opposed to the top of the module. This is to
@ -97,7 +97,7 @@ before all packages are imported::
app = config.get_app()
print('hello', config.get('hello.name'))
print('from', app.appname)
print('from', app.get_title())
something(config)
@ -143,7 +143,7 @@ Here is the script with logging incorporated::
log.debug("saying hello")
app = config.get_app()
print('hello', config.get('hello.name'))
print('from', app.appname)
print('from', app.get_title())
log.debug("about to do something")
if something(config):