Add charts
Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
parent
baad19f682
commit
6b2b4dc989
9 changed files with 163 additions and 93 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
good_ai/src/good_ai/good_ai/views/query.py
Normal file
13
good_ai/src/good_ai/good_ai/views/query.py
Normal 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
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
from typing import Literal, TypedDict
|
||||
from typing import Literal
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class SortBy(TypedDict):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue