Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typer-web to run typer cli as a web server (like click-web did) #433

Open
7 tasks done
axiomquant opened this issue Jul 20, 2022 · 5 comments
Open
7 tasks done

typer-web to run typer cli as a web server (like click-web did) #433

axiomquant opened this issue Jul 20, 2022 · 5 comments
Labels
feature New feature, enhancement or request

Comments

@axiomquant
Copy link

axiomquant commented Jul 20, 2022

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to Typer but to Click.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

import typer
from typer_web import create_fastapi_web_app

app = typer.Typer()

@create_fastapi_web_app
@app.command
def main(name: str):
    print(f"Hello {name}")


if __name__ == "__main__":
    app()

Description

Is it possible to create a typer-web module, which supports both the typer cli mode and the fastapi server mode? Click-web project https://github.com/fredrik-corneliusson/click-web can be used to generate a web app for click cli in a very simple way. It'd be great if typer can provide such a tool that supports both cli mode and web-server mode transparently.

Wanted Solution

As the description.

Wanted Code

NA

Alternatives

No response

Operating System

Linux

Operating System Details

No response

Typer Version

0.6.1

Python Version

3.7

Additional Context

No response

@axiomquant axiomquant added the feature New feature, enhancement or request label Jul 20, 2022
@rspring-oncai
Copy link

This would be a great feature to have, or alternatively some official guidance on how to use typer and fastapi at the same time (they are siblings, but do they get along?)

@zsiegel92
Copy link

This is what Hug does, which the FastAPI documentation mentions. Hug supports using the same function for both a Web API and a CLI (the following is from their docs):

"""An example of writing an API to scrape hacker news once, and then enabling usage everywhere"""
import hug
import requests


@hug.local()
@hug.cli()
@hug.get()
def top_post(section: hug.types.one_of(('news', 'newest', 'show'))='news'):
    """Returns the top post from the provided section"""
    content = requests.get('https://news.ycombinator.com/{0}'.format(section)).content
    text = content.decode('utf-8')
    return text.split('<tr class=\'athing\'>')[1].split("<a href")[1].split(">")[1].split("<")[0]

So, the question is really, can you do this with Typer and FastAPI? I think yes:

from typer import Typer
from fastapi import FastAPI

cli_app = Typer()
webapp = FastAPI()


@webapp.get("/hello/{name}")
@cli_app.command()
def hello(name: str):
    print(f"Hello {name}")


@webapp.get("/goodbye/{name}")
@cli_app.command()
def goodbye(name: str, formal: bool = False):
    if formal:
        print(f"Goodbye Ms. {name}. Have a good day.")
    else:
        print(f"Bye {name}!")


if __name__ == "__main__":
    cli_app()

There are two ways to use this file, which let's say is called hybrid.py:

  • If the file above is run with python -m uvicorn hybrid:webapp, running this works as a FastAPI application.
  • If the file above is run with python hybrid.py hello --help, it works as a Typer application and we get:
                                                                                              
 Usage: hybrid.py hello [OPTIONS] NAME                                                        
                                                                                              
╭─ Arguments ────────────────────────────────────────────────────────────────────────────────╮
│ *    name      TEXT  [default: None] [required]                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ──────────────────────────────────────────────────────────────────────────────────╮
│ --help          Show this message and exit.                                                │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

@zsiegel92
Copy link

Since the type annotations contain most of the business logic, not the decorators, even a pretty complicated Typer or FastAPI configuration should work with minimal repeated logic; a few exceptions of course, like if both the FastAPI endpoint and Typer command had to be deprecated, that would have be be specified in both decorators.

To ensure there is really a single source of truth for every shared behavior of these two interfaces, someone would need to write a single class that handles both.

The single class - HybridAPI, say - would need to follow the ASGI spec (Uvicorn docs), which means its __call__ method would have to have the signature __call__(self, scope, receive, send):.... This rules out the possibility of using the documented Typer API if __name__=="__main__": app().

Minimal working example of getting around that issue:

from typer import Typer
from fastapi import FastAPI

class HybridAPI(FastAPI):

    def __init__(self, *args, typer_args=[], typer_kwargs={}, **kwargs):
        self.cli_app = Typer(*typer_args, **typer_kwargs)
        super().__init__(*args, **kwargs)

    def get(self, *args, typer_args=[], typer_kwargs={}, **kwargs):
        fastAPIGet = super().get(*args, **kwargs)
        cliCommand = self.cli_app.command(*typer_args, **typer_kwargs)
        return lambda func: fastAPIGet(cliCommand(func))


app = HybridAPI()


@app.get("/hello/{name}")
def hello(name: str):
    print(f"Hello {name}")


@app.get("/goodbye/{name}")
def goodbye(name: str, formal: bool = False):
    if formal:
        print(f"Goodbye Ms. {name}. Have a good day.")
    else:
        print(f"Bye {name}!")


if __name__ == "__main__":
    app.cli_app()

All Typer and FastAPI functionality works. Of course, this only implements get (omitting post, put, etc.), and the Typer.__call__ (or any other Typer method) must be called via the cli_app field. Sharing positional or keyword arguments for the two applications would have to be implemented in the Frankenstein decorator functions.

To be clear I think this is a bad idea, but it was interesting to think about! Using two decorators, with some repeated information between them, is a small price to pay for defining business logic once.

@zsiegel92
Copy link

zsiegel92 commented Dec 13, 2023

Typer- and FastAPI-specific type annotations should work just fine together:

import typer
import fastapi
...
@app.get('/test')
def test(
    boolparam: Annotated[bool, typer.Option(prompt="Are you sure?"), fastapi.Body()],
):
    ...

@Declow
Copy link

Declow commented Jul 12, 2024

For all of the people that stumbles on this issue.
Nothing new needs to be implemented! You can in fact use click-web.

Take your Typer() instance and do the following

commands.py

import typer

typer_app = typer.Typer()

main.py

from click_web import create_click_web_app
import commands
import typer

typer_app = typer.Typer()

cmd = typer.main.get_command(commands.typer_app)
cmd.name = "cli"
app = create_click_web_app(commands, cmd)

start.sh

export FLASK_ENV=development
export FLASK_APP=app.py
flask run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature, enhancement or request
Projects
None yet
Development

No branches or pull requests

4 participants