Skip to content

Commit

Permalink
Blob metadata (#1154)
Browse files Browse the repository at this point in the history
* Add blob metadata

* fix

* change base config class

* make backward-compatible
  • Loading branch information
mike0sv committed Jun 20, 2024
1 parent fab873d commit 37adb48
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 28 deletions.
6 changes: 3 additions & 3 deletions src/evidently/ui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@

from evidently._pydantic_compat import SecretStr
from evidently.ui.components.base import AppBuilder
from evidently.ui.config import Config
from evidently.ui.config import AppConfig
from evidently.ui.config import load_config
from evidently.ui.config import settings
from evidently.ui.local_service import LocalConfig
from evidently.ui.security.token import TokenSecurityComponent
from evidently.ui.storage.common import EVIDENTLY_SECRET_ENV


def create_app(config: Config):
def create_app(config: AppConfig):
with config.context() as ctx:
builder = AppBuilder(ctx)
ctx.apply(builder)
Expand All @@ -21,7 +21,7 @@ def create_app(config: Config):
return app


def run(config: Config):
def run(config: AppConfig):
app = create_app(config)
uvicorn.run(app, host=config.service.host, port=config.service.port)

Expand Down
28 changes: 18 additions & 10 deletions src/evidently/ui/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,19 @@
from evidently.utils.dashboard import TemplateParams


class BlobMetadata(BaseModel):
id: BlobID
size: Optional[int]


class SnapshotMetadata(BaseModel):
id: UUID4
name: Optional[str] = None
timestamp: datetime.datetime
metadata: Dict[str, MetadataValueType]
tags: List[str]
is_report: bool
blob_id: BlobID
blob: "BlobMetadata"

_project: "Project" = PrivateAttr(None)
_dashboard_info: "DashboardInfo" = PrivateAttr(None)
Expand All @@ -81,15 +86,15 @@ def bind(self, project: "Project"):
return self

@classmethod
def from_snapshot(cls, snapshot: Snapshot, blob_id: BlobID) -> "SnapshotMetadata":
def from_snapshot(cls, snapshot: Snapshot, blob: "BlobMetadata") -> "SnapshotMetadata":
return SnapshotMetadata(
id=snapshot.id,
name=snapshot.name,
timestamp=snapshot.timestamp,
metadata=snapshot.metadata,
tags=snapshot.tags,
is_report=snapshot.is_report,
blob_id=blob_id,
blob=blob,
)

@property
Expand Down Expand Up @@ -250,7 +255,7 @@ def list_projects(self, project_ids: Optional[Set[ProjectID]]) -> List[Project]:
raise NotImplementedError

@abstractmethod
def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob_id: str):
def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob: "BlobMetadata"):
raise NotImplementedError

@abstractmethod
Expand Down Expand Up @@ -286,16 +291,19 @@ class BlobStorage(ABC):
def open_blob(self, id: BlobID):
raise NotImplementedError

def put_blob(self, path: str, obj):
def put_blob(self, blob_id: str, obj):
raise NotImplementedError

def get_snapshot_blob_id(self, project_id: ProjectID, snapshot: Snapshot) -> BlobID:
raise NotImplementedError

def put_snapshot(self, project_id: ProjectID, snapshot: Snapshot) -> BlobID:
def put_snapshot(self, project_id: ProjectID, snapshot: Snapshot) -> BlobMetadata:
id = self.get_snapshot_blob_id(project_id, snapshot)
self.put_blob(id, json.dumps(snapshot.dict(), cls=NumpyEncoder))
return id
return self.get_blob_metadata(id)

def get_blob_metadata(self, blob_id: BlobID) -> BlobMetadata:
raise NotImplementedError


