Skip to content
This repository was archived by the owner on Sep 13, 2023. It is now read-only.

redeploy models #255

Merged
merged 2 commits into from
May 19, 2022
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
2 changes: 2 additions & 0 deletions mlem/api/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ def deploy(
deploy_meta.dump(deploy_meta_or_path, fs, repo, index, external)
else:
deploy_meta = deploy_meta_or_path
if model is not None:
deploy_meta.replace_model(get_model_meta(model))

# ensuring links are working
deploy_meta.get_env()
Expand Down
8 changes: 3 additions & 5 deletions mlem/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any, Optional, Tuple, Type, TypeVar, Union

from mlem.core.base import MlemABC, build_mlem_object
from mlem.core.errors import InvalidArgumentError, WrongMetaType
from mlem.core.errors import InvalidArgumentError
from mlem.core.metadata import load, load_meta
from mlem.core.objects import MlemDataset, MlemModel, MlemObject

Expand All @@ -25,15 +25,13 @@ def get_dataset_value(dataset: Any, batch_size: Optional[int] = None) -> Any:
return dataset


def get_model_meta(model: Any) -> MlemModel:
def get_model_meta(model: Union[str, MlemModel]) -> MlemModel:
if isinstance(model, MlemModel):
if model.get_value() is None:
model.load_value()
return model
if isinstance(model, str):
model = load_meta(model)
if not isinstance(model, MlemModel):
raise WrongMetaType(model, MlemModel)
model = load_meta(model, force_type=MlemModel)
model.load_value()
return model
raise InvalidArgumentError(
Expand Down
4 changes: 2 additions & 2 deletions mlem/contrib/heroku/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from .meta import HerokuEnvMeta, HerokuState
from .meta import HerokuEnv, HerokuState

__all__ = ["HerokuEnvMeta", "HerokuState"]
__all__ = ["HerokuEnv", "HerokuState"]
9 changes: 6 additions & 3 deletions mlem/contrib/heroku/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class HerokuDeploy(MlemDeploy):
team: Optional[str] = None


class HerokuEnvMeta(MlemEnv[HerokuDeploy]):
class HerokuEnv(MlemEnv[HerokuDeploy]):
type: ClassVar = "heroku"
deploy_type: ClassVar = HerokuDeploy
api_key: Optional[str] = None
Expand All @@ -72,12 +72,15 @@ def deploy(self, meta: HerokuDeploy):
meta.state.app = create_app(meta, api_key=self.api_key)
meta.update()

if meta.state.image is None:
redeploy = False
if meta.state.image is None or meta.model_changed():
meta.state.image = build_heroku_docker(
meta.get_model(), meta.state.app.name, api_key=self.api_key
)
meta.update_model_hash()
meta.update()
if meta.state.release_state is None:
redeploy = True
if meta.state.release_state is None or redeploy:
meta.state.release_state = release_docker_app(
meta.state.app.name,
meta.state.image.image_id,
Expand Down
21 changes: 21 additions & 0 deletions mlem/core/objects.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Base classes for meta objects in MLEM
"""
import hashlib
import os
import posixpath
import time
Expand Down Expand Up @@ -341,6 +342,9 @@ def update(self):
with no_echo():
self._write_meta(self.location, False)

def meta_hash(self):
return hashlib.md5(safe_dump(self.dict()).encode("utf8")).hexdigest()


class MlemLink(MlemObject):
path: str
Expand Down Expand Up @@ -693,6 +697,8 @@ class Config:

abs_name: ClassVar[str] = "deploy_state"

model_hash: Optional[str] = None

@abstractmethod
def get_client(self):
raise NotImplementedError
Expand Down Expand Up @@ -824,6 +830,21 @@ def wait_for_status(
)
return False

def model_changed(self):
if self.state is None or self.state.model_hash is None:
return True
return self.get_model().meta_hash() != self.state.model_hash

def update_model_hash(self, model: Optional[MlemModel] = None):
model = model or self.get_model()
if self.state is None:
return
self.state.model_hash = model.meta_hash()

def replace_model(self, model: MlemModel):
self.model = model
self.model_link = self.model.make_link()


def find_object(
path: str, fs: AbstractFileSystem, repo: str = None
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"docker_registry.docker_io = mlem.contrib.docker.base:DockerIORegistry",
"docker_registry.heroku = mlem.contrib.heroku.build:HerokuRemoteRegistry",
"docker_registry.remote = mlem.contrib.docker.base:RemoteRegistry",
"env.heroku = mlem.contrib.heroku.meta:HerokuEnvMeta",
"env.heroku = mlem.contrib.heroku.meta:HerokuEnv",
"import.pandas = mlem.contrib.pandas:PandasImport",
"model_io.catboost_io = mlem.contrib.catboost:CatBoostModelIO",
"model_io.lightgbm_io = mlem.contrib.lightgbm:LightGBMModelIO",
Expand Down
4 changes: 2 additions & 2 deletions tests/cli/test_create.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from mlem.contrib.heroku.meta import HerokuEnvMeta
from mlem.contrib.heroku.meta import HerokuEnv
from mlem.core.metadata import load_meta
from mlem.utils.path import make_posix
from tests.cli.conftest import Runner
Expand All @@ -10,5 +10,5 @@ def test_create(runner: Runner, tmp_path):
)
assert result.exit_code == 0, result.exception
env = load_meta(str(tmp_path))
assert isinstance(env, HerokuEnvMeta)
assert isinstance(env, HerokuEnv)
assert env.api_key == "aaa"
43 changes: 27 additions & 16 deletions tests/contrib/test_heroku.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from mlem.contrib.heroku.meta import (
HerokuAppMeta,
HerokuDeploy,
HerokuEnvMeta,
HerokuEnv,
HerokuState,
)
from mlem.contrib.heroku.utils import (
Expand Down Expand Up @@ -61,9 +61,7 @@ def inner(prefix):

@pytest.fixture()
def heroku_env(tmpdir_factory):
return HerokuEnvMeta().dump(
str(tmpdir_factory.mktemp("heroku_test") / "env")
)
return HerokuEnv().dump(str(tmpdir_factory.mktemp("heroku_test") / "env"))


@pytest.fixture()
Expand Down Expand Up @@ -120,18 +118,7 @@ def test_state_ensured_app():
assert state.ensured_app.name == "name"


@heroku
@long
@heroku_matrix
def test_env_deploy_full(
tmp_path_factory, model, heroku_env, heroku_app_name, uses_docker_build
):
name = heroku_app_name("full-cycle")
meta_path = tmp_path_factory.mktemp("deploy-meta")
meta = deploy(
str(meta_path), model, heroku_env, app_name=name, team=HEROKU_TEAM
)

def _check_heroku_deployment(meta):
assert isinstance(meta, HerokuDeploy)
assert heroku_api_request("GET", f"/apps/{meta.state.ensured_app.name}")
meta.wait_for_status(
Expand Down Expand Up @@ -164,6 +151,30 @@ def test_env_deploy_full(
assert isinstance(res, list)
assert len(res) == 1


@heroku
@long
@heroku_matrix
def test_env_deploy_full(
tmp_path_factory,
model: MlemModel,
heroku_env,
heroku_app_name,
uses_docker_build,
):
name = heroku_app_name("full-cycle")
meta_path = tmp_path_factory.mktemp("deploy-meta")
meta = deploy(
str(meta_path), model, heroku_env, app_name=name, team=HEROKU_TEAM
)

_check_heroku_deployment(meta)

model.description = "New version"
model.update()
redeploy_meta = deploy(meta, model, heroku_env)

_check_heroku_deployment(redeploy_meta)
if CLEAR_APPS:
meta.destroy()

Expand Down