Move files

This commit is contained in:
Andras Schmelczer 2022-06-25 14:01:14 +02:00
commit 0bf865ce2a
181 changed files with 117 additions and 2394 deletions

1
great_ai/.gitignore vendored
View file

@ -1 +0,0 @@
build

View file

@ -1,34 +0,0 @@
# **S**coutinScience **U**tilitie**S** for text processing [![Lint and test ScoutinScience utilities](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml/badge.svg)](https://github.com/ScoutinScience/platform/actions/workflows/sus-general.yaml)
> amogus
## Exports
- [clean](src/sus/clean.py)
- [unique](src/sus/unique.py)
- [parallel_map](src/sus/parallel_map.py)
- [match_names](src/sus/match_names/match_names.py)
- [evaluate_ranking](src/sus/evaluate_ranking/evaluate_ranking.py)
- [get_sentences](src/sus/get_sentences.py)
### Requires loading spacy model
> This is automatic but will require some time.
> Add this to the Dockerfile for caching the spaCy model:
>
> ```docker
> RUN pip install --no-cache-dir en-core-web-sm@https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.3.0/en_core_web_sm-3.3.0-py3-none-any.whl
> ```
- [publication TEI](src/sus/publication_tei/publication_tei.py)
- [lemmatize_text](src/sus/lemmatize_text.py)
- [lemmatize_token](src/sus/lemmatize_token.py)
- [spacy model (nlp)](src/sus/nlp.py)
- [filter_sentences](src/sus/matcher/filter_sentences.py)
## Development
- Optional booleans must have a default value of `False`.
- No imports in top-level `__init__.py`, in order to not load anything unnecessary automatically
- Should only be updated through a PR

View file

@ -1,5 +0,0 @@
aws_region_name = your_region_like_eu-west-2
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_VERY_SECRET_ACCESS_KEY
large_files_bucket_name = create_a_bucket_and_put_its_name_here
aws_endpoint_url = this is optional, for backblaze, use this: https://s3.us-west-002.backblazeb2.com

View file

