Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion datamint/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def _get_endpoint(self, name: str, is_mlflow: bool = False):
kwargs['projects_api'] = self.projects
elif name == 'models':
kwargs['deploy_api'] = self.deploy
kwargs['projects_api'] = self.projects
endpoint = api_class(self.config, client=client, **kwargs)
# Inject this API instance into the endpoint so it can inject into entities
endpoint._api_instance = self
Expand Down Expand Up @@ -191,7 +192,8 @@ def _datasetsinfo(self) -> DatasetsInfoApi:

@property
def models(self) -> ModelsApi:
return self._get_endpoint('models')
"""Access the model registry endpoints (mlflow-adjacent host)."""
return self._get_endpoint('models', is_mlflow=True)

@property
def annotationworklists(self) -> AnnotationWorklistApi:
Expand Down
6 changes: 6 additions & 0 deletions datamint/api/endpoints/model_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from datamint.mlflow.models.tags import DATAMINT_LOGGED_MODEL_ID_TAG

if TYPE_CHECKING:
from datamint.entities.project import Project

from .models_api import ModelsApi


Expand Down Expand Up @@ -141,3 +143,7 @@ def get_metrics(self, version: ModelVersion | None = None) -> dict[str, float]:

def is_deployed(self) -> bool:
return self._api._deploy_api.image_exists(self.name)

def get_projects(self) -> list['Project']:
"""Projects this model is associated with."""
return self._api.get_projects(self.name)
27 changes: 26 additions & 1 deletion datamint/api/endpoints/models_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
if TYPE_CHECKING:
from datamint.entities.project import Project

from .projects_api import ProjectsApi


class ModelsApi(BaseApi):
"""API handler for the model registry.
Expand All @@ -27,14 +29,23 @@ class ModelsApi(BaseApi):
def __init__(self,
config: ApiConfig,
client: httpx.Client | None = None,
deploy_api: DeployModelApi | None = None) -> None:
deploy_api: DeployModelApi | None = None,
projects_api: 'ProjectsApi | None' = None) -> None:
super().__init__(config, client)
self._deploy_api = deploy_api or DeployModelApi(config, client=client)
self._projects_api = projects_api

@property
def _mlflow_client(self) -> mlflow.tracking.MlflowClient:
return mlflow.tracking.MlflowClient()

@property
def projects_api(self) -> 'ProjectsApi':
if self._projects_api is None:
from .projects_api import ProjectsApi
self._projects_api = ProjectsApi(self.config, client=self.client)
return self._projects_api

def get_list(self,
only_deployed: bool = False,
max_results: int | None = None) -> list[Model]:
Expand Down Expand Up @@ -76,6 +87,20 @@ def get_by_name(self, name: str) -> Model | None:
raise
return Model(_raw=raw_model, _api=self)

def get_projects(self, model_name: str, customer_id: str | None = None) -> list['Project']:
"""Get all projects a registered model is associated with.

Args:
model_name: Name of the registered model.
customer_id: Optional customer ID to scope the lookup.
"""
payload = {'model_name': model_name}
if customer_id is not None:
payload['customer_id'] = customer_id
response = self._make_request('POST', 'datamint/api/v1/model-info/get-project', json=payload)
project_ids = response.json().get('project_ids', [])
return [self.projects_api.get_by_id(pid) for pid in project_ids]

def create(self, name: str, description: str | None = None, exists_ok: bool = True) -> Model:
"""Create a new registered model.

Expand Down
8 changes: 8 additions & 0 deletions docs/source/client_api_content.rst
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,14 @@ a Datamint :mod:`~datamint.lightning.trainers`), rather than raising.
``Model.get_supported_modes()``/``get_metrics()`` are shortcuts that delegate
to the latest version when you don't need a specific one.

Find which projects a model belongs to
++++++++++++++++++++++++++++++++++++++

.. code-block:: python

model = api.models.get_by_name("my-model")
projects = model.get_projects() # list[Project]

Model registry (MLflow) operations
++++++++++++++++++++++++++++++++++

