Rename library

Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
Andras Schmelczer 2022-05-08 22:04:55 +02:00
commit eb7ddf1496
No known key found for this signature in database
GPG key ID: 39260B5B0614A13E
108 changed files with 839 additions and 837 deletions

0
great_ai/src/__init__.py Normal file
View file

View file

@ -0,0 +1,3 @@
from .great_ai import *
from .open_s3 import *
from .utilities import *

View file

@ -0,0 +1,4 @@
from .context import set_default_config
from .deploy import process_batch, process_single, serve
from .metrics import log_argument, log_metric
from .models import save_model, use_model

View file

@ -0,0 +1,2 @@
from .context import Context
from .get_context import get_context, set_default_config

View file

@ -0,0 +1,15 @@
from logging import Logger
from pydantic import BaseModel
from ..persistence import PersistenceDriver
class Context(BaseModel):
metrics_path: str
persistence: PersistenceDriver
is_production: bool
logger: Logger
class Config:
arbitrary_types_allowed = True

View file

@ -0,0 +1,85 @@
import os
import random
from logging import INFO, Logger
from pathlib import Path
from typing import Optional, cast
from great_ai.open_s3 import LargeFile
from great_ai.utilities.logger import create_logger
from ..persistence import ParallelTinyDbDriver, PersistenceDriver
from .context import Context
_context: Optional[Context] = None
PRODUCTION_KEY = "production"
def get_context() -> Context:
if _context is None:
set_default_config()
return cast(Context, _context)
def set_default_config(
log_level: int = INFO,
s3_config: Path = Path("s3.ini"),
seed: int = 42,
persistence_driver: PersistenceDriver = ParallelTinyDbDriver(
Path("tracing_database.json")
),
is_production_mode_override: Optional[bool] = None,
) -> None:
global _context
logger = create_logger("great_ai", level=log_level)
is_production = _is_in_production_mode(
override=is_production_mode_override, logger=logger
)
_initialize_large_file(s3_config, logger=logger)
_set_seed(seed)
if not persistence_driver.is_threadsafe:
logger.warning(
f"The selected persistence driver ({type(persistence_driver).__name__}) is not threadsafe"
)
_context = Context(
metrics_path="/metrics",
persistence=persistence_driver,
is_production=is_production,
logger=logger,
)
logger.info("Defaults: configured ✅")
def _is_in_production_mode(override: Optional[bool], logger: Logger) -> bool:
environment = os.environ.get("ENVIRONMENT", PRODUCTION_KEY).lower()
is_production = environment == PRODUCTION_KEY if override is None else override
if is_production:
logger.info("Running in production mode ✅")
else:
logger.warning("Running in development mode ‼️")
return is_production
def _initialize_large_file(s3_config: Path, logger: Logger) -> None:
if s3_config.exists():
LargeFile.configure_credentials_from_file(s3_config)
else:
logger.info(
f"Provided S3 config ({s3_config.resolve()}) not found, skipping LargeFile initialisation"
)
def _set_seed(seed: int) -> None:
random.seed(seed)
try:
import numpy
numpy.random.seed(seed + 1)
except ImportError:
pass

View file

@ -0,0 +1,3 @@
from .process_batch import process_batch
from .process_single import process_single
from .serve import serve

View file

@ -0,0 +1,64 @@
from pathlib import Path
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 fastapi.staticfiles import StaticFiles
from starlette.responses import HTMLResponse
from ..context import get_context
from ..helper import snake_case_to_text
from ..metrics import create_dash_app
from ..views import HealthCheckResponse, Query
PATH = Path(__file__).parent.resolve()
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.",
docs_url=None,
redoc_url=None,
)
if not disable_docs:
@app.get("/docs", include_in_schema=False)
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.mount(
"/assets", StaticFiles(directory=PATH / "../metrics/assets"), name="static"
)
@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)
return app

View file

@ -0,0 +1,25 @@
from typing import Any, Callable, Iterable, Optional, Sequence
from great_ai.utilities.parallel_map import parallel_map
from ..context import get_context
from ..tracing import TracingContext
from ..views import Trace
def process_batch(
function: Callable[..., Any],
batch: Iterable[Any],
concurrency: Optional[int] = None,
) -> Sequence[Trace]:
def inner(input: Any) -> Trace:
with TracingContext() as t:
result = function(input)
output = t.log_output(result)
return output
if not get_context().persistence.is_threadsafe:
concurrency = 1
get_context().logger.warning("Concurrency is ignored")
return parallel_map(inner, batch, concurrency=concurrency)

View file