@ -1,118 +0,0 @@
# [open(S3)](https://pypi.org/project/open-large/)
Storing, versioning, and downloading files from S3 made as easy as using `open()` in Python. Caching included.
## Motivation
Oftentimes, especially when working with data-heavy applications, large files can proliferate in a repository. Version controlling them is an obvious next step, however, GitHub's git LFS implementation [doesn't support the deletion](https://docs.github.com/en/repositories/working-with-files/managing-large-files/removing-files-from-git-large-file-storage#git-lfs-objects-in-your-repository) of large files, making it easy for them to eat-up the LFS quota and explode the size of your repos.
## Solution
```
pip install open-large
```
### Simple example
```python
from large_file import LargeFileS3
LargeFileS3.configure_credentials({
"aws_region_name": "your_region_like_eu-west-2",
"aws_access_key_id": "YOUR_ACCESS_KEY_ID",
"aws_secret_access_key": "YOUR_VERY_SECRET_ACCESS_KEY",
"large_files_bucket_name": "create_a_bucket_and_put_its_name_here",
})
# Creates a new version and deletes the older version leaving the 3 most recently used intact
with LargeFileS3("test.txt", "w", keep_last_n=3) as f:
for i in range(100000):
f.write('test\n')
# By default the latest version is returned
# but an optional `version` keyword argument can be provided as well
with LargeFileS3("test.txt", "r") as f:
print(f.readlines()[0])
```
> Automatically creates a file, writes to it, uploads it to S3, and then queries the most recent version of it.
> In this case, the latest version is already in the local cache, no download is required.
### More details
`LargeFile` behaves like an opened file (in the background it is a temp file after all). Binary reading and writing is supported along with the [different keywords](https://docs.python.org/3/library/functions.html#open) `open()` accepts.
The local cache can be configured with these properties:
```python
LargeFile.cache_path = Path('.cache')
LargeFile.max_cache_size = "30 GB"
```
#### I only need a path
In case you only need a path to the "remote" file, this pattern can be applied:
```python
path_to_model = LargeFile("folder-of-my-bert-model", version=31).get()
```
> This will first download the file/folder into your local cache folder. Then, it returns a `Path` object to the local version. Which can be turned into a string with `str(path_to_model)`.
The same approach works for uploads:
```python
LargeFile("folder-of-my-bert-model").push('path_to_local/folder_or_file')
```
> This way, both regular files and folders can be handled. The uploaded file is called **folder-of-my-bert-model**, the local name is ignored.
Lastly, all version of the remote object can be deleted by calling `LargeFile("my-file").delete()`. It will still reside in your local cache afterwards, its deletion will happen next time your local cache has to be pruned.
### Command-line example
The package can be used as a module from the command-line to give you more flexibility.
#### Setup
Create an .ini file (or use _~/.aws/credentials_). It may look like this:
```ini
aws_region_name = your_region_like_eu-west-2
aws_access_key_id = YOUR_ACCESS_KEY_ID
aws_secret_access_key = YOUR_VERY_SECRET_ACCESS_KEY
large_files_bucket_name = my_large_files
endpoint_url = this is optional, for backblaze, use this: https://s3.us-west-002.backblazeb2.com
```
> Just like in [example secrets](example_secrets.ini).
#### Print the expected options
```sh
python3 -m large_file --help
```
#### Upload some files
```sh
python3 -m large_file --backend s3 --secrets secrets.ini --push my_first_file.json folder/my_second_file my_folder
```
> Only the filename is used as the S3 name, the rest of the path is ignored.
#### Download some files to the local cache
This can be useful when building a Docker image for example. This way, the files can already reside inside the container and need not be downloaded later.
```sh
python3 -m large_file --backend s3 -secrets ~/.aws/credentials --cache my_first_file.json:3 my_second_file my_folder:0
```
> Versions may be specified by using `:`-s.
#### Delete remote files
```sh
python3 -m large_file --backend s3 --secrets ~/.aws/credentials --delete my_first_file.json
```

View file

@ -1,7 +0,0 @@
[build-system]
requires = [
"setuptools>=42",
"setuptools-git",
"wheel"
]
build-backend = "setuptools.build_meta"

View file

@ -1,23 +0,0 @@
click < 8.1.0
unidecode >= 1.3.0
multiprocess >= 0.70.0.0
tqdm >= 4.0.0
psutil >= 5.9.0
beautifulsoup4 >= 4.10.0
lxml >= 4.6.0
spacy >= 3.3.0
pydantic >= 1.8.0
scikit-learn == 1.1.1
matplotlib >= 3.5.0
numpy >= 1.22.0
langcodes[data] >= 3.3.0
segtok >= 1.5.11
langdetect >= 1.0.9
tinydb >= 4.7.0
pandas >= 1.4.0
pyaml >= 21.0.0
boto3 >= 1.23.0
fastapi >= 0.70.0
plotly >= 5.8.0
dash >= 2.4.0
uvicorn[standard] >= 0.17.0

View file

@ -1,52 +0,0 @@
[metadata]
name = great-ai
version = 0.0.1
author = András Schmelczer
author_email = andras@scoutinscience.com
description =
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/ScoutinScience/great-ai
project_urls =
Bug Tracker = https://github.com/ScoutinScience/great-ai/issues
classifiers =
Programming Language :: Python :: 3
Operating System :: OS Independent
[options]
package_dir =
= src
packages = find:
include_package_data = True
python_requires = >=3.8
install_requires =
unidecode >= 1.3.0
multiprocess >= 0.70.0.0
tqdm >= 4.0.0
psutil >= 5.9.0
beautifulsoup4 >= 4.10.0
lxml >= 4.6.0
spacy >= 3.3.0
pydantic >= 1.8.0
scikit-learn == 1.1.1
matplotlib >= 3.5.0
numpy >= 1.22.0
langcodes[data] >= 3.3.0
segtok >= 1.5.11
langdetect >= 1.0.9
tinydb >= 4.7.0
pandas >= 1.4.0
pyaml >= 21.0.0
boto3 >= 1.23.0
fastapi >= 0.70.0
plotly >= 5.8.0
dash >= 2.4.0
uvicorn[standard] >= 0.17.0
watchdog >= 2.1.0
pymongo >= 4.0.0
[options.package_data]
* = *.json, *.yaml, *.yml, *.css
[options.packages.find]
where = src

View file

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

View file

@ -1,194 +0,0 @@
#!/usr/bin/env python3
import logging
import re
import time
from importlib import import_module, reload
from pathlib import Path
from typing import Optional
import uvicorn
from uvicorn.config import LOGGING_CONFIG, Config
from uvicorn.subprocess import get_subprocess
from uvicorn.supervisors.basereload import BaseReload
from watchdog.events import FileSystemEvent, PatternMatchingEventHandler
from watchdog.observers import Observer
from .great_ai.constants import SERVER_NAME
from .great_ai.context import _is_in_production_mode
from .great_ai.deploy import GreatAI
from .great_ai.exceptions import ArgumentValidationError, MissingArgumentError
from .parse_arguments import parse_arguments
from .utilities import get_logger
logger = get_logger(SERVER_NAME)
GREAT_AI_LOGGING_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
},
},
}
def main() -> None:
args = parse_arguments()
should_auto_reload = not _is_in_production_mode(logger=None)
if args.workers > 1 and should_auto_reload:
raise ArgumentValidationError(
"Cannot use auto-reload with multiple workers: set the `--workers=1` CLI argument,"
+ "or set the ENVIRONMENT environment variable to `production`."
)
common_config = dict(
host=args.host,
port=args.port,
timeout_keep_alive=args.timeout_keep_alive,
workers=args.workers,
server_header=False,
reload=False,
log_config=GREAT_AI_LOGGING_CONFIG,
)
if not should_auto_reload:
file_name = get_script_name(args.file_name)
app = find_app(file_name)
logger.info(f"Starting uvicorn server with app={app}")
uvicorn.run(app, **common_config) # this will never return
class EventHandler(PatternMatchingEventHandler):
def __init__(self) -> None:
super().__init__(patterns=["*.py", "*.ipynb"], ignore_patterns=["__*.py"])
self.server: Optional[GreatAIReload] = None
self.restart()
def on_closed(self, event: FileSystemEvent) -> None:
logger.warning(f"File {event.src_path} has triggered a restart")
self.restart()
def restart(self) -> None:
file_name = get_script_name(args.file_name)
app = find_app(file_name)
if app is None:
logger.warning("Auto-reloading skipped")
return
self.stop_server()
config = Config(app, **common_config)
socket = config.bind_socket()
self.server = GreatAIReload(
config, target=uvicorn.Server(config=config).run, sockets=[socket]
)
self.server.startup()
def stop_server(self) -> None:
if self.server:
self.server.shutdown()
restart_handler = EventHandler()
observer = Observer()
observer.schedule(restart_handler, path=".", recursive=True)
observer.start()
try:
while True:
time.sleep(50)
finally:
observer.stop()
restart_handler.stop_server()
if args.file_name.endswith(".ipynb"):
Path(get_script_name_of_notebook(args.file_name)).unlink(missing_ok=True)
observer.join()
def get_script_name(file_name_argument: str) -> str:
if file_name_argument.endswith(".ipynb"):
logger.info("Converting notebook to Python script")
try:
from nbconvert import PythonExporter
exporter = PythonExporter()
content, _ = exporter.from_filename(file_name_argument)
file_name_argument = get_script_name_of_notebook(file_name_argument)
with open(file_name_argument, "w", encoding="utf-8") as f:
f.write(content)
except ImportError:
raise ImportError(
"Install `nbconvert` to be able to use Jupyter notebook files or use a regular Python file instead"
)
return re.sub(r"\.(py|ipynb)$", "", file_name_argument)
def get_script_name_of_notebook(notebook_name: str) -> str:
base_name = re.sub(r"\.ipynb$", "", notebook_name)
return f"__{base_name}__.py"
module = None
def find_app(file_name: str) -> Optional[str]:
global module
logging.disable(logging.CRITICAL)
try:
if module is None:
module = import_module(file_name)
else:
module = reload(module)
except Exception:
logging.disable(logging.NOTSET)
logger.exception("Could not load file because of an exception: fix your code")
return None
finally:
logging.disable(logging.NOTSET)
for name, value in module.__dict__.items():
if isinstance(value, GreatAI):
app_name = name
if app_name:
logger.info(f"Found `{app_name}` to be the GreatAI app ")
else:
raise MissingArgumentError(
"GreatAI app could not be found, make sure to use `@GreatAI.deploy` on your prediction function"
)
return f"{file_name}:{app_name}.app"
class GreatAIReload(BaseReload):
def startup(self) -> None:
self.process = get_subprocess(
config=self.config, target=self.target, sockets=self.sockets
)
self.process.start()
def shutdown(self) -> None:
self.process.terminate()
self.process.join()
for sock in self.sockets:
sock.close()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
exit()
except Exception as e:
logger.error(e)

