Rename library
Signed-off-by: András Schmelczer <andras@schmelczer.dev>
This commit is contained in:
parent
8708b157eb
commit
eb7ddf1496
108 changed files with 839 additions and 837 deletions
0
great_ai/src/__init__.py
Normal file
0
great_ai/src/__init__.py
Normal file
3
great_ai/src/great_ai/__init__.py
Normal file
3
great_ai/src/great_ai/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .great_ai import *
|
||||
from .open_s3 import *
|
||||
from .utilities import *
|
||||
4
great_ai/src/great_ai/great_ai/__init__.py
Normal file
4
great_ai/src/great_ai/great_ai/__init__.py
Normal 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
|
||||
2
great_ai/src/great_ai/great_ai/context/__init__.py
Normal file
2
great_ai/src/great_ai/great_ai/context/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from .context import Context
|
||||
from .get_context import get_context, set_default_config
|
||||
15
great_ai/src/great_ai/great_ai/context/context.py
Normal file
15
great_ai/src/great_ai/great_ai/context/context.py
Normal 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
|
||||
85
great_ai/src/great_ai/great_ai/context/get_context.py
Normal file
85
great_ai/src/great_ai/great_ai/context/get_context.py
Normal 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
|
||||
3
great_ai/src/great_ai/great_ai/deploy/__init__.py
Normal file
3
great_ai/src/great_ai/great_ai/deploy/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .process_batch import process_batch
|
||||
from .process_single import process_single
|
||||
from .serve import serve
|
||||
64
great_ai/src/great_ai/great_ai/deploy/create_fastapi_app.py
Normal file
64
great_ai/src/great_ai/great_ai/deploy/create_fastapi_app.py
Normal 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
|
||||
25
great_ai/src/great_ai/great_ai/deploy/process_batch.py
Normal file
25
great_ai/src/great_ai/great_ai/deploy/process_batch.py
Normal 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)
|
||||
11
great_ai/src/great_ai/great_ai/deploy/process_single.py
Normal file
11
great_ai/src/great_ai/great_ai/deploy/process_single.py
Normal 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
|
||||
48
great_ai/src/great_ai/great_ai/deploy/serve.py
Normal file
48
great_ai/src/great_ai/great_ai/deploy/serve.py
Normal 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
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
1
great_ai/src/great_ai/great_ai/exceptions/__init__.py
Normal file
1
great_ai/src/great_ai/great_ai/exceptions/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .argument_validation_error import ArgumentValidationError
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
class ArgumentValidationError(Exception):
|
||||
pass
|
||||
3
great_ai/src/great_ai/great_ai/helper/__init__.py
Normal file
3
great_ai/src/great_ai/great_ai/helper/__init__.py
Normal 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
|
||||
14
great_ai/src/great_ai/great_ai/helper/get_args.py
Normal file
14
great_ai/src/great_ai/great_ai/helper/get_args.py
Normal 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}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
def snake_case_to_text(snake_case: str) -> str:
|
||||
return snake_case.capitalize().replace("_", " ")
|
||||
13
great_ai/src/great_ai/great_ai/helper/text_to_hex_color.py
Normal file
13
great_ai/src/great_ai/great_ai/helper/text_to_hex_color.py
Normal 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)
|
||||
3
great_ai/src/great_ai/great_ai/metrics/__init__.py
Normal file
3
great_ai/src/great_ai/great_ai/metrics/__init__.py
Normal 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
|
||||
BIN
great_ai/src/great_ai/great_ai/metrics/assets/github.png
Normal file
BIN
great_ai/src/great_ai/great_ai/metrics/assets/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
112
great_ai/src/great_ai/great_ai/metrics/assets/index.css
Normal file
112
great_ai/src/great_ai/great_ai/metrics/assets/index.css
Normal 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
Loading…
Reference in a new issue