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 import FastAPI, status
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.openapi.docs import get_swagger_ui_html from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import RedirectResponse
from starlette.responses import HTMLResponse from starlette.responses import HTMLResponse
from ..context import get_context
from ..helper import snake_case_to_text 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( app = FastAPI(
title=snake_case_to_text(function_name), title=snake_case_to_text(function_name),
description=f"REST API wrapper for interacting with the '{function_name}' function.", 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: def custom_swagger_ui_html() -> HTMLResponse:
return get_swagger_ui_html(openapi_url="openapi.json", title=app.title) 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) @app.get("/health", status_code=status.HTTP_200_OK)
def check_health() -> HealthCheckResponse: def check_health() -> HealthCheckResponse:
return HealthCheckResponse(is_healthy=True) return HealthCheckResponse(is_healthy=True)

View file

@ -2,15 +2,10 @@ from typing import Any, Callable
import uvicorn import uvicorn
from fastapi import FastAPI, status 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 ..tracing import TracingContext
from ..views import Trace from ..views import Trace
from .create_fastapi_app import create_fastapi_app
def serve( def serve(
@ -19,15 +14,9 @@ def serve(
disable_metrics: bool = False, disable_metrics: bool = False,
configure: Callable[[FastAPI], None] = lambda _: None, configure: Callable[[FastAPI], None] = lambda _: None,
) -> None: ) -> None:
app = create_fastapi_app(function.__name__, disable_docs=disable_docs) app = create_fastapi_app(
function.__name__, disable_docs=disable_docs, disable_metrics=disable_metrics
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("/score", status_code=status.HTTP_200_OK, response_model=Trace) @app.post("/score", status_code=status.HTTP_200_OK, response_model=Trace)
def process(input: Any) -> 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 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 dash.dependencies import Input, Output
from flask import Flask from flask import Flask
from ..context import get_context 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_description import get_description
from .get_filter_from_datatable import get_filter_from_datatable
def create_dash_app(function_name: str) -> Flask: 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() documents = get_context().persistence.get_documents()
df = pd.DataFrame(documents) df = pd.DataFrame(documents)
app.layout = html.Div( app.layout = html.Div(
children=[ [
get_description(function_name), get_description(function_name),
html.Div( html.Div(
dash_table.DataTable( table := dash_table.DataTable(
id="table-paging-with-graph",
columns=[{"name": i, "id": i} for i in df.columns], columns=[{"name": i, "id": i} for i in df.columns],
page_current=0, page_current=0,
page_size=20, page_size=20,
@ -32,23 +39,34 @@ def create_dash_app(function_name: str) -> Flask:
sort_mode="multi", sort_mode="multi",
sort_by=[], 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( @app.callback(
Output("table-paging-with-graph", "data"), Output(table, "data"),
Input("table-paging-with-graph", "page_current"), Input(table, "page_current"),
Input("table-paging-with-graph", "page_size"), Input(table, "page_size"),
Input("table-paging-with-graph", "sort_by"), Input(table, "sort_by"),
Input("table-paging-with-graph", "filter_query"), Input(table, "filter_query"),
Input(interval, "n_intervals"),
) )
def update_table( 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]]: ) -> 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] non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
return get_context().persistence.query( return get_context().persistence.query(
@ -58,55 +76,49 @@ def create_dash_app(function_name: str) -> Flask:
take=page_size, take=page_size,
) )
# @app.callback( @app.callback(
# Output('table-paging-with-graph-container', "children"), Output(execution_time_histogram, "figure"),
# Input('table-paging-with-graph', "data")) Input(table, "filter_query"),
# def update_graph(rows): Input(interval, "n_intervals"),
# dff = pd.DataFrame(rows) )
# return html.Div( def update_execution_times(filter: str, _n_intervals: int) -> go.Figure:
# [ conjunctive_filters = [
# dcc.Graph( get_filter_from_datatable(f) for f in filter.split(" && ")
# id=column, ]
# figure={ non_null_conjunctive_filters = [f for f in conjunctive_filters if f is not None]
# "data": [
# { rows = get_context().persistence.query(
# "x": dff["country"], conjunctive_filters=non_null_conjunctive_filters
# "y": dff[column] if column in dff else [], )
# "type": "bar", df = pd.DataFrame(rows)
# "marker": {"color": "#0074D9"},
# } return px.histogram(
# ], df,
# "layout": { x="execution_time_ms",
# "xaxis": {"automargin": True}, labels={"execution_time_ms": "Execution time (ms)"},
# "yaxis": {"automargin": True}, nbins=20,
# "height": 250, title="Execution times",
# "margin": {"t": 10, "l": 10, "r": 10}, log_y=True,
# }, )
# },
# ) @app.callback(
# for column in ["pop", "lifeExp", "gdpPercap"] 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 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 multiprocessing import Lock
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict from typing import Any, Callable, Dict, Optional
import pandas as pd import pandas as pd
from black import List from black import List
@ -35,9 +35,9 @@ class ParallelTinyDbDriver(PersistenceDriver):
def query( def query(
self, self,
conjunctive_filters: List[Filter], conjunctive_filters: List[Filter],
sort_by: List[SortBy], sort_by: List[SortBy] = [],
skip: int, skip: int = 0,
take: int, take: Optional[int] = None,
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
documents = self.get_documents() documents = self.get_documents()
df = pd.DataFrame(documents) df = pd.DataFrame(documents)
@ -57,7 +57,9 @@ class ParallelTinyDbDriver(PersistenceDriver):
inplace=False, 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: def _safe_execute(self, func: Callable[[TinyDB], Any]) -> Any:
with lock: with lock:

View file

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

View file

@ -2,5 +2,6 @@ from .filter import Filter
from .health_check_response import HealthCheckResponse from .health_check_response import HealthCheckResponse
from .model import Model from .model import Model
from .operators import operators from .operators import operators
from .query import Query
from .sort_by import SortBy from .sort_by import SortBy
from .trace import Trace 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): class SortBy(TypedDict):