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 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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
|
||||||
|
|
@ -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 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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
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):
|
class SortBy(TypedDict):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue