Skip to content

Commit 36ba2cc

Browse files
authored
feat: migrate remainder of User Story from Robot to pytest (and DocArtifact) (kubeflow#1331)
* feat: migrate remainder of User Story from Robot to pytest (and DocArtifact) - Migrate Robot Framework user stories to pytest with comprehensive test coverage: - Model name storage and validation - Model description storage and updates - Longer documentation storage via DocArtifact - Enhance ModelVersion with registered_model_id field and improved documentation - Add DocArtifact create() and update() methods with proper artifact type mapping - Improve Makefile deployment with conditional image building (helpful locally) - Add migration comments to existing Robot Framework tests per convention This change completes the DocArtifact implementation and modernizes the test suite by migrating user stories from Robot Framework to pytest for better maintainability and as previously agreed and enacted. With this PR, all Robot UserStor-ies are migrated to pytest Signed-off-by: Matteo Mortari <matteo.mortari@gmail.com> * linting Signed-off-by: Matteo Mortari <matteo.mortari@gmail.com> * implement code review feedback Signed-off-by: Matteo Mortari <matteo.mortari@gmail.com> --------- Signed-off-by: Matteo Mortari <matteo.mortari@gmail.com>
1 parent 929b423 commit 36ba2cc

5 files changed

Lines changed: 122 additions & 6 deletions

File tree

clients/python/Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ all: install tidy
22

33
IMG_VERSION ?= latest
44
IMG ?= ghcr.io/kubeflow/model-registry/server:${IMG_VERSION}
5+
BUILD_IMAGE ?= true # whether to build the MR server image
56

67
.PHONY: install
78
install:
@@ -16,7 +17,11 @@ clean:
1617

1718
.PHONY: deploy-latest-mr
1819
deploy-latest-mr:
19-
cd ../../ && IMG_VERSION=${IMG_VERSION} IMG=${IMG} make image/build ARGS="--load$(if ${DEV_BUILD}, --target dev-build)" && LOCAL=1 ./scripts/deploy_on_kind.sh
20+
cd ../../ && \
21+
$(if $(filter true,$(BUILD_IMAGE)),\
22+
IMG_VERSION=${IMG_VERSION} IMG=${IMG} make image/build ARGS="--load$(if ${DEV_BUILD}, --target dev-build)" && \
23+
) \
24+
LOCAL=1 ./scripts/deploy_on_kind.sh
2025
kubectl port-forward -n kubeflow services/model-registry-service 8080:8080 & echo $$! >> .port-forwards.pid
2126

2227
.PHONY: deploy-test-minio

clients/python/src/model_registry/types/artifacts.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
)
2323
from mr_openapi import (
2424
ArtifactState,
25+
DocArtifactCreate,
26+
DocArtifactUpdate,
2527
ModelArtifactCreate,
2628
ModelArtifactUpdate,
2729
)
@@ -87,11 +89,23 @@ class DocArtifact(Artifact):
8789

8890
@override
8991
def create(self, **kwargs) -> Any:
90-
raise NotImplementedError
92+
"""Create a new DocArtifactCreate object."""
93+
return DocArtifactCreate(
94+
customProperties=self._map_custom_properties(),
95+
**self._props_as_dict(exclude=("id", "custom_properties")),
96+
artifactType="doc-artifact",
97+
**kwargs,
98+
)
9199

92100
@override
93101
def update(self, **kwargs) -> Any:
94-
raise NotImplementedError
102+
"""Create a new DocArtifactUpdate object."""
103+
return DocArtifactUpdate(
104+
customProperties=self._map_custom_properties(),
105+
**self._props_as_dict(exclude=("id", "name", "custom_properties")),
106+
artifactType="doc-artifact",
107+
**kwargs,
108+
)
95109

96110
@override
97111
def as_basemodel(self) -> DocArtifactBaseModel:

