Open axiomquant opened 2 years ago
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?)
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
:
python -m uvicorn hybrid:webapp
, running this works as a FastAPI application.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. │
╰────────────────────────────────────────────────────────────────────────────────────────────╯
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.
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()],
):
...
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
First Check
Commit to Help
Example Code
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
Alternatives
No response
Operating System
Linux
Operating System Details
No response
Typer Version
0.6.1
Python Version
3.7
Additional Context
No response