Dataset Repository Architecture¶
The dataset repository provides a unified API for dataset storage operations, enabling metaseed-hub and other integrations to swap in custom storage backends (e.g., database) while the standalone metaseed uses filesystem storage.
Overview¶
graph TB
subgraph Consumers
UI[Web UI]
MCP[MCP Server]
API[REST API]
end
subgraph Manager["Dataset Manager"]
DM[DatasetManager]
end
subgraph Repositories["Repository Implementations"]
FSR[FilesystemDatasetRepository]
DBR[DatabaseDatasetRepository - Hub]
end
subgraph Storage["Storage Backends"]
FILE[JSON Files]
DB[Database]
end
UI --> DM
MCP --> DM
API --> DM
DM --> FSR
DM --> DBR
FSR --> FILE
DBR --> DB
Components¶
| Component | Location | Responsibility |
|---|---|---|
| DatasetRepository | metaseed.repositories.dataset_repository |
Abstract storage interface |
| DatasetInfo | metaseed.repositories.dataset_repository |
Summary info for listing |
| DatasetData | metaseed.repositories.dataset_repository |
Full dataset contents |
| FilesystemDatasetRepository | metaseed.repositories.filesystem_dataset |
JSON file-based storage |
| DatasetManager | metaseed.ui.dataset_manager |
Business logic + state integration |
| DatasetManagerFactory | metaseed.ui.dataset_manager |
Creates managers bound to an AppState |
DatasetRepository Interface¶
The repository interface defines the storage contract. Backends (filesystem,
database, object store) implement it and are injected via DatasetManagerFactory.
from metaseed.repositories import DatasetRepository, DatasetInfo, DatasetData
class DatasetRepository(ABC):
def list(self) -> list[DatasetInfo]: ...
def save(self, name: str, data: DatasetData) -> DatasetInfo: ...
def load(self, name: str) -> DatasetData: ...
def delete(self, name: str) -> bool: ...
def exists(self, name: str) -> bool: ...
@staticmethod
def validate_name(name: str) -> str | None: ...
Data Classes¶
DatasetInfo¶
Summary information for listing datasets:
@dataclass
class DatasetInfo:
name: str # Dataset identifier
profile: str # Profile name (e.g., "miappe")
version: str # Profile version (e.g., "1.2")
entity_count: int # Number of entities
modified: str # ISO timestamp
skipped_count: int = 0 # Entities in the file a load could not build
A load returns a DatasetInfo whose entity_count is the number of entities it built and whose skipped_count is the number in the file it could not build: one with no _type, a type the profile lacks, or values the entity cannot be constructed from. Each is logged with the reason. load_status reads the two counts: loaded when nothing was skipped, partially_loaded when some entities were, and not_loaded when the file held entities and none loaded. The MCP load_dataset tool and POST /api/datasets/load answer with that status, and the dataset page shows a warning naming the count, so a load that lost entities is not reported as a success (#348).
A dataset whose profile version is not installed cannot be built at all: the specification that defines its entities is absent. A dataset is portable, and its file names a profile a user-defined specification provides, so this happens when a dataset arrives from another machine or its specification was removed. Opening it from the web interface does not load it: the state of the dataset already open is left as it was, and the datasets page says which profile and version the dataset needs and which versions of that profile are installed (data-testid="dataset-profile-missing"). Loading first and failing on the specification left the previous dataset cleared and answered with a server error, which every graph poll then repeated.
DatasetData¶
Full dataset contents for save/load:
@dataclass
class DatasetData:
name: str
profile: str
version: str
entities: list[dict] # Serialized entity data
modified: str
Usage Patterns¶
Standalone Metaseed (Default)¶
Uses filesystem storage automatically. A DatasetManagerFactory creates a
manager bound to an AppState (the default factory uses
FilesystemDatasetRepository):
from metaseed.ui.dataset_manager import DatasetManagerFactory
from metaseed.ui.state import AppState
state = AppState(profile="miappe")
manager = DatasetManagerFactory().get_manager(state)
# List datasets
datasets = manager.list_datasets()
# Save current state
info = manager.save_dataset("my-experiment")
# Load dataset
info = manager.load_dataset("my-experiment")
# Delete dataset
manager.delete_dataset("old-experiment")
Inside a request handler, use resolve_dataset_manager(app, state) instead of
constructing a factory directly; it reuses the MCP-session factory when one is
attached to the app so UI and MCP operations share a repository:
from metaseed.ui.dataset_manager import resolve_dataset_manager
manager = resolve_dataset_manager(app, state)
info = manager.save_dataset("my-experiment")
metaseed-hub Integration (Custom Backend)¶
metaseed-hub swaps in a custom DatasetRepository by constructing a
DatasetManagerFactory with it. The factory then hands out managers backed by
that repository instead of the default filesystem store:
from metaseed.repositories import DatasetRepository, DatasetInfo, DatasetData
from metaseed.ui.dataset_manager import DatasetManagerFactory
class DatabaseDatasetRepository(DatasetRepository):
"""Database-backed dataset storage for metaseed-hub."""
def __init__(self, session):
self._session = session
def list(self) -> list[DatasetInfo]:
...
def save(self, name: str, data: DatasetData) -> DatasetInfo:
error = self.validate_name(name)
if error:
raise ValueError(error)
...
def load(self, name: str) -> DatasetData:
...
def delete(self, name: str) -> bool:
...
def exists(self, name: str) -> bool:
...
# Build a factory bound to the custom repository, then create managers from it.
factory = DatasetManagerFactory(sync_repo=DatabaseDatasetRepository(session))
manager = factory.get_manager(state)
The factory is attached to the app (via the MCP context) so that
resolve_dataset_manager(app, state) returns managers using this backend for
every request in the session.
Direct Repository Access¶
For scripts or external tools:
from metaseed.repositories import FilesystemDatasetRepository, DatasetData
# Custom storage location
repo = FilesystemDatasetRepository(Path("/data/metaseed/datasets"))
# List available datasets
for info in repo.list():
print(f"{info.name}: {info.profile}/{info.version} ({info.entity_count} entities)")
# Load and inspect
data = repo.load("experiment-2024")
for entity in data.entities:
print(f" {entity['_type']}: {entity.get('title', entity.get('unique_id'))}")
Dependency Injection API¶
Managers are created through DatasetManagerFactory and resolved per request
with resolve_dataset_manager:
from metaseed.ui.dataset_manager import (
DatasetManager, # Business logic + state integration
DatasetManagerFactory, # Creates managers bound to an AppState
resolve_dataset_manager, # Resolves the manager for a request
)
| Symbol | Purpose |
|---|---|
DatasetManagerFactory(sync_repo=...) |
Holds the repository; get_manager(state) returns a manager for that state |
DatasetManagerFactory.get_manager(state) |
Get or create the DatasetManager bound to state |
resolve_dataset_manager(app, state) |
Prefer the MCP-session factory attached to app, else a default factory |
The factory caches managers per AppState using a WeakValueDictionary, so
managers are garbage-collected when their state is released.
Backward Compatibility¶
The metaseed.ui.datasets module provides backward-compatible functions:
from metaseed.ui.datasets import (
list_datasets, # Returns list[dict]
save_dataset, # state, name -> dict
load_dataset, # state, name -> dict
delete_dataset, # name -> bool
get_current_dataset_name,
set_current_dataset_name,
auto_save,
)
These delegate to DatasetManager internally. auto_save returns None on success and the failure reason otherwise; the edit stays in the session, and the UI and MCP tools report that it was not saved.
File Format¶
FilesystemDatasetRepository stores datasets as JSON. A save writes to a temporary file beside the target and renames it over the target, so a save that fails partway leaves the previous version of the dataset intact.
{
"name": "my-experiment",
"profile": "miappe",
"version": "1.2",
"modified": "2024-01-15T10:30:00",
"entities": [
{
"_type": "Investigation",
"_parent_unique_id": null,
"unique_id": "INV-001",
"title": "My Investigation"
},
{
"_type": "Study",
"_parent_unique_id": "INV-001",
"_parent_field": "studies",
"unique_id": "STU-001",
"title": "Study One",
"investigation_id": "INV-001"
}
]
}
_parent_field names the parent field that holds the entity. A parent can hold one entity type in several fields — a DCAT catalogue's creator and publisher are both Agents — so the type alone does not say which (ADR 006). A file written before _parent_field existed is read by taking the parent field whose value names the entity; when no field names it and the parent has exactly one field of that type, that field is used. An entity that matches neither stays under its parent without a field, and loading logs a warning naming the candidate fields.
Design Principles¶
The dataset repository follows the same repository-layer principles documented
for the entity repository: dependency
injection, interface segregation, single responsibility, and open/closed. In
addition, the metaseed.ui.datasets module preserves a backward-compatible
function API that delegates to DatasetManager internally.