class DataStorage(ABC):
Expand Down Expand Up @@ -696,8 +704,8 @@ def add_snapshot(self, user_id: UserID, project_id: ProjectID, snapshot: Snapsho
user.id, EntityType.Project, project_id, Permission.PROJECT_SNAPSHOT_ADD
):
raise ProjectNotFound() # todo: better exception
blob_id = self.blob.put_snapshot(project_id, snapshot)
self.metadata.add_snapshot(project_id, snapshot, blob_id)
blob = self.blob.put_snapshot(project_id, snapshot)
self.metadata.add_snapshot(project_id, snapshot, blob)
self.data.extract_points(project_id, snapshot)

def delete_snapshot(self, user_id: UserID, project_id: ProjectID, snapshot_id: SnapshotID):
Expand Down Expand Up @@ -733,7 +741,7 @@ def load_snapshot(
) -> Snapshot:
if isinstance(snapshot, SnapshotID):
snapshot = self.get_snapshot_metadata(user_id, project_id, snapshot)
with self.blob.open_blob(snapshot.blob_id) as f:
with self.blob.open_blob(snapshot.blob.id) as f:
return parse_obj_as(Snapshot, json.load(f))

def get_snapshot_metadata(
Expand Down
16 changes: 14 additions & 2 deletions src/evidently/ui/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ def validate(self):


class Config(BaseModel):
security: SecurityComponent
service: ServiceComponent
additional_components: Dict[str, Component] = {}

_components: List[Component] = PrivateAttr(default_factory=list)
Expand All @@ -99,6 +97,11 @@ def context(self) -> Iterator[ConfigContext]:
del self._ctx


class AppConfig(Config):
security: SecurityComponent
service: ServiceComponent


TConfig = TypeVar("TConfig", bound=Config)


Expand Down Expand Up @@ -127,6 +130,15 @@ def load_config(config_type: Type[TConfig], box: dict) -> TConfig:
return config_type(additional_components=components, **named_components)


def load_config_from_file(cls: Type[TConfig], path: str, envvar_prefix: str = "EVIDENTLY") -> TConfig:
dc = dynaconf.Dynaconf(
envvar_prefix=envvar_prefix,
)
dc.configure(settings_module=path)
config = load_config(cls, dc)
return config


settings = dynaconf.Dynaconf(
envvar_prefix="EVIDENTLY",
)
4 changes: 2 additions & 2 deletions src/evidently/ui/local_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from evidently.ui.components.storage import LocalStorageComponent
from evidently.ui.components.storage import StorageComponent
from evidently.ui.components.telemetry import TelemetryComponent
from evidently.ui.config import Config
from evidently.ui.config import AppConfig
from evidently.ui.errors import EvidentlyServiceError


Expand All @@ -36,7 +36,7 @@ def apply(self, ctx: ComponentContext, builder: AppBuilder):
builder.kwargs["debug"] = self.debug


class LocalConfig(Config):
class LocalConfig(AppConfig):
security: SecurityComponent = NoSecurityComponent()
service: ServiceComponent = LocalServiceComponent()
storage: StorageComponent = LocalStorageComponent()
Expand Down
27 changes: 18 additions & 9 deletions src/evidently/ui/storage/local/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from evidently.test_suite import TestSuite
from evidently.tests.base_test import Test
from evidently.tests.base_test import TestStatus
from evidently.ui.base import BlobMetadata
from evidently.ui.base import BlobStorage
from evidently.ui.base import DataStorage
from evidently.ui.base import MetadataStorage
Expand Down Expand Up @@ -84,6 +85,9 @@ def rmtree(self, path: str):
def invalidate_cache(self, path):
self.fs.invalidate_cache(posixpath.join(self.path, path))

def size(self, path):
return self.fs.size(posixpath.join(self.path, path))


class FSSpecBlobStorage(BlobStorage):
base_path: str
Expand All @@ -104,15 +108,18 @@ def get_snapshot_blob_id(self, project_id: ProjectID, snapshot: Snapshot) -> Blo
return posixpath.join(str(project_id), SNAPSHOTS, str(snapshot.id)) + ".json"

@contextlib.contextmanager
def open_blob(self, path: str):
with self.location.open(path) as f:
def open_blob(self, blob_id: str):
with self.location.open(blob_id) as f:
yield f

def put_blob(self, path: str, obj) -> str:
self.location.makedirs(posixpath.dirname(path))
with self.location.open(path, "w") as f:
def put_blob(self, blob_id: BlobID, obj) -> BlobID:
self.location.makedirs(posixpath.dirname(blob_id))
with self.location.open(blob_id, "w") as f:
f.write(obj)
return path
return blob_id

def get_blob_metadata(self, blob_id: BlobID) -> BlobMetadata:
return BlobMetadata(id=blob_id, size=self.location.size(blob_id))


def load_project(location: FSLocation, path: str) -> Optional[Project]:
Expand Down Expand Up @@ -169,7 +176,9 @@ def reload_snapshot(self, project: Project, snapshot_id: SnapshotID, skip_errors
snapshot_path = posixpath.join(str(project.id), SNAPSHOTS, str(snapshot_id) + ".json")
with self.location.open(snapshot_path) as f:
suite = parse_obj_as(Snapshot, json.load(f))
snapshot = SnapshotMetadata.from_snapshot(suite, snapshot_path).bind(project)
snapshot = SnapshotMetadata.from_snapshot(
suite, BlobMetadata(id=snapshot_path, size=self.location.size(snapshot_path))
).bind(project)
self.snapshots[project.id][snapshot_id] = snapshot
self.snapshot_data[project.id][snapshot_id] = suite
except ValidationError as e:
Expand Down Expand Up @@ -220,11 +229,11 @@ def list_projects(self, project_ids: Optional[Set[ProjectID]]) -> List[Project]:
projects.sort(key=lambda x: x.created_at or default_date, reverse=True)
return projects

def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob_id: str):
def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob: BlobMetadata):
project = self.get_project(project_id)
if project is None:
raise ProjectNotFound()
self.state.snapshots[project_id][snapshot.id] = SnapshotMetadata.from_snapshot(snapshot, blob_id).bind(project)
self.state.snapshots[project_id][snapshot.id] = SnapshotMetadata.from_snapshot(snapshot, blob).bind(project)
self.state.snapshot_data[project_id][snapshot.id] = snapshot

