From e6a05c33265365448c985d163cf32af5c0811ed5 Mon Sep 17 00:00:00 2001 From: Oliver Lambson Date: Thu, 22 Aug 2024 10:50:30 +0200 Subject: [PATCH] bored-charts router --- README.md | 2 ++ bored-charts/README.md | 9 ++++----- bored-charts/boredcharts/__init__.py | 4 +++- bored-charts/boredcharts/jinja.py | 12 ++++++------ bored-charts/boredcharts/router.py | 16 ++++++++++++++++ examples/full/bcexample/figures.py | 14 +++++--------- examples/minimal/main.py | 9 ++++----- 7 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 bored-charts/boredcharts/router.py diff --git a/README.md b/README.md index 73b9e3b..4658aa0 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Build easy, minimal, PDF-able data reports with markdown and python. - [x] separate pyproject.toml for lib and example - [x] publish to pypi - [ ] less boilerplate to register figure endpoints + - [x] @bored_router.chart decorator + - [ ] allow decoration of functions that return figures directly - [ ] make report_name path parameter optional - [ ] deploy to [bored-charts-example.oliverlambson.com](https://bored-charts-example.oliverlambson.com) - [ ] dashboard layout with grid layout diff --git a/bored-charts/README.md b/bored-charts/README.md index dca90a3..5273b50 100644 --- a/bored-charts/README.md +++ b/bored-charts/README.md @@ -22,17 +22,16 @@ pip install bored-charts uvicorn from pathlib import Path import plotly.express as px -from boredcharts import boredcharts +from boredcharts import BCRouter, boredcharts from boredcharts.jinja import to_html -from fastapi import APIRouter from fastapi.responses import HTMLResponse pages = Path(__file__).parent.absolute() / "pages" -figure_router = APIRouter() +figure_router = BCRouter() -@figure_router.get("/report/{report_name}/figure/usa_population", name="usa_population") -async def usa_population(report_name: str) -> HTMLResponse: +@figure_router.chart("usa_population") +async def usa_population() -> HTMLResponse: df = px.data.gapminder().query("country=='United States'") fig = px.bar(df, x="year", y="pop") return HTMLResponse(to_html(fig)) diff --git a/bored-charts/boredcharts/__init__.py b/bored-charts/boredcharts/__init__.py index dfa759a..38e66d8 100644 --- a/bored-charts/boredcharts/__init__.py +++ b/bored-charts/boredcharts/__init__.py @@ -1,7 +1,9 @@ -__version__ = "0.1.4" +__version__ = "0.2.0" +from boredcharts.router import BCRouter from boredcharts.webapp import boredcharts __all__ = [ + "BCRouter", "boredcharts", ] diff --git a/bored-charts/boredcharts/jinja.py b/bored-charts/boredcharts/jinja.py index 8859b0d..202c4ce 100644 --- a/bored-charts/boredcharts/jinja.py +++ b/bored-charts/boredcharts/jinja.py @@ -93,7 +93,7 @@ def figure( figure: str, *, css_class: str = "min-h-112 min-w-80", - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Markup: """Jinja function to display a figure. @@ -104,10 +104,10 @@ def figure( {{ figure("example_figure_with_params", param_1="foo") }} """ report = context.resolve("report") - if isinstance(report, Undefined): - raise ValueError("report is not available in the context") - if not isinstance(report, str): - raise ValueError(f"report must be a string, got {type(report)}") + if not isinstance(report, Undefined): + if not isinstance(report, str): + raise ValueError(f"report must be a string, got {type(report)}") + kwargs.update(report_name=report) request = context.resolve("request") if isinstance(request, Undefined): @@ -115,7 +115,7 @@ def figure( if not isinstance(request, Request): raise ValueError(f"request must be a Request, got {type(request)}") - url = request.url_for(figure, report_name=report).include_query_params(**kwargs) + url = request.url_for(figure).include_query_params(**kwargs) # note using dedent to return a valid root-level element return Markup( diff --git a/bored-charts/boredcharts/router.py b/bored-charts/boredcharts/router.py new file mode 100644 index 0000000..266e08f --- /dev/null +++ b/bored-charts/boredcharts/router.py @@ -0,0 +1,16 @@ +from collections.abc import Callable + +from fastapi import APIRouter +from fastapi.types import DecoratedCallable + + +class BCRouter(APIRouter): + def chart( + self, + name: str, + ) -> Callable[[DecoratedCallable], DecoratedCallable]: + path = f"/figure/{name}" + return self.api_route( + path=path, + name=name, + ) diff --git a/examples/full/bcexample/figures.py b/examples/full/bcexample/figures.py index 4456e66..66d7e5c 100644 --- a/examples/full/bcexample/figures.py +++ b/examples/full/bcexample/figures.py @@ -2,12 +2,12 @@ import matplotlib.pyplot as plt import numpy as np import plotly.express as px +from boredcharts import BCRouter from boredcharts.jinja import to_html -from fastapi import APIRouter from fastapi.responses import HTMLResponse from plotly.graph_objects import Figure -router = APIRouter() +router = BCRouter() async def example(report_name: str, country: str) -> Figure: @@ -31,14 +31,12 @@ async def example(report_name: str, country: str) -> Figure: # TODO: pass functions into framework, auto generate these routes -@router.get( - "/report/{report_name}/figure/example_simple_usa", name="example_simple_usa" -) +@router.chart("example_simple_usa") async def fig_example_simple(report_name: str) -> HTMLResponse: return HTMLResponse(to_html(await example(report_name, "United States"))) -@router.get("/report/{report_name}/figure/example_params", name="example_params") +@router.chart("example_params") async def fig_example(report_name: str, country: str) -> HTMLResponse: return HTMLResponse(to_html(await example(report_name, country))) @@ -101,9 +99,7 @@ async def elasticity_vs_profit( # TODO: pass functions into framework, auto generate these routes -@router.get( - "/report/{report_name}/figure/elasticity_vs_profit", name="elasticity_vs_profit" -) +@router.chart("elasticity_vs_profit") async def fig_elasticity_vs_profit( report_name: str, margin: float | None = None ) -> HTMLResponse: diff --git a/examples/minimal/main.py b/examples/minimal/main.py index b160a8c..13773bc 100644 --- a/examples/minimal/main.py +++ b/examples/minimal/main.py @@ -1,17 +1,16 @@ from pathlib import Path import plotly.express as px -from boredcharts import boredcharts +from boredcharts import BCRouter, boredcharts from boredcharts.jinja import to_html -from fastapi import APIRouter from fastapi.responses import HTMLResponse pages = Path(__file__).parent.absolute() / "pages" -figure_router = APIRouter() +figure_router = BCRouter() -@figure_router.get("/report/{report_name}/figure/usa_population", name="usa_population") -async def usa_population(report_name: str) -> HTMLResponse: +@figure_router.chart("usa_population") +async def usa_population() -> HTMLResponse: df = px.data.gapminder().query("country=='United States'") fig = px.bar(df, x="year", y="pop") return HTMLResponse(to_html(fig))