Skip to main content

A framework-agnostic management command system for Python apps (FastAPI, Flask, Sanic, Starlette, or plain Python) inspired by Django management commands.

Project description

Build

Cli Manager

A Python package that enables you to create and manage custom management commands, similar to Django's management system, for any Python application — FastAPI, Flask, Sanic, Starlette, or plain Python. This package uses Python's click to define, register, and execute commands for your application dynamically. Both synchronous and asynchronous commands are supported.

Features

  • Dynamic Command Registration: Automatically discover and register commands located in specific directories (recursively, optionally).
  • Class-Based Commands: Easily define reusable commands by subclassing BaseCommand (sync) or AsyncBaseCommand (async).
  • Decorator API: Register plain functions (sync or async) directly with @system.command().
  • Custom Arguments: Commands can specify their own arguments and options via Argument.positional(...) / Argument.option(...).
  • Lifecycle Hooks: setup() and teardown(exc) run before and after every command, even on exceptions.
  • Aliases, Help & Hidden Commands: Class-level aliases, help, short_help, hidden attributes for ergonomic CLIs.
  • Plugin Discovery: Register commands advertised by installed packages through cmd_manager.commands entry points.
  • Framework-agnostic: Drop into any Python app — FastAPI, Flask, Sanic, Starlette, or a plain script — and thread your app/context into commands via constructor args.

Installation

Install the package via pip:

pip install cmd-manager

Usage

1. Define a synchronous command

# src/scripts/mycommand.py
from cmd_manager import Argument, BaseCommand


class Command(BaseCommand):
    """Print the arguments passed in."""

    arguments = (
        Argument.positional("arg1"),
        Argument.option("--n", type=int, default=1),
    )

    def run(self, *args, **kwargs):
        print(f"Running with args: {args}, kwargs: {kwargs}")

Argument.positional wraps click.argument; Argument.option wraps click.option. Every click keyword (type, prompt, required, is_flag, multiple, ...) is forwarded verbatim.

2. Define an asynchronous command

# src/scripts/fetch.py
import click
from cmd_manager import Argument, AsyncBaseCommand


class Command(AsyncBaseCommand):
    """Fetch a URL asynchronously."""

    arguments = (Argument.option("--url", required=True),)

    async def setup(self):
        self.session = await open_session()

    async def run(self, *args, **kwargs):
        body = await self.session.get(kwargs["url"])
        click.echo(body)

    async def teardown(self, exc=None):
        await self.session.close()

The system detects async commands automatically and runs them on an event loop — setup, run, and teardown are all awaited.

3. Register and run

# cli_runner.py
from cmd_manager import ManagementCommandSystem

system = ManagementCommandSystem()
system.register(package="src.scripts")
cli = system.create_cli()

if __name__ == "__main__":
    cli()

Then:

python cli_runner.py mycommand arg1_value --n 3
python cli_runner.py fetch --url https://example.com
python cli_runner.py list   # built-in: prints every registered command

4. Recursive discovery & sub-packages

system.register(package="src.scripts", recursive=True)

src/scripts/users/create.py becomes the command users-create.

5. Prefixing third-party command packages

system.register(prefix="ext-", package="external_package.scripts")

Per-package constructor args (useful when the host app and a plugin expect different DI payloads). app can be any framework instance (FastAPI, Flask, Sanic, Starlette, ...) or any arbitrary object — ManagementCommandSystem is framework-agnostic and just forwards constructor args to each command:

system = ManagementCommandSystem(app=app)  # app is any object you want injected
system.register(package="external_package.scripts",
                init_kwargs={"app": app, "config": plugin_cfg})

6. Decorator API for one-off commands

from cmd_manager import Argument

@system.command("greet", arguments=[Argument.option("--name", default="world")])
def greet(name):
    """Say hello."""
    click.echo(f"hello {name}")


@system.command()  # name defaults to "fetch-once" (kebab-cased function name)
async def fetch_once(url):
    ...

7. Plugin discovery via entry points

A plugin package declares the command in its pyproject.toml:

[project.entry-points."cmd_manager.commands"]
greet = "my_plugin.scripts.greet:Command"

The host app then loads every installed plugin command with one call:

system.register_entry_points()

8. Command metadata

Any BaseCommand / AsyncBaseCommand subclass can set class-level attributes:

class Command(BaseCommand):
    """Long help text comes from the docstring by default."""

    name = "do-thing"            # override the discovered name
    short_help = "Do a thing."   # one-liner shown in `--help`
    aliases = ("dt",)             # alternate names (registered as hidden subcommands)
    hidden = False                # hide from `--help` listings

Example

See the example/ folder; run it with:

python example_runner.py whats_my_name
python example_runner.py list

Authors

@mhsiddiqui

Contributing

Contributions are always welcome! Please read CONTRIBUTING.md and adhere to the project's code of conduct.

Feedback and Support

Please open an issue and follow the template, so the community can help you.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cmd_manager-0.2.0.tar.gz (8.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cmd_manager-0.2.0-py3-none-any.whl (10.3 kB view details)

Uploaded Python 3

File details

Details for the file cmd_manager-0.2.0.tar.gz.

File metadata

  • Download URL: cmd_manager-0.2.0.tar.gz
  • Upload date:
  • Size: 8.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cmd_manager-0.2.0.tar.gz
Algorithm Hash digest
SHA256 8b8c90c30288b5c7b5c6bbcd4d1d9b2c029dbd255dd332de8ed9e93e57b95a27
MD5 98d8adff9c89cd34af8964c1f20a51c5
BLAKE2b-256 da362b0c1c916a67dec97374e69fc428e467784a38e48021837e996fb0aa38eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for cmd_manager-0.2.0.tar.gz:

Publisher: release.yml on mhsiddiqui/cmd-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cmd_manager-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: cmd_manager-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for cmd_manager-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8bb23ccaedfa500e79361cfaede42ba269408944ccab682c2602de9e9d18baab
MD5 957e9d15ac328e93a4a2190325cd4a82
BLAKE2b-256 6d5120bfee1cbf19e2c6e30cfc81b314d4fa3098b329e7e1cd1f13fd9d6c161d

See more details on using hashes here.

Provenance

The following attestation bundles were made for cmd_manager-0.2.0-py3-none-any.whl:

Publisher: release.yml on mhsiddiqui/cmd-manager

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page