def delete_snapshot(self, project_id: ProjectID, snapshot_id: SnapshotID):
Expand Down
8 changes: 6 additions & 2 deletions src/evidently/ui/workspace/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from evidently._pydantic_compat import parse_obj_as
from evidently.suite.base_suite import Snapshot
from evidently.ui.api.service import EVIDENTLY_APPLICATION_NAME
from evidently.ui.base import BlobMetadata
from evidently.ui.base import BlobStorage
from evidently.ui.base import DataStorage
from evidently.ui.base import MetadataStorage
Expand Down Expand Up @@ -146,7 +147,7 @@ def delete_project(self, project_id: ProjectID):
def list_projects(self, project_ids: Optional[Set[ProjectID]]) -> List[Project]:
return self._request("/api/projects", "GET", response_model=List[Project])

def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob_id: str):
def add_snapshot(self, project_id: ProjectID, snapshot: Snapshot, blob: "BlobMetadata"):
return self._request(f"/api/projects/{project_id}/snapshots", "POST", body=snapshot.dict())

def delete_snapshot(self, project_id: ProjectID, snapshot_id: SnapshotID):
Expand Down Expand Up @@ -182,7 +183,10 @@ def put_blob(self, path: str, obj):
pass

def get_snapshot_blob_id(self, project_id: ProjectID, snapshot: Snapshot) -> BlobID:
pass
return ""

def get_blob_metadata(self, blob_id: BlobID) -> BlobMetadata:
return BlobMetadata(id=blob_id, size=0)


class NoopDataStorage(DataStorage):
Expand Down

0 comments on commit 37adb48

Please sign in to comment.