Add charts

Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
Andras Schmelczer 2022-04-10 19:52:04 +02:00
parent 282a066bb5
commit 4b92a75841
9 changed files with 163 additions and 93 deletions

View file

@ -1,12 +1,20 @@
from typing import Any, Dict, List
from fastapi import FastAPI, status
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import RedirectResponse
from starlette.responses import HTMLResponse
from ..context import get_context
from ..helper import snake_case_to_text
from ..views import HealthCheckResponse
from ..metrics import create_dash_app
from ..views import HealthCheckResponse, Query
def create_fastapi_app(function_name: str, disable_docs: bool) -> FastAPI:
def create_fastapi_app(
function_name: str, disable_docs: bool, disable_metrics: bool
) -> FastAPI:
app = FastAPI(
title=snake_case_to_text(function_name),
description=f"REST API wrapper for interacting with the '{function_name}' function.",
@ -20,6 +28,27 @@ def create_fastapi_app(function_name: str, disable_docs: bool) -> FastAPI:
def custom_swagger_ui_html() -> HTMLResponse:
return get_swagger_ui_html(openapi_url="openapi.json", title=app.title)
@app.get("/docs/index.html", include_in_schema=False)
def redirect_to_entrypoint() -> RedirectResponse:
return RedirectResponse("/docs")
if not disable_metrics:
dash_app = create_dash_app(function_name)
app.mount(get_context().metrics_path, WSGIMiddleware(dash_app))
@app.get("/", include_in_schema=False)
def redirect_to_entrypoint() -> RedirectResponse:
return RedirectResponse("/metrics")
@app.post("/query", status_code=status.HTTP_200_OK)
def query_metrics(query: Query) -> List[Dict[str, Any]]:
return get_context().persistence.query(
conjunctive_filters=query.filter,
sort_by=query.sort,
skip=query.skip,
take=query.take,
)
@app.get("/health", status_code=status.HTTP_200_OK)
def check_health() -> HealthCheckResponse:
return HealthCheckResponse(is_healthy=True)

View file

@ -2,15 +2,10 @@ from typing import Any, Callable
import uvicorn
from fastapi import FastAPI, status
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.responses import RedirectResponse
from good_ai.good_ai.deploy.create_fastapi_app import create_fastapi_app
from ..context import get_context
from ..metrics import create_dash_app
from ..tracing import TracingContext
from ..views import Trace
from .create_fastapi_app import create_fastapi_app
def serve(
@ -19,15 +14,9 @@ def serve(
disable_metrics: bool = False,
configure: Callable[[FastAPI], None] = lambda _: None,
) -> None:
app = create_fastapi_app(function.__name__, disable_docs=disable_docs)
if not disable_metrics:
dash_app = create_dash_app(function.__name__)
app.mount(get_context().metrics_path, WSGIMiddleware(dash_app))
@app.get("/", include_in_schema=False)
def redirect_to_entrypoint() -> RedirectResponse:
return RedirectResponse("/metrics")
app = create_fastapi_app(
function.__name__, disable_docs=disable_docs, disable_metrics=disable_metrics
)
@app.post("/score", status_code=status.HTTP_200_OK, response_model=Trace)
def process(input: Any) -> Trace:

View file

@ -1,27 +1,34 @@
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List
import pandas as pd
from dash import Dash, dash_table, html
import plotly.express as px
import plotly.graph_objects as go
from dash import Dash, dash_table, dcc, html
from dash.dependencies import Input, Output
from flask import Flask
from ..context import get_context
from ..views import Filter, SortBy, operators
from ..helper import snake_case_to_text
from ..views import SortBy
from .get_description import get_description
from .get_filter_from_datatable import get_filter_from_datatable
def create_dash_app(function_name: str) -> Flask:
app = Dash(function_name, requests_pathname_prefix=get_context().metrics_path + "/")
app = Dash(
function_name,
requests_pathname_prefix=get_context().metrics_path + "/",
title=snake_case_to_text(function_name),
)
documents = get_context().persistence.get_documents()
df = pd.DataFrame(documents)
app.layout = html.Div(
children=[
[
get_description(function_name),
html.Div(
dash_table.DataTable(
id="table-paging-with-graph",
table := dash_table.DataTable(
columns=[{"name": i, "id": i} for i in df.columns],
page_current=0,
page_size=20,
@ -32,23 +39,34 @@ def create_dash_app(function_name: str) -> Flask:
sort_mode="multi",
sort_by=[],
),
style={"height": 750, "overflowY": "scroll"},
),
html.Div(id="table-paging-with-graph-container"),
execution_time_histogram := dcc.Graph(),
parallel_coords := dcc.Graph(),
interval := dcc.Interval(
interval=4 * 1000, # in milliseconds
n_intervals=0,
),
]
)
@app.callback(
Output("table-paging-with-graph", "data"),
Input("table-paging-with-graph", "page_current"),
Input("table-paging-with-graph", "page_size"),
Input("table-paging-with-graph", "sort_by"),
Input("table-paging-with-graph", "filter_query"),
Output(table, "data"),
Input(table, "page_current"),
Input(table, "page_size"),
Input(table, "sort_by"),
Input(table, "filter_query"),
Input(interval, "n_intervals"),
)
def update_table(
page_current: int, page_size: int, sort_by: List[SortBy], filter: str
page_current: int,
page_size: int,
sort_by: List[SortBy],
filter: str,
n_intervals: int,
) -> List[Dict[str, Any]]:
conjunctive_filters = [get_filter(f) for f in filter.split(" && ")]
conjunctive_filters = [
get_filter_from_datatable(f) for f in filter.split(" && ")
]
non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
return get_context().persistence.query(
@ -58,55 +76,49 @@ def create_dash_app(function_name: str) -> Flask:
take=page_size,
)
# @app.callback(
# Output('table-paging-with-graph-container', "children"),
# Input('table-paging-with-graph', "data"))
# def update_graph(rows):
# dff = pd.DataFrame(rows)
# return html.Div(
# [
# dcc.Graph(
# id=column,
# figure={
# "data": [
# {
# "x": dff["country"],
# "y": dff[column] if column in dff else [],
# "type": "bar",
# "marker": {"color": "#0074D9"},
# }
# ],
# "layout": {
# "xaxis": {"automargin": True},
# "yaxis": {"automargin": True},
# "height": 250,
# "margin": {"t": 10, "l": 10, "r": 10},
# },
# },
# )
# for column in ["pop", "lifeExp", "gdpPercap"]
# ]
# )
@app.callback(
Output(execution_time_histogram, "figure"),
Input(table, "filter_query"),
Input(interval, "n_intervals"),
)
def update_execution_times(filter: str, _n_intervals: int) -> go.Figure:
conjunctive_filters = [
get_filter_from_datatable(f) for f in filter.split(" && ")
]
non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
rows = get_context().persistence.query(
conjunctive_filters=non_null_conjunctive_filters
)
df = pd.DataFrame(rows)
return px.histogram(
df,
x="execution_time_ms",
labels={"execution_time_ms": "Execution time (ms)"},
nbins=20,
title="Execution times",
log_y=True,
)
@app.callback(
Output(parallel_coords, "figure"),
Input(table, "filter_query"),
Input(interval, "n_intervals"),
)
def update_parallel_coords(filter: str, _n_intervals: int) -> go.Figure:
conjunctive_filters = [
get_filter_from_datatable(f) for f in filter.split(" && ")
]
non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
rows = get_context().persistence.query(
conjunctive_filters=non_null_conjunctive_filters
)
df = pd.DataFrame(rows)
return px.parallel_coordinates(
df, labels={c: snake_case_to_text(c) for c in df.columns}
)
return app.server
def get_filter(description: str) -> Optional[Filter]:
print(description)
for operator in operators:
if operator in description:
name_part, value_part = description.split(operator, 1)
value_part = value_part.strip()
name_part = name_part[name_part.find("{") + 1 : name_part.rfind("}")]
v0 = value_part[0]
if v0 == value_part[-1] and v0 in ("'", '"', "`"):
value: Union[str, float] = value_part[1:-1].replace("\\" + v0, v0)
else:
try:
value = float(value_part)
except ValueError:
value = value_part
return Filter(property=name_part, operator=operator, value=value)
return None

View file

@ -0,0 +1,23 @@
from typing import Optional, Union
from ..views import Filter, operators
def get_filter_from_datatable(description: str) -> Optional[Filter]:
for operator in operators:
if operator in description:
name_part, value_part = description.split(operator, 1)
value_part = value_part.strip()
name_part = name_part[name_part.find("{") + 1 : name_part.rfind("}")]
v0 = value_part[0]
if v0 == value_part[-1] and v0 in ("'", '"', "`"):
value: Union[str, float] = value_part[1:-1].replace("\\" + v0, v0)
else:
try:
value = float(value_part)
except ValueError:
value = value_part
return Filter(property=name_part, operator=operator, value=value)
return None

View file

@ -1,6 +1,6 @@
from multiprocessing import Lock
from pathlib import Path
from typing import Any, Callable, Dict
from typing import Any, Callable, Dict, Optional
import pandas as pd
from black import List
@ -35,9 +35,9 @@ class ParallelTinyDbDriver(PersistenceDriver):
def query(
self,
conjunctive_filters: List[Filter],
sort_by: List[SortBy],
skip: int,
take: int,
sort_by: List[SortBy] = [],
skip: int = 0,
take: Optional[int] = None,
) -> List[Dict[str, Any]]:
documents = self.get_documents()
df = pd.DataFrame(documents)
@ -57,7 +57,9 @@ class ParallelTinyDbDriver(PersistenceDriver):
inplace=False,
)
return df.iloc[skip : skip + take].to_dict("records")
result = df.iloc[skip:] if take is None else df.iloc[skip : skip + take]
return result.to_dict("records")
def _safe_execute(self, func: Callable[[TinyDB], Any]) -> Any:
with lock:

View file

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Dict
from typing import Any, Dict, Optional
from black import List
@ -25,8 +25,8 @@ class PersistenceDriver(ABC):
def query(
self,
conjunctive_filters: List[Filter],
sort_by: List[SortBy],
skip: int,
take: int,
sort_by: List[SortBy] = [],
skip: int = 0,
take: Optional[int] = None,
) -> List[Dict[str, Any]]:
pass

View file

@ -2,5 +2,6 @@ from .filter import Filter
from .health_check_response import HealthCheckResponse
from .model import Model
from .operators import operators
from .query import Query
from .sort_by import SortBy
from .trace import Trace

View file

@ -0,0 +1,13 @@
from typing import List
from pydantic import BaseModel
from .filter import Filter
from .sort_by import SortBy
class Query(BaseModel):
filter: List[Filter] = []
sort: List[SortBy] = []
skip: int = 0
take: int = 100

View file

@ -1,4 +1,5 @@
from typing import Literal, TypedDict
from typing import Literal
from typing_extensions import TypedDict
class SortBy(TypedDict):