diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 395e186..2ac7d4e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -113,24 +113,6 @@ jobs: run: | pytest vetiver/tests/test_sklearn.py - test-pydantic-old: - name: "Test pydantic v1" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: 3.9 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e .[dev] - python -m pip install 'pydantic<2.0.0' - - - name: Run tests - run: | - make test - typecheck: runs-on: ubuntu-latest steps: diff --git a/pyproject.toml b/pyproject.toml index 52365ba..2d711fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,8 +31,8 @@ requires-python = ">=3.9" dependencies =[ "numpy", "pandas", - "fastapi", - "pydantic", + "fastapi>0.93", + "pydantic>2", "joblib", "uvicorn", "scikit-learn", diff --git a/vetiver/prototype.py b/vetiver/prototype.py index 2b58d93..b3a50b7 100644 --- a/vetiver/prototype.py +++ b/vetiver/prototype.py @@ -170,7 +170,7 @@ def _item(value): # if its a numpy type, we have to take the Python type due to Pydantic dict_data = { - f"{key}": (type(value.item()), Field(..., example=_item(value))) + f"{key}": (type(value.item()), Field(..., examples=[_item(value)])) for key, value in dict_data.items() } prototype = create_prototype(**dict_data) @@ -197,8 +197,8 @@ def _(data: dict): dict_data.update( { key: ( - type(value["example"]), - Field(..., example=value["example"]), + type(value["examples"]), + Field(..., examples=[value["examples"]]), ) } ) @@ -242,5 +242,5 @@ def _(data: NoneType): def _to_field(data): basemodel_input = dict() for key, value in data.items(): - basemodel_input[key] = (type(value), Field(..., example=value)) + basemodel_input[key] = (type(value), Field(..., examples=[value])) return basemodel_input diff --git a/vetiver/server.py b/vetiver/server.py index ea55298..23af56f 100644 --- a/vetiver/server.py +++ b/vetiver/server.py @@ -6,6 +6,7 @@ from typing import Callable, List, Union from urllib.parse import urljoin from warnings import warn +from contextlib import asynccontextmanager import httpx import pandas as pd @@ -77,7 +78,7 @@ def __init__( ) -> None: self.model = model self.app_factory = app_factory - self.app = app_factory() + self.app = app_factory(lifespan=self._lifespan) self.workbench_path = None if "check_ptype" in kwargs: @@ -97,18 +98,19 @@ def __init__( self._init_app() + @asynccontextmanager + async def _lifespan(self, app: FastAPI): + logger = logging.getLogger("uvicorn.error") + if self.workbench_path: + logger.info(f"VetiverAPI starting at {self.workbench_path}") + else: + logger.info("VetiverAPI starting...") + yield + def _init_app(self): app = self.app app.openapi = self._custom_openapi - @app.on_event("startup") - async def startup_event(): - logger = logging.getLogger("uvicorn.error") - if self.workbench_path: - logger.info(f"VetiverAPI starting at {self.workbench_path}") - else: - logger.info("VetiverAPI starting...") - @app.get("/", include_in_schema=False) def docs_redirect(): diff --git a/vetiver/templates/model_card.qmd b/vetiver/templates/model_card.qmd index 40bfc9b..5fbcd47 100644 --- a/vetiver/templates/model_card.qmd +++ b/vetiver/templates/model_card.qmd @@ -45,7 +45,7 @@ A [model card](https://doi.org/10.1145/3287560.3287596) provides brief, transpar ```{python} #| echo: false model_desc = v.description -num_features = len(v.prototype.construct().dict()) +num_features = len(v.prototype.model_construct().model_dump()) display(Markdown(f""" - A {model_desc} using {num_features} feature{'s'[:num_features^1]}. diff --git a/vetiver/tests/test_add_endpoint.py b/vetiver/tests/test_add_endpoint.py index 5a5f1a2..412d62a 100644 --- a/vetiver/tests/test_add_endpoint.py +++ b/vetiver/tests/test_add_endpoint.py @@ -17,7 +17,7 @@ def data() -> pd.DataFrame: def test_endpoint_adds(client, data): - response = client.post("/sum/", data=data.to_json(orient="records")) + response = client.post("/sum/", content=data.to_json(orient="records")) assert response.status_code == 200 assert response.json() == {"sum": [3, 6, 9]} @@ -26,7 +26,7 @@ def test_endpoint_adds(client, data): def test_endpoint_adds_no_prototype(client_no_prototype, data): data = pd.DataFrame({"B": [1, 1, 1], "C": [2, 2, 2], "D": [3, 3, 3]}) - response = client_no_prototype.post("/sum/", data=data.to_json(orient="records")) + response = client_no_prototype.post("/sum/", content=data.to_json(orient="records")) assert response.status_code == 200 assert response.json() == {"sum": [3, 6, 9]} diff --git a/vetiver/tests/test_custom_handler.py b/vetiver/tests/test_custom_handler.py index c984ca6..b6f438e 100644 --- a/vetiver/tests/test_custom_handler.py +++ b/vetiver/tests/test_custom_handler.py @@ -41,7 +41,7 @@ def test_custom_vetiver_model(): assert not v.metadata.required_pkgs assert isinstance(v.model, sklearn.dummy.DummyRegressor) # change to model_construct for pydantic v3 - assert isinstance(v.prototype.construct(), pydantic.BaseModel) + assert isinstance(v.prototype.model_construct(), pydantic.BaseModel) def test_custom_vetiver_model_no_ptype(): @@ -60,4 +60,4 @@ def test_custom_vetiver_model_no_ptype(): assert v.description == "A regression model for testing purposes" assert isinstance(v.model, sklearn.dummy.DummyRegressor) # change to model_construct for pydantic v3 - assert isinstance(v.prototype.construct(), pydantic.BaseModel) + assert isinstance(v.prototype.model_construct(), pydantic.BaseModel) diff --git a/vetiver/tests/test_monitor.py b/vetiver/tests/test_monitor.py index b0f875e..4a9386a 100644 --- a/vetiver/tests/test_monitor.py +++ b/vetiver/tests/test_monitor.py @@ -9,7 +9,7 @@ import pytest -rng = pd.date_range("1/1/2012", periods=10, freq="S") +rng = pd.date_range("1/1/2012", periods=10, freq="s") new = dict(x=range(len(rng)), y=range(len(rng))) df = pd.DataFrame(new, index=rng) td = timedelta(seconds=2) diff --git a/vetiver/tests/test_prepare_docker.py b/vetiver/tests/test_prepare_docker.py index 0b50f35..0a16b7f 100644 --- a/vetiver/tests/test_prepare_docker.py +++ b/vetiver/tests/test_prepare_docker.py @@ -1,29 +1,18 @@ import pytest import vetiver import pins +import requests from pathlib import Path from tempfile import TemporaryDirectory import pandas as pd import numpy as np -DOCKER_URL = "http://0.0.0.0:8080/predict" +DOCKER_URL = "http://0.0.0.0:8080" # uses GitHub Actions to deploy model into Docker # see vetiver-python/script/setup-docker for files -@pytest.mark.docker -def test_deployed_dockerfile(): - np.random.seed(500) - - X, y = vetiver.mock.get_mock_data() - response = vetiver.predict(endpoint=DOCKER_URL, data=X) - - assert isinstance(response, pd.DataFrame), response - assert response.iloc[0, 0] == 44.47 - assert len(response) == 100 - - @pytest.fixture() def create_vetiver_model(): X, y = vetiver.get_mock_data() @@ -41,6 +30,38 @@ def test_warning_if_no_protocol(create_vetiver_model): vetiver.get_board_pkgs(board) +@pytest.mark.docker +def test_prototype(): + np.random.seed(500) + + X, y = vetiver.mock.get_mock_data() + response = requests.get(endpoint=DOCKER_URL + "/prototype") + response = requests.get("/prototype") + assert response.status_code == 200, response.text + assert response.json() == { + "properties": { + "B": {"examples": [55], "type": "integer"}, + "C": {"examples": [65], "type": "integer"}, + "D": {"examples": [17], "type": "integer"}, + }, + "required": ["B", "C", "D"], + "title": "prototype", + "type": "object", + } + + +@pytest.mark.docker +def test_deployed_dockerfile(): + np.random.seed(500) + + X, y = vetiver.mock.get_mock_data() + response = vetiver.predict(endpoint=DOCKER_URL + "/predict", data=X) + + assert isinstance(response, pd.DataFrame), response + assert response.iloc[0, 0] == 44.47 + assert len(response) == 100 + + @pytest.mark.parametrize( "prot,output", [ @@ -49,7 +70,6 @@ def test_warning_if_no_protocol(create_vetiver_model): (("gcs", "gs"), "gcsfs"), ], ) -@pytest.fixture(scope="module") def test_get_board_pkgs(prot, output, create_vetiver_model): board = pins.board_temp(allow_pickle_read=True) board.fs.protocol = prot diff --git a/vetiver/tests/test_rsconnect.py b/vetiver/tests/test_rsconnect.py index adeafae..9895cb3 100644 --- a/vetiver/tests/test_rsconnect.py +++ b/vetiver/tests/test_rsconnect.py @@ -2,6 +2,7 @@ import json import sklearn import pins +import requests import pandas as pd import numpy as np @@ -84,13 +85,36 @@ def test_deploy(rsc_short): extra_files=["requirements.txt"], ) + h = {"Authorization": f'Key {get_key("susan")}'} # get url of where content lives client = RSConnectClient(connect_server) dicts = client.content_search() rsc_api = list(filter(lambda x: x["title"] == "testapi", dicts)) content_url = rsc_api[0].get("content_url") - h = {"Authorization": f'Key {get_key("susan")}'} + # check that endpoint is alive + ping_response = requests.get(content_url + "/ping", headers=h) + assert ping_response.status_code == 200, ping_response.text + + prototype_response = requests.get(content_url + "/prototype", headers=h) + assert prototype_response.status_code == 200, prototype_response.text + assert prototype_response.json() == { + "properties": { + "B": {"examples": [55], "type": "integer"}, + "C": {"examples": [65], "type": "integer"}, + "D": {"examples": [17], "type": "integer"}, + }, + "required": ["B", "C", "D"], + "title": "prototype", + "type": "object", + } + + assert ( + model.prototype.model_construct().model_dump() + == vetiver.vetiver_create_prototype(prototype_response.json()) + .model_construct() + .model_dump() + ) endpoint = vetiver.vetiver_endpoint(content_url + "/predict") response = vetiver.predict(endpoint, X_df, headers=h) diff --git a/vetiver/tests/test_server.py b/vetiver/tests/test_server.py index 97150c0..07bba3c 100644 --- a/vetiver/tests/test_server.py +++ b/vetiver/tests/test_server.py @@ -43,7 +43,7 @@ class CustomPrototype(BaseModel): v = VetiverModel( model=model, # move to model_construct for pydantic 3 - prototype_data=CustomPrototype.construct(), + prototype_data=CustomPrototype.model_construct(), model_name="my_model", versioned=None, description="A regression model for testing purposes", @@ -82,9 +82,9 @@ def test_get_prototype(client, model): assert response.status_code == 200, response.text assert response.json() == { "properties": { - "B": {"example": 55, "type": "integer"}, - "C": {"example": 65, "type": "integer"}, - "D": {"example": 17, "type": "integer"}, + "B": {"examples": [55], "type": "integer"}, + "C": {"examples": [65], "type": "integer"}, + "D": {"examples": [17], "type": "integer"}, }, "required": ["B", "C", "D"], "title": "prototype", @@ -92,8 +92,8 @@ def test_get_prototype(client, model): } assert ( - model.prototype.construct().dict() - == vetiver_create_prototype(response.json()).construct().dict() + model.prototype.model_construct().model_dump() + == vetiver_create_prototype(response.json()).model_construct().model_dump() ) diff --git a/vetiver/tests/test_spacy.py b/vetiver/tests/test_spacy.py index 06cf49e..f074276 100644 --- a/vetiver/tests/test_spacy.py +++ b/vetiver/tests/test_spacy.py @@ -68,7 +68,7 @@ def test_good_prototype_shape(data, spacy_model): model_schema = v.prototype.model_json_schema() expected = { "properties": { - "col": {"example": "1", "title": "Col", "type": "string"}, + "col": {"examples": ["1"], "title": "Col", "type": "string"}, }, "required": ["col"], "title": "prototype", @@ -77,7 +77,7 @@ def test_good_prototype_shape(data, spacy_model): except AttributeError: # pydantic v1 model_schema = v.prototype.schema_json() expected = '{"title": "prototype", "type": "object", "properties": \ -{"col": {"title": "Col", "example": "1", "type": "string"}}, "required": ["col"]}' +{"col": {"title": "Col", "examples": ["1"], "type": "string"}}, "required": ["col"]}' assert model_schema == expected diff --git a/vetiver/tests/test_vetiver_model.py b/vetiver/tests/test_vetiver_model.py index ba84541..63fcb89 100644 --- a/vetiver/tests/test_vetiver_model.py +++ b/vetiver/tests/test_vetiver_model.py @@ -45,9 +45,9 @@ def test_vetiver_model_array_prototype(): json_schema = v.prototype.model_json_schema() expected = { "properties": { - "0": {"example": 96, "title": "0", "type": "integer"}, - "1": {"example": 11, "title": "1", "type": "integer"}, - "2": {"example": 33, "title": "2", "type": "integer"}, + "0": {"examples": [96], "title": "0", "type": "integer"}, + "1": {"examples": [11], "title": "1", "type": "integer"}, + "2": {"examples": [33], "title": "2", "type": "integer"}, }, "required": ["0", "1", "2"], "title": "prototype", @@ -58,15 +58,15 @@ def test_vetiver_model_array_prototype(): expected = '{\ "title": "prototype", \ "type": "object", \ -"properties": {"0": {"title": "0", "example": 96, "type": "integer"}, \ -"1": {"title": "1", "example": 11, "type": "integer"}, \ -"2": {"title": "2", "example": 33, "type": "integer"}}, \ +"properties": {"0": {"title": "0", "examples": [96], "type": "integer"}, \ +"1": {"title": "1", "examples": [11], "type": "integer"}, \ +"2": {"title": "2", "examples": [33], "type": "integer"}}, \ "required": ["0", "1", "2"]}' assert v.model == model assert issubclass(v.prototype, vetiver.Prototype) # change to model_construct for pydantic v3 - assert isinstance(v.prototype.construct(), pydantic.BaseModel) + assert isinstance(v.prototype.model_construct(), pydantic.BaseModel) assert json_schema == expected @@ -83,15 +83,15 @@ def test_vetiver_model_dict_like_prototype(prototype_data): assert v.model == model # change to model_construct for pydantic v3 - assert isinstance(v.prototype.construct(), pydantic.BaseModel) + assert isinstance(v.prototype.model_construct(), pydantic.BaseModel) try: json_schema = v.prototype.model_json_schema() expected = { "properties": { - "B": {"example": 96, "title": "B", "type": "integer"}, - "C": {"example": 11, "title": "C", "type": "integer"}, - "D": {"example": 33, "title": "D", "type": "integer"}, + "B": {"examples": [96], "title": "B", "type": "integer"}, + "C": {"examples": [11], "title": "C", "type": "integer"}, + "D": {"examples": [33], "title": "D", "type": "integer"}, }, "required": ["B", "C", "D"], "title": "prototype", @@ -102,9 +102,9 @@ def test_vetiver_model_dict_like_prototype(prototype_data): expected = '{\ "title": "prototype", \ "type": "object", \ -"properties": {"B": {"title": "B", "example": 96, "type": "integer"}, \ -"C": {"title": "C", "example": 11, "type": "integer"}, \ -"D": {"title": "D", "example": 33, "type": "integer"}}, \ +"properties": {"B": {"title": "B", "examples": [96], "type": "integer"}, \ +"C": {"title": "C", "examples": [11], "type": "integer"}, \ +"D": {"title": "D", "examples": [33], "type": "integer"}}, \ "required": ["B", "C", "D"]}' assert json_schema == expected @@ -120,9 +120,9 @@ def test_vetiver_model_dict_like_prototype(prototype_data): {"B": 0, "C": False, "D": None}, { "properties": { - "B": {"example": 0, "title": "B", "type": "integer"}, - "C": {"example": False, "title": "C", "type": "boolean"}, - "D": {"example": None, "title": "D", "type": "null"}, + "B": {"examples": [0], "title": "B", "type": "integer"}, + "C": {"examples": [False], "title": "C", "type": "boolean"}, + "D": {"examples": [None], "title": "D", "type": "null"}, }, "required": ["B", "C", "D"], "title": "prototype", @@ -141,7 +141,7 @@ def test_falsy_prototypes(prototype_data, expected): metadata=None, ) - assert isinstance(v.prototype.construct(), pydantic.BaseModel) + assert isinstance(v.prototype.model_construct(), pydantic.BaseModel) assert v.prototype.model_json_schema() == expected @@ -179,7 +179,7 @@ def test_vetiver_model_from_pin(): assert isinstance(v2, VetiverModel) assert isinstance(v2.model, sklearn.base.BaseEstimator) # change to model_construct for pydantic v3 - assert isinstance(v2.prototype.construct(), pydantic.BaseModel) + assert isinstance(v2.prototype.model_construct(), pydantic.BaseModel) assert v2.metadata.user == {"test": 123} assert v2.metadata.version is not None assert v2.metadata.required_pkgs == ["scikit-learn"] @@ -215,7 +215,7 @@ def test_vetiver_model_from_pin_user_metadata(): assert isinstance(v2, VetiverModel) assert isinstance(v2.model, sklearn.base.BaseEstimator) # change to model_construct for pydantic v3 - assert isinstance(v2.prototype.construct(), pydantic.BaseModel) + assert isinstance(v2.prototype.model_construct(), pydantic.BaseModel) assert v2.metadata.user == custom_meta assert v2.metadata.version is not None assert v2.metadata.required_pkgs == loaded_pkgs diff --git a/vetiver/tests/test_write_app.py b/vetiver/tests/test_write_app.py index cb07a17..95c3dc9 100644 --- a/vetiver/tests/test_write_app.py +++ b/vetiver/tests/test_write_app.py @@ -12,6 +12,7 @@ def vetiver_model_creation(): return vetiver.VetiverModel(model, "model") +@pytest.fixture(scope="function") def test_write_app(vetiver_model_creation): with TemporaryDirectory() as tempdir: file = Path(tempdir, "app.py") diff --git a/vetiver/utils.py b/vetiver/utils.py index c727cd5..8cbef6f 100644 --- a/vetiver/utils.py +++ b/vetiver/utils.py @@ -64,7 +64,10 @@ def serialize_prototype(prototype): serialized_schema = dict() for key, value in schema.items(): - example = value.get("example", None) + example = value.get("example", None) or value.get("examples", None) + # with pydantic's move from example to [examples], now we have to + # make sure we're extracting the right value + example = example[0] if isinstance(value, list) else example default = value.get("default", None) serialized_schema[key] = example if example is not None else default