clients/python/src/model_registry/types/contexts.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,17 @@ class ModelVersion(BaseResourceModel):
3636
3737
Attributes:
3838
name: Name of this version.
39-
author: Author of the model version.
40-
description: Description of the object.
39+
author: Author of this model version.
40+
state: Status of this model version.
41+
description: Description of this object.
4142
external_id: Customizable ID. Has to be unique among instances of the same type.
4243
artifacts: Artifacts associated with this version.
4344
"""
4445

4546
name: str
4647
author: str | None = None
4748
state: ModelVersionState = ModelVersionState.LIVE
49+
registered_model_id: str | None = None
4850

4951
@override
5052
def create(self, *, registered_model_id: str, **kwargs) -> ModelVersionCreate: # type: ignore[override]
@@ -75,6 +77,7 @@ def from_basemodel(cls, source: ModelVersionBaseModel) -> ModelVersion:
7577
author=source.author,
7678
description=source.description,
7779
external_id=source.external_id,
80+
registered_model_id=source.registered_model_id,
7881
create_time_since_epoch=source.create_time_since_epoch,
7982
last_update_time_since_epoch=source.last_update_time_since_epoch,
8083
custom_properties=cls._unmap_custom_properties(source.custom_properties)

clients/python/tests/test_client.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from model_registry import ModelRegistry, utils
88
from model_registry.exceptions import StoreError
99
from model_registry.types import ModelArtifact
10+
from model_registry.types.artifacts import DocArtifact
1011

1112
from .extras.async_task_runner import AsyncTaskRunner
1213

@@ -22,23 +23,30 @@ def test_secure_client():
2223

2324
@pytest.mark.e2e
2425
async def test_register_new(client: ModelRegistry):
26+
"""As a MLOps engineer I would like to store Model name
27+
"""
2528
name = "test_model"
2629
version = "1.0.0"
2730
rm = client.register_model(
2831
name,
29-
"s3",
32+
"https://acme.org/something",
3033
model_format_name="test_format",
3134
model_format_version="test_version",
3235
version=version,
3336
)
3437
assert rm.id
38+
assert rm.name == name # check the Model name
3539

3640
mr_api = client._api
3741
mv = await mr_api.get_model_version_by_params(rm.id, version)
3842
assert mv
3943
assert mv.id
44+
assert mv.name == version
45+
assert mv.registered_model_id == rm.id
46+
4047
ma = await mr_api.get_model_artifact_by_params(name, mv.id)
4148
assert ma
49+
assert ma.uri == "https://acme.org/something"
4250

4351

4452
@pytest.mark.e2e
@@ -1086,3 +1094,85 @@ def test_upload_large_model_file(
10861094
mv = client.get_model_artifact(name="large_test_model", version=version)
10871095
assert mv
10881096
assert mv.name == "large_test_model"
1097+
1098+
1099+
@pytest.mark.e2e
1100+
async def test_as_mlops_engineer_i_would_like_to_update_a_description_of_the_model(client: ModelRegistry):
1101+
"""As a MLOps engineer I would like to update a description of the model
1102+
"""
1103+
name = "test_model"
1104+
version = "1.0.0"
1105+
rm = client.register_model(
1106+
name,
1107+
"https://acme.org/something",
1108+
model_format_name="test_format",
1109+
model_format_version="test_version",
1110+
version=version,
1111+
owner="me",
1112+
description="Lorem ipsum dolor sit amet",
1113+
)
1114+
assert rm.id
1115+
1116+
rm.description = "New description"
1117+
rm =client.update(rm)
1118+
assert rm.description == "New description"
1119+
assert rm.owner == "me"
1120+
1121+
1122+
@pytest.mark.e2e
1123+
async def test_as_mlops_engineer_i_would_like_to_store_a_description_of_the_model(client: ModelRegistry):
1124+
"""As a MLOps engineer I would like to store a description of the model
1125+
Note: on Creation, the Description belongs to the Model Version; we could improve the logic to maintain it for the Registered Model if it's not already existing
1126+
"""
1127+
name = "test_model"
1128+
version = "1.0.0"
1129+
rm = client.register_model(
1130+
name,
1131+
"https://acme.org/something",
1132+
model_format_name="test_format",
1133+
model_format_version="test_version",
1134+
version=version,
1135+
description="consectetur adipiscing elit",
1136+
)
1137+
assert rm.id
1138+
1139+
mr_api = client._api
1140+
mv = await mr_api.get_model_version_by_params(rm.id, version)
1141+
assert mv
1142+
assert mv.id
1143+
assert mv.description == "consectetur adipiscing elit"
1144+
ma = await mr_api.get_model_artifact_by_params(name, mv.id)
1145+
assert ma
1146+
1147+
rm.description = "Lorem ipsum dolor sit amet"
1148+
assert client.update(rm).description == "Lorem ipsum dolor sit amet"
1149+
mv.description = "consectetur adipiscing elit2"
1150+
assert client.update(mv).description == "consectetur adipiscing elit2"
1151+
ma.description = "sed do eiusmod tempor incididunt"
1152+
assert client.update(ma).description == "sed do eiusmod tempor incididunt"
1153+
1154+
1155+
@pytest.mark.e2e
1156+
async def test_as_mlops_engineer_i_would_like_to_store_a_longer_documentation_for_the_model(client: ModelRegistry):
1157+
"""As a MLOps engineer I would like to store a longer documentation for the model
1158+
"""
1159+
name = "test_model"
1160+
version = "1.0.0"
1161+
rm = client.register_model(
1162+
name,
1163+
"https://acme.org/something",
1164+
model_format_name="test_format",
1165+
model_format_version="test_version",
1166+
version=version,
1167+
description="consectetur adipiscing elit",
1168+
)
1169+
assert rm.id
1170+
1171+
mr_api = client._api
1172+
mv = await mr_api.get_model_version_by_params(rm.id, version)
1173+
assert mv
1174+
assert mv.id
1175+
1176+
da = await mr_api.upsert_model_version_artifact(DocArtifact(uri="https://README.md"), mv.id)
1177+
assert da
1178+
assert da.uri == "https://README.md"

test/robot/UserStory.robot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ These User Story(-ies) are defined in the PM document
1010

1111
*** Test Cases ***
1212
As a MLOps engineer I would like to store Model name
13+
# MIGRATED to test_register_new in pytest
1314
${rId} Given I create a RegisteredModel having name=${name}
1415
${vId} And I create a child ModelVersion having registeredModelID=${rId} name=v1
1516
${aId} And I create a child ModelArtifact having modelversionId=${vId} uri=s3://12345
@@ -22,6 +23,7 @@ As a MLOps engineer I would like to store Model name
2223
And Should be equal ${r["uri"]} s3://12345
2324

2425
As a MLOps engineer I would like to store a description of the model
26+
# MIGRATED to test_as_mlops_engineer_i_would_like_to_store_a_description_of_the_model in pytest
2527
Set To Dictionary ${registered_model} description=Lorem ipsum dolor sit amet name=${name}
2628
Set To Dictionary ${model_version} description=consectetur adipiscing elit
2729
Set To Dictionary ${model_artifact} description=sed do eiusmod tempor incididunt
@@ -42,6 +44,7 @@ As a MLOps engineer I would like to store a description of the model
4244
And Should be equal ${r["description"]} sed do eiusmod tempor incididunt
4345

4446
As a MLOps engineer I would like to update a description of the model
47+
# MIGRATED to test_as_mlops_engineer_i_would_like_to_update_a_description_of_the_model in pytest
4548
Set To Dictionary ${registered_model} description=Lorem ipsum dolor sit amet name=${name} owner=me
4649
${rId} Given I create a RegisteredModel payload=${registered_model}
4750
&{registered_model_update} Create dictionary description=New description
@@ -51,6 +54,7 @@ As a MLOps engineer I would like to update a description of the model
5154
And Should be equal ${r["owner"]} me
5255

5356
As a MLOps engineer I would like to store a longer documentation for the model
57+
# MIGRATED to test_as_mlops_engineer_i_would_like_to_store_a_longer_documentation_for_the_model in pytest
5458
Set To Dictionary ${registered_model} description=Lorem ipsum dolor sit amet name=${name}
5559
Set To Dictionary ${model_version} description=consectetur adipiscing elit
5660
Set To Dictionary ${model_artifact} description=sed do eiusmod tempor incididunt

0 commit comments

Comments
 (0)