View file

@ -1,11 +0,0 @@
from .context import configure
from .deploy import GreatAI
from .models import save_model, use_model
from .output_models import (
ClassificationOutput,
MultiLabelClassificationOutput,
RegressionOutput,
)
from .parameters import log_metric, parameter
from .persistence import MongodbDriver, ParallelTinyDbDriver, TracingDatabaseDriver
from .tracing import add_ground_truth, delete_ground_truth, query_ground_truth

View file

@ -1,30 +0,0 @@
from ..large_file import LargeFileMongo, LargeFileS3
from .persistence.mongodb_driver import MongodbDriver
ENV_VAR_KEY = "ENVIRONMENT"
PRODUCTION_KEY = "production"
DASHBOARD_PATH = "/dashboard"
MONGO_CONFIG_PATHS = ["mongodb.ini", "mongo.ini", "mongo_db.ini", "mongo-db.ini"]
DEFAULT_TRACING_DATABASE_CONFIG_PATHS = {
MongodbDriver: MONGO_CONFIG_PATHS,
}
DEFAULT_LARGE_FILE_CONFIG_PATHS = {
LargeFileS3: ["s3.ini", "b2.ini"],
LargeFileMongo: MONGO_CONFIG_PATHS,
}
GITHUB_LINK = "https://github.com/ScoutinScience/great-ai"
TRAIN_SPLIT_TAG_NAME = "train"
TEST_SPLIT_TAG_NAME = "test"
VALIDATION_SPLIT_TAG_NAME = "validation"
GROUND_TRUTH_TAG_NAME = "ground_truth"
PRODUCTION_TAG_NAME = "production"
DEVELOPMENT_TAG_NAME = "development"
ONLINE_TAG_NAME = "online"
SERVER_NAME = "GreatAI-Server"
SE4ML_WEBSITE = "https://se-ml.github.io/practices/"