@ -0,0 +1,11 @@
from typing import Any, Callable
from ..tracing import TracingContext
from ..views import Trace
def process_single(function: Callable[..., Any], input_value: Any) -> Trace:
with TracingContext() as t:
result = function(input_value)
output = t.log_output(result)
return output

View file

@ -0,0 +1,48 @@
from typing import Any, Callable
import uvicorn
from fastapi import FastAPI, status
from uvicorn.config import LOGGING_CONFIG
from ..tracing import TracingContext
from ..views import Trace
from .create_fastapi_app import create_fastapi_app
def serve(
function: Callable[..., Any],
disable_docs: bool = False,
disable_metrics: bool = False,
configure: Callable[[FastAPI], None] = lambda _: None,
) -> None:
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:
with TracingContext() as t:
result = function(input)
output = t.log_output(result)
return output
configure(app)
uvicorn.run(
app,
host="0.0.0.0",
port=5050,
log_config={
**LOGGING_CONFIG,
"formatters": {
"default": {
"()": "great_ai.logger.CustomFormatter",
"fmt": "%(asctime)s | %(levelname)8s | %(message)s",
},
"access": {
"()": "great_ai.logger.CustomFormatter",
"fmt": "%(asctime)s | %(levelname)8s | %(message)s", # noqa: E501
},
},
},
)

View file

@ -0,0 +1 @@
from .argument_validation_error import ArgumentValidationError

View file

@ -0,0 +1,2 @@
class ArgumentValidationError(Exception):
pass

View file

@ -0,0 +1,3 @@
from .get_args import get_args
from .snake_case_to_text import snake_case_to_text
from .text_to_hex_color import text_to_hex_color

View file

@ -0,0 +1,14 @@
import inspect
from typing import Any, Callable, Dict, Mapping, Sequence
def get_args(
func: Callable[..., Any], args: Sequence[Any], kwargs: Mapping[str, Any]
) -> Dict[str, Any]:
signature = inspect.signature(func)
filter_keys = [
param.name
for param in signature.parameters.values()
if param.kind == param.POSITIONAL_OR_KEYWORD
]
return {**dict(zip(filter_keys, args)), **kwargs}

View file

@ -0,0 +1,2 @@
def snake_case_to_text(snake_case: str) -> str:
return snake_case.capitalize().replace("_", " ")

View file

@ -0,0 +1,13 @@
import colorsys
from hashlib import md5
def text_to_hex_color(text: str) -> str:
ascii_bytes = text.encode("ascii")
digest = md5(
ascii_bytes
).hexdigest() # the built-in hash function is salted differently in each process
integer = int(digest, 16)
hue = integer % 6311 / 6311.0
rgb = colorsys.hsv_to_rgb(hue, 0.75, 0.6)
return "#" + "".join("%02X" % round(i * 255) for i in rgb)

View file

@ -0,0 +1,3 @@
from .create_dash_app import create_dash_app
from .log_argument import log_argument
from .log_metric import log_metric

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,112 @@
:root {
--accent-color: #47c2d0;
--error-color: #a30808;
--background-color: #edf5f6;
--small-padding: 10px;
--medium-padding: 20px;
--large-padding: 40px;
--border-radius: 10px;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -1px rgb(0 0 0 / 6%);
}
* {
margin: 0;
box-sizing: border-box;
}
body {
background-color: var(--background-color);
font-family: Arial, Helvetica, sans-serif;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: var(--medium-padding) 0 var(--small-padding) 0;
}
.glance,
.table-container,
.parallel-coords {
padding: var(--large-padding);
margin: var(--large-padding);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
background-color: white;
overflow: hidden;
}
.glance {
display: flex;
}
.glance .description {
width: 350px;
display: flex;
flex-direction: column;
justify-content: center;
}
.glance .dash-graph {
flex: 1;
}
.table-container {
padding: 0;
}
.table-container h2 {
padding: var(--small-padding);
margin: 0;
}
.table-container h2,
footer.watermark {
opacity: 0.35;
}
.dash-spreadsheet {
overflow: auto;
}
th,
td {
max-width: 350px;
}
.parallel-coords {
padding: 0;
}
footer.watermark {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--large-padding);
background-color: #ddd;
position: relative;
}
footer.watermark div {
display: flex;
flex-direction: column;
}
h6 {
font-size: 3rem;
margin-top: 0;
}
a img {
width: 64px;
height: 64px;
cursor: pointer;
transition: transform 300ms;
}
a img:hover {
transform: scale(1.1);
}

Some files were not shown because too many files have changed in this diff Show more