cyclopts icon indicating copy to clipboard operation
cyclopts copied to clipboard

cyclopts and URL query params

Open liquidcarbon opened this issue 3 months ago • 7 comments

Hello Brian! Thanks a lot for your library! I like it a lot, definitely more than Typer. ;)

I've been somewhat obsessed with URL query parameters, as a way to steer code execution directly from browser URL. To that end, I've contributed some guides to marimo notebooks that describe how to use pydantic for query parameter models, including for turning marimo notebooks into a cyclopts CLI app.

There are obvious similarities in CLI and URL query params usage:

URL: baseurl/path?k1=v1&k2=v2
     ^^^^^^^ ^^^^ ^^^^^^^^^^^
CLI: COMMAND ARGS   OPTIONS

or app/command/args?options

Of course, web frameworks do a range of things that CLIs don't, and vice versa, but still...

Here's a question for you: what do you think about developing a web API based on cyclopts?

Possible use cases may be grouped into what came first, CLI or web app.

  1. CLI first
  • a) automatically expose CLI to web - an app could be autogenerated using minimalistic framework like microdot - I'm sort of slowly working on that
  • b) API docs from cyclopts' neat CLI help messaging - /path?help
  • c) controlled exposure of linux commands - e.g. wrappers around ls, find, cat | grep;
  • d) maybe remote server management from anywhere, using just browser and special URLs
  1. API first
  • a) automated testing of endpoints with their query params by calling CLI
  • b) debug on the server side using CLI (of course there's curl)
  • c) various possible AI ideas - local CLI for remote APIs that tolerate robotic requests

liquidcarbon avatar Oct 03 '25 00:10 liquidcarbon

Working example:

from cyclopts import App as CLIApp, Parameter
from dataclasses import dataclass
from microdot import Microdot, Request as MicrodotRequest
from microdot.test_client import TestClient
from pydantic import BaseModel
from typing import Annotated
import asyncio
import inspect

cli = CLIApp()


class Movie(BaseModel):
    title: str
    year: int = 2023


@cli.command
def add(movie: Annotated[Movie, Parameter(name="*")]):
    res = f"Adding movie: {movie}"
    return movie.model_dump()


@cli.command
def watch(movie: Annotated[Movie, Parameter(name="*")]):
    res = f"Loading movie: {movie}"
    return movie.model_dump()


return_value = cli.__call__(["add", "The Creator"])
print(f"CLI says: {return_value}")


class App(Microdot):
    """Microdot Web App derived automatically from Cyclopts CLI App."""

    @classmethod
    def from_cli_app(cls, cli_app: CLIApp):
        app = cls()
        _commands = {
            k: v for k, v in cli_app._commands.items() if not k.startswith("-")
        }
        for _name, _app in _commands.items():
            _fn = _app.default_command
            _fn_signature = inspect.signature(_fn)
            sub_app = Microdot()

            @sub_app.get("/")
            async def fn(request: MicrodotRequest):
                _args = [v[0] for k, v in request.args.items()]
                # probably needs something clever, starting with _fn_signature
                return cli_app([_name, *_args])

            app.mount(sub_app, url_prefix=f"/{_name}")

        return app

async def test():
    app = App.from_cli_app(cli)
    client = TestClient(app)
    response = await client.get("/watch/?title=The%20Creator")
    print(f"API says: {response.json}")

asyncio.run(test())

# output:
# CLI says: {'title': 'The Creator', 'year': 2023}
# API says: {'title': 'The Creator', 'year': 2023}

liquidcarbon avatar Oct 03 '25 03:10 liquidcarbon

Here's a question for you: what do you think about developing a web API based on cyclopts?

I'd certainly be interested! I don't have a ton of direct web experience, so I'd have to lean on others (like you!) on idiomaticness, best-practices, etc.

If you're willing to work on it, I'd be more than happy to help support! I can answer any specific questions, the best way to go about parsing items, etc etc. If you are up for it, definitely start development based on the v4-develop branch so that we don't run into difficult merge conflicts in the future.

To initially point you in the correct direction, I think you would be most interested in App.assemble_argument_collection, and then also the corresponding ArgumentCollection.match. We'd probably have to slightly tweak the match capabilities. Please try and take a look! I'm mostly focusing on getting v4 out the door, which will probably happen in 1~2 weeks. So if you have any breaking proposed changes, now would be a good time! Otherwise they may have to wait for a v5. However, I imagine most changes would inherently be backwards compatible because we still want Cyclopts to be a cli-first library.

BrianPugh avatar Oct 03 '25 19:10 BrianPugh

Is there a summary of what's new in v4?

I definitely don't program at your level but I'll give it a go!

Also, curious to hear from @miguelgrinberg

liquidcarbon avatar Oct 04 '25 00:10 liquidcarbon

The current WIP Changelog can be scene at the bottom of this post.

BrianPugh avatar Oct 04 '25 12:10 BrianPugh

Also, curious to hear from @miguelgrinberg

I'm actually more interested in the reverse path, which is to generate CLI clients for web APIs.

miguelgrinberg avatar Oct 04 '25 13:10 miguelgrinberg

I'm actually more interested in the reverse path, which is to generate CLI clients for web APIs.

@miguelgrinberg do you wish for a general solution, or for a specific type of apps? what's an example or two of an app for which you wish to have a CLI?

I'm picturing something model-centric, where endpoints (either kind) are methods on a model. But the functions for CLI and API must be a little different, the actual functions under @app.route / @app.command might look similar to FastAPI's def get_foo(model: Annotated[Model, Query()]): - the annotations point to what we do with the model.

liquidcarbon avatar Oct 06 '25 18:10 liquidcarbon

what's an example or two of an app for which you wish to have a CLI?

Nothing specific in mind. This is a pattern that applies to all APIs. It is often convenient to offer a CLI that can make calls, so that you don't have to use curl or similar primitive clients. It's a different use case than yours, I think.

miguelgrinberg avatar Oct 06 '25 22:10 miguelgrinberg