View file

@ -1,191 +0,0 @@
import os
import random
from logging import DEBUG, Logger
from pathlib import Path
from typing import Any, Dict, Optional, Type, cast
from pydantic import BaseModel
from great_ai.large_file import LargeFile, LargeFileLocal
from great_ai.utilities import get_logger
from .constants import (
DEFAULT_LARGE_FILE_CONFIG_PATHS,
DEFAULT_TRACING_DATABASE_CONFIG_PATHS,
ENV_VAR_KEY,
PRODUCTION_KEY,
SE4ML_WEBSITE,
)
from .persistence import ParallelTinyDbDriver, TracingDatabaseDriver
class Context(BaseModel):
tracing_database: TracingDatabaseDriver
large_file_implementation: Type[LargeFile]
is_production: bool
logger: Logger
should_log_exception_stack: bool
prediction_cache_size: int
dashboard_table_size: int
class Config:
arbitrary_types_allowed = True
def to_flat_dict(self) -> Dict[str, Any]:
return {
"tracing_database": type(self.tracing_database).__name__,
"large_file_implementation": self.large_file_implementation.__name__,
"is_production": self.is_production,
"should_log_exception_stack": self.should_log_exception_stack,
"prediction_cache_size": self.prediction_cache_size,
"dashboard_table_size": self.dashboard_table_size,
}
_context: Optional[Context] = None
def get_context() -> Context:
if _context is None:
configure()
return cast(Context, _context)
def configure(
*,
log_level: int = DEBUG,
seed: int = 42,
tracing_database: Optional[Type[TracingDatabaseDriver]] = None,
large_file_implementation: Optional[Type[LargeFile]] = None,
should_log_exception_stack: Optional[bool] = None,
prediction_cache_size: int = 512,
disable_se4ml_banner: bool = False,
dashboard_table_size: int = 50,
) -> None:
global _context
logger = get_logger("great_ai", level=log_level)
if _context is not None:
logger.warn(
"Configuration has been already initialised, overwriting.\n"
+ "Make sure to call `configure()` before importing your application code."
)
is_production = _is_in_production_mode(logger=logger)
_set_seed(seed)
tracing_database = _initialize_tracing_database(tracing_database, logger=logger)()
if not tracing_database.is_production_ready:
if is_production:
logger.error(
f"The selected tracing database ({type(tracing_database).__name__}) is not recommended for production"
)
else:
logger.warning(
f"The selected tracing database ({type(tracing_database).__name__}) is not recommended for production"
)
_context = Context(
tracing_database=tracing_database,
large_file_implementation=_initialize_large_file(
large_file_implementation, logger=logger
),
is_production=is_production,
logger=logger,
should_log_exception_stack=not is_production
if should_log_exception_stack is None
else should_log_exception_stack,
prediction_cache_size=prediction_cache_size,
dashboard_table_size=dashboard_table_size,
)
logger.info("Settings: configured ✅")
for k, v in get_context().to_flat_dict().items():
logger.info(f"🔩 {k}: {v}")
if not is_production and not disable_se4ml_banner:
logger.warning(
"You still need to check whether you follow all best practices before trusting your deployment."
)
logger.warning(f"> Find out more at {SE4ML_WEBSITE}")
def _is_in_production_mode(logger: Optional[Logger]) -> bool:
environment = os.environ.get(ENV_VAR_KEY)
if environment is None:
if logger:
logger.warning(
f"Environment variable {ENV_VAR_KEY} is not set, defaulting to development mode ‼️"
)
is_production = False
else:
is_production = environment.lower() == PRODUCTION_KEY
if logger:
if not is_production:
logger.info(
f"Value of {ENV_VAR_KEY} is `{environment}` which is not equal to `{PRODUCTION_KEY}`"
+ "defaulting to development mode ‼️"
)
else:
logger.info("Running in production mode ✅")
return is_production
def _initialize_tracing_database(
selected: Optional[Type[TracingDatabaseDriver]], logger: Logger
) -> Type[TracingDatabaseDriver]:
for tracing_driver, paths in DEFAULT_TRACING_DATABASE_CONFIG_PATHS.items():
if selected is None or selected == tracing_driver:
if tracing_driver.initialized:
logger.warning(
f"{tracing_driver.__name__} has been already configured: skipping initialisation"
)
return tracing_driver
for p in paths:
if Path(p).exists():
logger.info(
f"Found credentials file ({Path(p).absolute()}), initialising {tracing_driver.__name__}"
)
tracing_driver.configure_credentials_from_file(p)
return tracing_driver
logger.warning(
"Cannot find credentials files, defaulting to using ParallelTinyDbDriver"
)
return ParallelTinyDbDriver
def _initialize_large_file(
selected: Optional[Type[LargeFile]], logger: Logger
) -> Type[LargeFile]:
for large_file, paths in DEFAULT_LARGE_FILE_CONFIG_PATHS.items():
if selected is None or selected == large_file:
if large_file.initialized:
logger.warning(
f"{large_file.__name__} has been already configured: skipping initialisation"
)
return large_file
for p in paths:
if Path(p).exists():
logger.info(
f"Found credentials file ({Path(p).absolute()}), initialising {large_file.__name__}"
)
large_file.configure_credentials_from_file(p)
return large_file
logger.warning("Cannot find credentials files, defaulting to using LargeFileLocal")
return LargeFileLocal
def _set_seed(seed: int) -> None:
random.seed(seed)
try:
import numpy
numpy.random.seed(seed + 1)
except ImportError:
pass