Expand Down
120 changes: 76 additions & 44 deletions notebooks/04_experiment_tracking/02_model_registry.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@
"id": "556c43fc",
"metadata": {},
"source": [
"# Model Registry Tutorial",
"",
"`api.models` is a facade over Datamint's MLflow-backed model registry. It wraps MLflow's",
"`RegisteredModel` / `ModelVersion` objects in plain Python objects (`Model`, `ModelVersion`),",
"so you can register, list, and inspect models without learning MLflow's object model.",
"",
"This notebook covers:",
"",
"- Registering a model with `api.models.create()`",
"- Listing and finding models with `api.models.get_list()` / `get_by_name()`",
"- Filtering to deployed models with `only_deployed=True`",
"- Inspecting a model's versions with `model.get_versions()` / `get_latest_version()`",
"- Reading what a version was trained for: `get_task_type()`, `get_supported_modes()`, `get_annotation_specs()`",
"- Reading training metrics with `get_metrics()`",
"- Checking deployment status with `is_deployed()`"
"# Model Registry Tutorial\n",
"\n",
"`api.models` is a facade over Datamint's MLflow-backed model registry. It wraps MLflow's\n",
"`RegisteredModel` / `ModelVersion` objects in plain Python objects (`Model`, `ModelVersion`),\n",
"so you can register, list, and inspect models without learning MLflow's object model.\n",
"\n",
"This notebook covers:\n",
"- Registering a model with `api.models.create()`\n",
"- Listing and finding models with `api.models.get_list()` / `get_by_name()`\n",
"- Filtering to deployed models with `only_deployed=True`\n",
"- Inspecting a model's versions with `model.get_versions()` / `get_latest_version()`\n",
"- Reading what a version was trained for: `get_task_type()`, `get_supported_modes()`, `get_annotation_specs()`\n",
"- Reading training metrics with `get_metrics()`\n",
"- Checking deployment status with `is_deployed()`\n",
"- Finding which projects a model is associated with, with `model.get_projects()`"
]
},
{
"cell_type": "markdown",
"id": "ff7cc0e3",
"metadata": {},
"source": [
"## Setup",
"",
"## Setup\n",
"\n",
"Update `datamint` first if needed, then configure your API key."
]
},
Expand Down Expand Up @@ -59,10 +59,10 @@
"id": "547f3483",
"metadata": {},
"source": [
"## Register A Model",
"",
"Registering ahead of training gives you a stable name to reference later. `create()` returns the",
"existing model instead of raising when one with this name is already registered",
"## Register A Model\n",
"\n",
"Registering ahead of training gives you a stable name to reference later. `create()` returns the\n",
"existing model instead of raising when one with this name is already registered\n",
"(`exists_ok=True` by default)."
]
},
Expand All @@ -84,9 +84,9 @@
"id": "7cc966eb",
"metadata": {},
"source": [
"## List And Find Models",
"",
"`get_list()` returns every registered model; `get_by_name()` returns a single one, or `None` if it",
"## List And Find Models\n",
"\n",
"`get_list()` returns every registered model; `get_by_name()` returns a single one, or `None` if it\n",
"doesn't exist."
]
},
Expand All @@ -110,9 +110,9 @@
"id": "c30b3598",
"metadata": {},
"source": [
"## Filter To Deployed Models",
"",
"Pass `only_deployed=True` to skip models that don't have a deployed image yet. See",
"## Filter To Deployed Models\n",
"\n",
"Pass `only_deployed=True` to skip models that don't have a deployed image yet. See\n",
"`05_deployment/01_deploy_registered_model.ipynb` for how to deploy one."
]
},
Expand All @@ -134,11 +134,11 @@
"id": "fab1e7a0",
"metadata": {},
"source": [
"## Inspect Versions Of A Trained Model",
"",
"The rest of this notebook needs a model with at least one version behind it, typically one",
"registered by a Datamint trainer (see `06_end_to_end`) or by",
"`04_experiment_tracking/01_mlflow_manual_logging.ipynb`. Replace `MODEL_NAME` below with one",
"## Inspect Versions Of A Trained Model\n",
"\n",
"The rest of this notebook needs a model with at least one version behind it, typically one\n",
"registered by a Datamint trainer (see `06_end_to_end`) or by\n",
"`04_experiment_tracking/01_mlflow_manual_logging.ipynb`. Replace `MODEL_NAME` below with one\n",
"you've already trained."
]
},
Expand Down Expand Up @@ -167,10 +167,10 @@
"id": "d3045992",
"metadata": {},
"source": [
"## What A Version Was Trained For",
"",
"If the version was logged with the `datamint` MLflow flavor (true for anything trained through a",
"Datamint trainer), you can read the task type, supported prediction modes, and annotation specs",
"## What A Version Was Trained For\n",
"\n",
"If the version was logged with the `datamint` MLflow flavor (true for anything trained through a\n",
"Datamint trainer), you can read the task type, supported prediction modes, and annotation specs\n",
"straight from the model artifact, without inspecting the training run."
]
},
Expand All @@ -194,10 +194,10 @@
"id": "cdfdcb81",
"metadata": {},
"source": [
"## Training Metrics",
"",
"`get_metrics()` reads the metrics logged for this version's training run. It returns `{}` instead",
"of raising when there's no training run behind the version, for example a model registered from",
"## Training Metrics\n",
"\n",
"`get_metrics()` reads the metrics logged for this version's training run. It returns `{}` instead\n",
"of raising when there's no training run behind the version, for example a model registered from\n",
"outside Datamint."
]
},
Expand All @@ -221,16 +221,48 @@
"model.get_metrics() # shortcut, uses the latest version"
]
},
{
"cell_type": "markdown",
"id": "a4281df3",
"metadata": {},
"source": [
"## Find Which Projects A Model Belongs To\n",
"\n",
"A registered model can be associated with one or more Datamint projects. `model.get_projects()`\n",
"returns the full `Project` objects, resolved via `api.projects`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ac7a0fe2",
"metadata": {},
"outputs": [],
"source": [
"projects = model.get_projects()\n",
"print(f\"{MODEL_NAME} belongs to {len(projects)} project(s)\")\n",
"\n",
"for project in projects:\n",
" print(project.id, project.name)"
]
},
{
"cell_type": "markdown",
"id": "5edb693f",
"metadata": {},
"source": "## Next Steps\n\nOnce you have a model version you're happy with, alias it and deploy it, see\n`05_deployment/01_deploy_registered_model.ipynb`. Registered models are also created\nautomatically when you pass `--ai-model <name>` to `datamint upload` with a name that doesn't\nexist yet."
"source": [
"## Next Steps\n",
"\n",
"Once you have a model version you're happy with, alias it and deploy it, see\n",
"`05_deployment/01_deploy_registered_model.ipynb`. Registered models are also created\n",
"automatically when you pass `--ai-model <name>` to `datamint upload` with a name that doesn't\n",
"exist yet."
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"display_name": "env",
"language": "python",
"name": "python3"
},
Expand All @@ -244,9 +276,9 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.13"
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
}
Loading