From f67ce3e9ac3a13cb454f446c202fc8dece79e9f4 Mon Sep 17 00:00:00 2001 From: luandalmazo Date: Fri, 7 Aug 2026 14:05:24 -0300 Subject: [PATCH] add get_projects --- datamint/api/client.py | 4 +- datamint/api/endpoints/model_types.py | 6 + datamint/api/endpoints/models_api.py | 27 +++- docs/source/client_api_content.rst | 8 ++ .../02_model_registry.ipynb | 120 +++++++++++------- 5 files changed, 119 insertions(+), 46 deletions(-) diff --git a/datamint/api/client.py b/datamint/api/client.py index 7f5df99f..4c6fcd91 100644 --- a/datamint/api/client.py +++ b/datamint/api/client.py @@ -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 @@ -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: diff --git a/datamint/api/endpoints/model_types.py b/datamint/api/endpoints/model_types.py index 99bd7486..2f9d43f0 100644 --- a/datamint/api/endpoints/model_types.py +++ b/datamint/api/endpoints/model_types.py @@ -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 @@ -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) diff --git a/datamint/api/endpoints/models_api.py b/datamint/api/endpoints/models_api.py index e93d4ef9..872dd012 100644 --- a/datamint/api/endpoints/models_api.py +++ b/datamint/api/endpoints/models_api.py @@ -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. @@ -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]: @@ -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. diff --git a/docs/source/client_api_content.rst b/docs/source/client_api_content.rst index 83e2afec..872adc04 100644 --- a/docs/source/client_api_content.rst +++ b/docs/source/client_api_content.rst @@ -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 ++++++++++++++++++++++++++++++++++ diff --git a/notebooks/04_experiment_tracking/02_model_registry.ipynb b/notebooks/04_experiment_tracking/02_model_registry.ipynb index bffc4085..c09e7a22 100644 --- a/notebooks/04_experiment_tracking/02_model_registry.ipynb +++ b/notebooks/04_experiment_tracking/02_model_registry.ipynb @@ -5,21 +5,21 @@ "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()`" ] }, { @@ -27,8 +27,8 @@ "id": "ff7cc0e3", "metadata": {}, "source": [ - "## Setup", - "", + "## Setup\n", + "\n", "Update `datamint` first if needed, then configure your API key." ] }, @@ -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)." ] }, @@ -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." ] }, @@ -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." ] }, @@ -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." ] }, @@ -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." ] }, @@ -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." ] }, @@ -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 ` 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 ` to `datamint upload` with a name that doesn't\n", + "exist yet." + ] } ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "env", "language": "python", "name": "python3" }, @@ -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 -} \ No newline at end of file +}