View file

@ -1 +0,0 @@
from .great_ai import GreatAI

View file

@ -1,211 +0,0 @@
import inspect
from functools import lru_cache, partial, wraps
from typing import (
Any,
Callable,
Generic,
Iterable,
List,
Optional,
Type,
TypeVar,
Union,
cast,
)
from fastapi import APIRouter, FastAPI, status
from pydantic import BaseModel, create_model
from great_ai.great_ai.deploy.routes.bootstrap_dashboard import bootstrap_dashboard
from great_ai.great_ai.views.cache_statistics import CacheStatistics
from great_ai.utilities import parallel_map
from ..constants import DASHBOARD_PATH
from ..context import get_context
from ..helper import (
freeze_arguments,
get_function_metadata_store,
snake_case_to_text,
use_http_exceptions,
)
from ..parameters import automatically_decorate_parameters
from ..tracing.tracing_context import TracingContext
from ..views import ApiMetadata, HealthCheckResponse, Trace
from .routes import (
bootstrap_docs_endpoints,
bootstrap_feedback_endpoints,
bootstrap_trace_endpoints,
)
T = TypeVar("T")
class GreatAI(Generic[T]):
def __init__(self, func: Callable[..., Any], version: str):
func = automatically_decorate_parameters(func)
get_function_metadata_store(func).is_finalised = True
self._func = func
def func_in_tracing_context(*args: Any, **kwargs: Any) -> Trace[T]:
with TracingContext[T](func.__name__) as t:
result = func(*args, **kwargs)
output = t.finalise(output=result)
return output
self._cached_func = lru_cache(get_context().prediction_cache_size)(
func_in_tracing_context
) # cannot put decorator on method, because it require the context to be setup
wraps(func)(self)
self._version = version
self.app = FastAPI(
title=self.name,
version=self.version,
description=self.documentation
+ f"\n\nFind out more in the [dashboard]({DASHBOARD_PATH}).",
docs_url=None,
redoc_url=None,
)
@staticmethod
def deploy(
func: Optional[Callable[..., T]] = None,
*,
version: str = "0.0.1",
disable_rest_api: bool = False,
disable_docs: bool = False,
disable_dashboard: bool = False,
) -> Union[Callable[[Callable[..., T]], "GreatAI[T]"], "GreatAI[T]"]:
if func is None:
return cast(
Callable[[Callable[..., T]], GreatAI[T]],
partial(
GreatAI.deploy,
disable_http=disable_rest_api,
disable_docs=disable_docs,
disable_dashboard=disable_dashboard,
),
)
instance = GreatAI[T](func, version=version)
if not disable_rest_api:
instance._bootstrap_rest_api(
disable_docs=disable_docs, disable_dashboard=disable_dashboard
)
return instance
@freeze_arguments
def __call__(self, *args: Any, **kwargs: Any) -> Trace[T]:
return self._cached_func(*args, **kwargs)
def process_batch(
self,
batch: Iterable[Any],
concurrency: Optional[int] = None,
) -> List[Trace[T]]:
return parallel_map(
freeze_arguments(self._cached_func), batch, concurrency=concurrency
)
@property
def name(self) -> str:
return snake_case_to_text(self._func.__name__)
@property
def version(self) -> str:
return (
f"{self._version}+{get_function_metadata_store(self._func).model_versions}"
)
@property
def documentation(self) -> str:
return (
f"GreatAI wrapper for interacting with the `{self._func.__name__}` function.\n\n"
+ (
"\n".join(
line.strip()
for line in (self._func.__doc__ or "").split("\n")
if line.strip()
)
)
)
def _bootstrap_rest_api(self, disable_docs: bool, disable_dashboard: bool) -> None:
self._bootstrap_prediction_endpoint()
if not disable_docs:
bootstrap_docs_endpoints(self.app)
if not disable_dashboard:
bootstrap_dashboard(
self.app,
function_name=self._func.__name__,
documentation=self.documentation,
)
bootstrap_trace_endpoints(self.app)
bootstrap_feedback_endpoints(self.app)
self._bootstrap_meta_endpoints()
def _bootstrap_prediction_endpoint(self) -> None:
router = APIRouter(
prefix="/predict",
tags=["predictions"],
)
schema = self._get_schema()
@router.post("/", status_code=status.HTTP_200_OK, response_model=Trace[T])
@use_http_exceptions
def predict(input_value: schema) -> Trace[T]: # type: ignore
return self(**cast(BaseModel, input_value).dict())
self.app.include_router(router)
def _get_schema(self) -> Type[BaseModel]:
signature = inspect.signature(self._func)
parameters = {
p.name: (
p.annotation if p.annotation != inspect._empty else Any,
p.default if p.default != inspect._empty else ...,
)
for p in signature.parameters.values()
if p.name in get_function_metadata_store(self._func).input_parameter_names
}
schema: Type[BaseModel] = create_model("InputModel", **parameters) # type: ignore
return schema
def _bootstrap_meta_endpoints(self) -> None:
router = APIRouter(
tags=["meta"],
)
@router.get("/health", status_code=status.HTTP_200_OK)
def check_health() -> HealthCheckResponse:
hits, misses, maxsize, cache_size = self._cached_func.cache_info()
cache_statistics = CacheStatistics(
hits=hits, misses=misses, size=cache_size, max_size=maxsize
)
return HealthCheckResponse(
is_healthy=True, cache_statistics=cache_statistics
)
@router.get(
"/version", response_model=ApiMetadata, status_code=status.HTTP_200_OK
)
def get_version() -> ApiMetadata:
return ApiMetadata(
name=self.name,
version=self.version,
documentation=self.documentation,
configuration=get_context().to_flat_dict(),
)
self.app.include_router(router)

View file

@ -1,4 +0,0 @@
from .bootstrap_dashboard import bootstrap_dashboard
from .bootstrap_docs_endpoints import bootstrap_docs_endpoints
from .bootstrap_feedback_endpoints import bootstrap_feedback_endpoints
from .bootstrap_trace_endpoints import bootstrap_trace_endpoints

View file

@ -1,27 +0,0 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from ...constants import DASHBOARD_PATH
from .dashboard import create_dash_app
PATH = Path(__file__).parent.resolve()
def bootstrap_dashboard(app: FastAPI, function_name: str, documentation: str) -> None:
dash_app = create_dash_app(function_name, documentation)
app.mount(DASHBOARD_PATH, WSGIMiddleware(dash_app))
@app.get("/", include_in_schema=False)
def redirect_to_entrypoint() -> RedirectResponse:
return RedirectResponse(DASHBOARD_PATH)
app.mount(
"/assets",
StaticFiles(directory=PATH / "dashboard/assets"),
name="static",
)

View file

@ -1,14 +0,0 @@
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import RedirectResponse
from starlette.responses import HTMLResponse
def bootstrap_docs_endpoints(app: FastAPI) -> None:
@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_docs() -> RedirectResponse:
return RedirectResponse("/docs")

View file

@ -1,44 +0,0 @@
from typing import Any
from fastapi import APIRouter, FastAPI, HTTPException, Response, status
from ...context import get_context
from ...views import EvaluationFeedbackRequest
def bootstrap_feedback_endpoints(app: FastAPI) -> None:
router = APIRouter(
prefix="/traces/{trace_id}/feedback",
tags=["feedback"],
)
@router.put("/", status_code=status.HTTP_202_ACCEPTED)
def set_feedback(trace_id: str, input: EvaluationFeedbackRequest) -> Response:
trace = get_context().tracing_database.get(trace_id)
if trace is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
trace.feedback = input.feedback
get_context().tracing_database.update(trace_id, trace)
return Response(status_code=status.HTTP_202_ACCEPTED)
@router.get("/", status_code=status.HTTP_200_OK)
def get_feedback(trace_id: str) -> Any:
trace = get_context().tracing_database.get(trace_id)
if trace is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return trace.feedback
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT)
def delete_feedback(trace_id: str) -> Any:
trace = get_context().tracing_database.get(trace_id)
if trace is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
trace.feedback = None
get_context().tracing_database.update(trace_id, trace)
return Response(status_code=status.HTTP_204_NO_CONTENT)
app.include_router(router)

View file

@ -1,44 +0,0 @@
from typing import List
from fastapi import APIRouter, FastAPI, HTTPException, Response, status
from ...context import get_context
from ...views import Query, Trace
def bootstrap_trace_endpoints(app: FastAPI) -> None:
router = APIRouter(
prefix="/traces",
tags=["traces"],
)
@router.post("/", status_code=status.HTTP_200_OK, response_model=List[Trace])
def query_traces(
query: Query,
skip: int = 0,
take: int = 100,
) -> List[Trace]:
return get_context().tracing_database.query(
conjunctive_filters=query.filter,
conjunctive_tags=query.conjunctive_tags,
since=query.since,
until=query.until,
has_feedback=query.has_feedback,
sort_by=query.sort,
skip=skip,
take=take,
)[0]
@router.get("/{trace_id}", status_code=status.HTTP_200_OK, response_model=Trace)
def get_trace(trace_id: str) -> Trace:
result = get_context().tracing_database.get(trace_id)
if result is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return result
@router.delete("/{trace_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_trace(trace_id: str) -> Response:
get_context().tracing_database.delete(trace_id)
return Response(status_code=status.HTTP_204_NO_CONTENT)
app.include_router(router)

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