From cd43ee135aeb580738d5e6394f60f8dbe5185286 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 13:17:02 +0700 Subject: [PATCH 01/39] fix: remove deprecated package from setup.py, run pip install in Dockerfile --- python/sdk/Dockerfile | 2 ++ python/sdk/setup.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/python/sdk/Dockerfile b/python/sdk/Dockerfile index b2968dc22..8c1844458 100644 --- a/python/sdk/Dockerfile +++ b/python/sdk/Dockerfile @@ -10,5 +10,7 @@ RUN apt-get update && apt-get install build-essential curl vim wget -y COPY . .${WORKDIR} +RUN pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" + RUN pip install . RUN pip install ".[test]" diff --git a/python/sdk/setup.py b/python/sdk/setup.py index 7431c447f..9b5c953ca 100644 --- a/python/sdk/setup.py +++ b/python/sdk/setup.py @@ -13,14 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import imp +import importlib.util import os from setuptools import find_packages, setup -version = imp.load_source( - "merlin.version", os.path.join("merlin", "version.py") -).VERSION +# get version from version.py +spec = importlib.util.spec_from_file_location( + "sdk.version", os.path.join("merlin/version.py") +) + +v_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(v_module) + +version = v_module.VERSION with open("requirements.txt") as f: REQUIRE = f.read().splitlines() From 0dc1e7cdad2a5d0e1141f629e3e0f90474c7d9ed Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 15:21:18 +0700 Subject: [PATCH 02/39] feat: support python 3.9 - 3.13 in setup.py --- python/sdk/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdk/setup.py b/python/sdk/setup.py index 9b5c953ca..e1b836750 100644 --- a/python/sdk/setup.py +++ b/python/sdk/setup.py @@ -47,7 +47,7 @@ setup_requires=["setuptools_scm"], tests_require=TESTS_REQUIRE, extras_require={"test": TESTS_REQUIRE}, - python_requires=">=3.8,<3.11", + python_requires=">=3.9,<=3.13", long_description=open("README.md").read(), long_description_content_type="text/markdown", entry_points=""" From 494338ef1edde742c0a13a4f4ade8865368d1509 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 15:22:13 +0700 Subject: [PATCH 03/39] chore: adjust workflow for testing --- .github/workflows/release.yml | 8 +++++--- python/sdk/Dockerfile | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 189d77208..2cff68e13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ on: required: true pypi_password: required: true + workflow_dispatch: env: DOCKER_REGISTRY: ghcr.io @@ -101,9 +102,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] - needs: - - publish-python-sdk + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Log in to the Container registry @@ -112,6 +111,9 @@ jobs: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Install dependencies + working-directory: ./python/sdk + run: pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" - name: Build Merlin SDK Docker py${{ matrix.python-version }} env: CONTAINER_REGISTRY: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }} diff --git a/python/sdk/Dockerfile b/python/sdk/Dockerfile index 8c1844458..b2968dc22 100644 --- a/python/sdk/Dockerfile +++ b/python/sdk/Dockerfile @@ -10,7 +10,5 @@ RUN apt-get update && apt-get install build-essential curl vim wget -y COPY . .${WORKDIR} -RUN pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" - RUN pip install . RUN pip install ".[test]" From 108c01cd8f58482b7c190d97fa71dcededfe4739 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 15:31:08 +0700 Subject: [PATCH 04/39] feat: adjust python-version used in all workflows --- .github/workflows/merlin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index d45f21378..9547cba3b 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: @@ -77,7 +77,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: @@ -109,7 +109,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: From a6a20088b5af63a81cc3576910e6097fbf420c59 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 17:31:32 +0700 Subject: [PATCH 05/39] chore: bump caraml-auth-google to pre release version --- python/observation-publisher/requirements.txt | 2 +- python/sdk/requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/observation-publisher/requirements.txt b/python/observation-publisher/requirements.txt index 3722ef866..4a382771f 100644 --- a/python/observation-publisher/requirements.txt +++ b/python/observation-publisher/requirements.txt @@ -30,7 +30,7 @@ botocore==1.35.39 # s3transfer cachetools==5.3.2 # via google-auth -caraml-auth-google==0.0.0.post7 +caraml-auth-google==0.0.0.post14.dev0 # via merlin-sdk caraml-upi-protos==1.0.0 # via diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index 3542b7e6f..9d7efb11b 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -16,5 +16,5 @@ PyYAML>=5.4 six>=1.10 urllib3>=1.26 numpy<=1.23.5 # Temporary pin numpy due to https://numpy.org/doc/stable/release/1.20.0-notes.html#numpy-1-20-0-release-notes -caraml-auth-google==0.0.0.post7 +caraml-auth-google==0.0.0.post14.dev0 pydantic==2.5.3 \ No newline at end of file From 938e7e1adc1d3a4f6047677afb6de4f8d49f0142 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Mon, 14 Apr 2025 17:53:18 +0700 Subject: [PATCH 06/39] feat: install setuptools and stuff from CI --- .github/workflows/merlin.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 9547cba3b..137013d00 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -67,6 +67,7 @@ jobs: working-directory: ./python/batch-predictor run: | pip install pipenv==2023.7.23 + pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" make setup - name: Run batch-predictor test working-directory: ./python/batch-predictor @@ -98,7 +99,8 @@ jobs: - name: Install dependencies working-directory: ./python/pyfunc-server run: | - pip install pipenv==2023.7.23 + pip install + pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" make setup - name: Run pyfunc-server test working-directory: ./python/pyfunc-server From 54b115b2cfcdbfe4d081f236fb0841c275344bf0 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 10:33:48 +0700 Subject: [PATCH 07/39] chore: add Cython to requirements --- .github/workflows/merlin.yml | 2 +- python/batch-predictor/requirements_test.txt | 3 ++- python/pyfunc-server/setup.py | 1 + python/sdk/requirements_test.txt | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 137013d00..e23a13b20 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -99,7 +99,7 @@ jobs: - name: Install dependencies working-directory: ./python/pyfunc-server run: | - pip install + pip install pipenv==2023.7.23 pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" make setup - name: Run pyfunc-server test diff --git a/python/batch-predictor/requirements_test.txt b/python/batch-predictor/requirements_test.txt index 67b37771f..4bd18d836 100644 --- a/python/batch-predictor/requirements_test.txt +++ b/python/batch-predictor/requirements_test.txt @@ -5,4 +5,5 @@ google-cloud-bigquery scikit-learn>=1.1.2 joblib>=0.13.0,<1.2.0 # >=1.2.0 upon upgrade of kserve's version mypy-protobuf>=1.19 -types-PyYAML \ No newline at end of file +types-PyYAML +Cython \ No newline at end of file diff --git a/python/pyfunc-server/setup.py b/python/pyfunc-server/setup.py index 732c9942a..7b86e5b8b 100644 --- a/python/pyfunc-server/setup.py +++ b/python/pyfunc-server/setup.py @@ -35,6 +35,7 @@ "types-protobuf", "types-requests", "xgboost==1.6.2", + "Cython" ] setup( diff --git a/python/sdk/requirements_test.txt b/python/sdk/requirements_test.txt index 2eface6ea..9cd1d3b1b 100644 --- a/python/sdk/requirements_test.txt +++ b/python/sdk/requirements_test.txt @@ -15,4 +15,5 @@ types-six types-protobuf urllib3-mock>=0.3.3 xarray -xgboost==1.6.2 \ No newline at end of file +xgboost==1.6.2 +Cython \ No newline at end of file From 9789182d2cff9807431e5e89d3451503a8d48aea Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 10:56:46 +0700 Subject: [PATCH 08/39] chore: move Cython installation before building setup.py --- .github/workflows/merlin.yml | 2 -- python/batch-predictor/Makefile | 2 +- python/batch-predictor/requirements_test.txt | 3 +-- python/pyfunc-server/Makefile | 2 +- python/pyfunc-server/setup.py | 1 - python/sdk/requirements_test.txt | 3 +-- 6 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index e23a13b20..9547cba3b 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -67,7 +67,6 @@ jobs: working-directory: ./python/batch-predictor run: | pip install pipenv==2023.7.23 - pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" make setup - name: Run batch-predictor test working-directory: ./python/batch-predictor @@ -100,7 +99,6 @@ jobs: working-directory: ./python/pyfunc-server run: | pip install pipenv==2023.7.23 - pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" make setup - name: Run pyfunc-server test working-directory: ./python/pyfunc-server diff --git a/python/batch-predictor/Makefile b/python/batch-predictor/Makefile index 207b1b422..db3374177 100644 --- a/python/batch-predictor/Makefile +++ b/python/batch-predictor/Makefile @@ -40,6 +40,6 @@ proto: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && pip install setuptools setuptools_scm twine wheel + cd ../sdk && pip install setuptools setuptools_scm twine wheel Cython cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file diff --git a/python/batch-predictor/requirements_test.txt b/python/batch-predictor/requirements_test.txt index 4bd18d836..67b37771f 100644 --- a/python/batch-predictor/requirements_test.txt +++ b/python/batch-predictor/requirements_test.txt @@ -5,5 +5,4 @@ google-cloud-bigquery scikit-learn>=1.1.2 joblib>=0.13.0,<1.2.0 # >=1.2.0 upon upgrade of kserve's version mypy-protobuf>=1.19 -types-PyYAML -Cython \ No newline at end of file +types-PyYAML \ No newline at end of file diff --git a/python/pyfunc-server/Makefile b/python/pyfunc-server/Makefile index 388075983..dafdf4bd4 100644 --- a/python/pyfunc-server/Makefile +++ b/python/pyfunc-server/Makefile @@ -18,6 +18,6 @@ benchmark: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && pip install setuptools setuptools_scm twine wheel + cd ../sdk && pip install setuptools setuptools_scm twine wheel Cython cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file diff --git a/python/pyfunc-server/setup.py b/python/pyfunc-server/setup.py index 7b86e5b8b..732c9942a 100644 --- a/python/pyfunc-server/setup.py +++ b/python/pyfunc-server/setup.py @@ -35,7 +35,6 @@ "types-protobuf", "types-requests", "xgboost==1.6.2", - "Cython" ] setup( diff --git a/python/sdk/requirements_test.txt b/python/sdk/requirements_test.txt index 9cd1d3b1b..2eface6ea 100644 --- a/python/sdk/requirements_test.txt +++ b/python/sdk/requirements_test.txt @@ -15,5 +15,4 @@ types-six types-protobuf urllib3-mock>=0.3.3 xarray -xgboost==1.6.2 -Cython \ No newline at end of file +xgboost==1.6.2 \ No newline at end of file From d657a20ac67829c916b31404fa6ce55af25c1b44 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 11:10:20 +0700 Subject: [PATCH 09/39] chore: try to use python -m pip instead of pip directly --- python/batch-predictor/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/batch-predictor/Makefile b/python/batch-predictor/Makefile index db3374177..f24772b71 100644 --- a/python/batch-predictor/Makefile +++ b/python/batch-predictor/Makefile @@ -40,6 +40,6 @@ proto: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && pip install setuptools setuptools_scm twine wheel Cython + cd ../sdk && python -m pip install setuptools setuptools_scm twine wheel Cython cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file From d5be94cbd230e3653325fa03e26d3267a42de29d Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 11:16:19 +0700 Subject: [PATCH 10/39] feat: add cython to Pipfile --- python/batch-predictor/Makefile | 2 +- python/batch-predictor/Pipfile | 1 + python/pyfunc-server/Makefile | 2 +- python/pyfunc-server/Pipfile | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/python/batch-predictor/Makefile b/python/batch-predictor/Makefile index f24772b71..98d3d1429 100644 --- a/python/batch-predictor/Makefile +++ b/python/batch-predictor/Makefile @@ -40,6 +40,6 @@ proto: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && python -m pip install setuptools setuptools_scm twine wheel Cython + cd ../sdk && python -m pip install setuptools setuptools_scm twine wheel cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file diff --git a/python/batch-predictor/Pipfile b/python/batch-predictor/Pipfile index 716141c98..6f529bc47 100644 --- a/python/batch-predictor/Pipfile +++ b/python/batch-predictor/Pipfile @@ -8,3 +8,4 @@ merlin-batch-predictor = {editable = true,extras = ["test"],path = "."} [packages] merlin-batch-predictor = {extras = ["test"], file = ".", editable = true} +cython = "*" diff --git a/python/pyfunc-server/Makefile b/python/pyfunc-server/Makefile index dafdf4bd4..388075983 100644 --- a/python/pyfunc-server/Makefile +++ b/python/pyfunc-server/Makefile @@ -18,6 +18,6 @@ benchmark: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && pip install setuptools setuptools_scm twine wheel Cython + cd ../sdk && pip install setuptools setuptools_scm twine wheel cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file diff --git a/python/pyfunc-server/Pipfile b/python/pyfunc-server/Pipfile index a3074b4ae..55bf72754 100644 --- a/python/pyfunc-server/Pipfile +++ b/python/pyfunc-server/Pipfile @@ -6,5 +6,6 @@ verify_ssl = true [packages] pyfuncserver = {editable = true, extras = ["test"], path = "."} merlin-pyfunc-server = {extras = ["test"], file = ".", editable = true} +cython = "*" [dev-packages] From 359109734be021a379a41b1baac67cf33085429e Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 13:41:18 +0700 Subject: [PATCH 11/39] feat: adjust TESTS_REQUIRE version --- python/pyfunc-server/setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/pyfunc-server/setup.py b/python/pyfunc-server/setup.py index 732c9942a..e586a0fc4 100644 --- a/python/pyfunc-server/setup.py +++ b/python/pyfunc-server/setup.py @@ -25,16 +25,16 @@ REQUIRE = f.read().splitlines() TESTS_REQUIRE = [ - "joblib>=0.13.0,<1.2.0", # >=1.2.0 upon upgrade of kserve's version + "joblib>=1.2.0", "mypy", + "pytest>=7.0", "pytest-benchmark", "pytest-tornasync", - "pytest", "requests", - "scikit-learn>=1.1.2", + "scikit-learn>=1.3.1", "types-protobuf", "types-requests", - "xgboost==1.6.2", + "xgboost>=1.7.6", ] setup( From 43293e1b2dc2d233391dfa33536d0e3bf093ad1f Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 14:18:55 +0700 Subject: [PATCH 12/39] chore: try other versions of the dependencies --- python/pyfunc-server/Pipfile | 2 +- python/pyfunc-server/setup.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/pyfunc-server/Pipfile b/python/pyfunc-server/Pipfile index 55bf72754..dc6dc63f7 100644 --- a/python/pyfunc-server/Pipfile +++ b/python/pyfunc-server/Pipfile @@ -5,7 +5,7 @@ verify_ssl = true [packages] pyfuncserver = {editable = true, extras = ["test"], path = "."} +cython = ">=3.0" merlin-pyfunc-server = {extras = ["test"], file = ".", editable = true} -cython = "*" [dev-packages] diff --git a/python/pyfunc-server/setup.py b/python/pyfunc-server/setup.py index e586a0fc4..a97ddad1a 100644 --- a/python/pyfunc-server/setup.py +++ b/python/pyfunc-server/setup.py @@ -26,11 +26,11 @@ TESTS_REQUIRE = [ "joblib>=1.2.0", - "mypy", - "pytest>=7.0", - "pytest-benchmark", + "mypy>=1.5.4", + "pytest>=8.1", + "pytest-benchmark>=5.1.0", "pytest-tornasync", - "requests", + "requests>=2.31.0", "scikit-learn>=1.3.1", "types-protobuf", "types-requests", @@ -44,7 +44,7 @@ description="Model Server implementation for Merlin PyFunc model", long_description=open("README.md").read(), long_description_content_type="text/markdown", - python_requires=">=3.8,<3.11", + python_requires=">=3.8,<=3.13", packages=find_packages(exclude=["test"]), install_requires=REQUIRE, tests_require=TESTS_REQUIRE, From 040ba721f5073639461878dfd6cf199d5a0ac2ce Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 14:41:02 +0700 Subject: [PATCH 13/39] feat: adjust batch-predictor dependency to support python 3.11 --- python/batch-predictor/Makefile | 2 +- python/batch-predictor/Pipfile | 2 +- python/batch-predictor/requirements_test.txt | 6 +++--- python/batch-predictor/setup.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/python/batch-predictor/Makefile b/python/batch-predictor/Makefile index 98d3d1429..207b1b422 100644 --- a/python/batch-predictor/Makefile +++ b/python/batch-predictor/Makefile @@ -40,6 +40,6 @@ proto: .PHONY: build_install_sdk build_install_sdk: - cd ../sdk && python -m pip install setuptools setuptools_scm twine wheel + cd ../sdk && pip install setuptools setuptools_scm twine wheel cd ../sdk && python setup.py sdist bdist_wheel pipenv run pip install ../sdk/dist/merlin_sdk-0.0.0-py3-none-any.whl \ No newline at end of file diff --git a/python/batch-predictor/Pipfile b/python/batch-predictor/Pipfile index 6f529bc47..0c5aecbcc 100644 --- a/python/batch-predictor/Pipfile +++ b/python/batch-predictor/Pipfile @@ -7,5 +7,5 @@ verify_ssl = true merlin-batch-predictor = {editable = true,extras = ["test"],path = "."} [packages] +cython = ">=3.0" merlin-batch-predictor = {extras = ["test"], file = ".", editable = true} -cython = "*" diff --git a/python/batch-predictor/requirements_test.txt b/python/batch-predictor/requirements_test.txt index 67b37771f..033f66cb7 100644 --- a/python/batch-predictor/requirements_test.txt +++ b/python/batch-predictor/requirements_test.txt @@ -1,8 +1,8 @@ -pytest +pytest>=8.1 pytest-cov mypy google-cloud-bigquery -scikit-learn>=1.1.2 -joblib>=0.13.0,<1.2.0 # >=1.2.0 upon upgrade of kserve's version +scikit-learn>=1.3.1 +joblib>=1.2.0 mypy-protobuf>=1.19 types-PyYAML \ No newline at end of file diff --git a/python/batch-predictor/setup.py b/python/batch-predictor/setup.py index 8b93a76af..9d06b8707 100644 --- a/python/batch-predictor/setup.py +++ b/python/batch-predictor/setup.py @@ -34,7 +34,7 @@ description="Base PySpark application for running Merlin prediction batch job", long_description=open("README.md").read(), long_description_content_type="text/markdown", - python_requires=">=3.8,<3.11", + python_requires=">=3.9,<=3.13", packages=find_packages(exclude=["test"]), install_requires=REQUIRE, tests_require=TESTS_REQUIRE, From df556783dff216790feed564e27070a47c903fc1 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 16:37:22 +0700 Subject: [PATCH 14/39] chore: bump dependency version to support python 3.13 --- python/sdk/requirements.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index 9d7efb11b..f5545984b 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -15,6 +15,8 @@ python_dateutil>=2.5.3 PyYAML>=5.4 six>=1.10 urllib3>=1.26 -numpy<=1.23.5 # Temporary pin numpy due to https://numpy.org/doc/stable/release/1.20.0-notes.html#numpy-1-20-0-release-notes +numpy>=1.26.4 caraml-auth-google==0.0.0.post14.dev0 -pydantic==2.5.3 \ No newline at end of file +pydantic<2.0 +grpcio-tools==1.60.0 +grpcio==1.60.0 \ No newline at end of file From bcfad36e6eae2fbc8ac5156b018a2de8cb162627 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 16:58:30 +0700 Subject: [PATCH 15/39] feat: pindown sdk dependency version to support python 3.13.* --- python/sdk/requirements.txt | 8 ++++---- python/sdk/setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index f5545984b..e28dc86bb 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -8,7 +8,7 @@ dataclasses-json>=0.5.2 # allow Flyte version 1.2.0 or above to import Merlin S docker<=6.1.3 GitPython>=3.1.40 google-cloud-storage>=1.19.0 -protobuf>=3.12.0,<5.0.0 # Determined by the mlflow dependency +protobuf>=5.26.1,<6.0dev mlflow==1.26.1 PyPrind>=2.11.2 python_dateutil>=2.5.3 @@ -16,7 +16,7 @@ PyYAML>=5.4 six>=1.10 urllib3>=1.26 numpy>=1.26.4 -caraml-auth-google==0.0.0.post14.dev0 +caraml-auth-google==0.0.0.post16.dev0 pydantic<2.0 -grpcio-tools==1.60.0 -grpcio==1.60.0 \ No newline at end of file +grpcio-tools==1.71.0 +grpcio==1.71.0 \ No newline at end of file diff --git a/python/sdk/setup.py b/python/sdk/setup.py index e1b836750..fbc090088 100644 --- a/python/sdk/setup.py +++ b/python/sdk/setup.py @@ -47,7 +47,7 @@ setup_requires=["setuptools_scm"], tests_require=TESTS_REQUIRE, extras_require={"test": TESTS_REQUIRE}, - python_requires=">=3.9,<=3.13", + python_requires=">=3.9,<3.14", long_description=open("README.md").read(), long_description_content_type="text/markdown", entry_points=""" From 05b7312a95a91aafcfc59f2c15e5c34ccf98a97b Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:03:53 +0700 Subject: [PATCH 16/39] feat: use importlib instead of imp --- python/batch-predictor/setup.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/python/batch-predictor/setup.py b/python/batch-predictor/setup.py index 9d06b8707..843c8c399 100644 --- a/python/batch-predictor/setup.py +++ b/python/batch-predictor/setup.py @@ -12,14 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import imp +import importlib.util import os from setuptools import find_packages, setup -version = imp.load_source( - "merlinpyspark.version", os.path.join("merlinpyspark", "version.py") -).VERSION +# get version from version.py +spec = importlib.util.spec_from_file_location( + "merlinpyspark.version", os.path.join("merlinpyspark","version.py") +) + +v_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(v_module) + +version = v_module.VERSION with open("requirements.txt") as f: REQUIRE = f.read().splitlines() @@ -34,7 +40,7 @@ description="Base PySpark application for running Merlin prediction batch job", long_description=open("README.md").read(), long_description_content_type="text/markdown", - python_requires=">=3.9,<=3.13", + python_requires=">=3.9,<3.14", packages=find_packages(exclude=["test"]), install_requires=REQUIRE, tests_require=TESTS_REQUIRE, From 23eb6613e478bf3dbbbab8c3752f9592d269145c Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:04:19 +0700 Subject: [PATCH 17/39] feat: bump pyarrow version to version which work with python 3.9 - 3.13 --- python/batch-predictor/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/batch-predictor/requirements.txt b/python/batch-predictor/requirements.txt index ab3b888b8..ce9546a1b 100644 --- a/python/batch-predictor/requirements.txt +++ b/python/batch-predictor/requirements.txt @@ -2,6 +2,6 @@ cloudpickle==2.0.0 findspark==2.0.1 merlin-sdk==0.0.0 mlflow==1.26.1 -pyarrow>=0.14.1,<=17.0.0 +pyarrow==19.0.0 pyspark==3.1.3 setuptools<75 \ No newline at end of file From 2b70ae3aa1fbfa8aac46d745425cb1dd759b99b3 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:10:12 +0700 Subject: [PATCH 18/39] chore: revert unnecessary changes --- python/observation-publisher/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/observation-publisher/requirements.txt b/python/observation-publisher/requirements.txt index 4a382771f..3722ef866 100644 --- a/python/observation-publisher/requirements.txt +++ b/python/observation-publisher/requirements.txt @@ -30,7 +30,7 @@ botocore==1.35.39 # s3transfer cachetools==5.3.2 # via google-auth -caraml-auth-google==0.0.0.post14.dev0 +caraml-auth-google==0.0.0.post7 # via merlin-sdk caraml-upi-protos==1.0.0 # via From 6449bb06ad7ab7d7d6580d6c2b0ec01d68ae2401 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:17:09 +0700 Subject: [PATCH 19/39] feat: use importlib instead of imp --- python/pyfunc-server/setup.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/python/pyfunc-server/setup.py b/python/pyfunc-server/setup.py index a97ddad1a..f9360e5fe 100644 --- a/python/pyfunc-server/setup.py +++ b/python/pyfunc-server/setup.py @@ -12,14 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -import imp +import importlib.util import os from setuptools import find_packages, setup -version = imp.load_source( - "pyfuncserver.version", os.path.join("pyfuncserver", "version.py") -).VERSION +# get version from version.py +spec = importlib.util.spec_from_file_location( + "pyfuncserver.version", os.path.join("pyfuncserver","version.py") +) + +v_module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(v_module) + +version = v_module.VERSION with open("requirements.txt") as f: REQUIRE = f.read().splitlines() @@ -44,7 +50,7 @@ description="Model Server implementation for Merlin PyFunc model", long_description=open("README.md").read(), long_description_content_type="text/markdown", - python_requires=">=3.8,<=3.13", + python_requires=">=3.8,<3.14", packages=find_packages(exclude=["test"]), install_requires=REQUIRE, tests_require=TESTS_REQUIRE, From 8850153bf38e60cee7fea1e7768f0515411f86d8 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:17:34 +0700 Subject: [PATCH 20/39] feat: bump confluent-kafka to version which work with python 3.9 - 3.13 --- python/pyfunc-server/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/pyfunc-server/requirements.txt b/python/pyfunc-server/requirements.txt index 562406840..4601b937b 100644 --- a/python/pyfunc-server/requirements.txt +++ b/python/pyfunc-server/requirements.txt @@ -1,7 +1,7 @@ argparse>=1.4.0 caraml-upi-protos>=0.3.4 cloudpickle==2.0.0 -confluent-kafka==2.3.0 +confluent-kafka>=2.6.0 grpcio-health-checking grpcio-reflection merlin-sdk==0.0.0 From ddb237188e61ff5ece34a04d97c3aad957228319 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:38:46 +0700 Subject: [PATCH 21/39] chore: revert pydantic version --- python/sdk/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index e28dc86bb..9eea81468 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -17,6 +17,6 @@ six>=1.10 urllib3>=1.26 numpy>=1.26.4 caraml-auth-google==0.0.0.post16.dev0 -pydantic<2.0 +pydantic==2.5.3 grpcio-tools==1.71.0 grpcio==1.71.0 \ No newline at end of file From 6a0b6cc6afcae0ef57379f48d227b15ad0cea4c6 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Tue, 15 Apr 2025 17:39:12 +0700 Subject: [PATCH 22/39] feat: only runs workflow for python 3.9 - 3.12 --- .github/workflows/merlin.yml | 6 +++--- .github/workflows/release.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 9547cba3b..54a678f3d 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: @@ -77,7 +77,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: @@ -109,7 +109,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2cff68e13..173d12eb8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,7 +102,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Log in to the Container registry From 72439f83b5bcedc3e0fc662d383c368da611d4f4 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:09:31 +0700 Subject: [PATCH 23/39] chore: downgrade protobuf version to work with arize --- python/sdk/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index 9eea81468..49fd05335 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -8,7 +8,7 @@ dataclasses-json>=0.5.2 # allow Flyte version 1.2.0 or above to import Merlin S docker<=6.1.3 GitPython>=3.1.40 google-cloud-storage>=1.19.0 -protobuf>=5.26.1,<6.0dev +protobuf>=4.21.6,<5 mlflow==1.26.1 PyPrind>=2.11.2 python_dateutil>=2.5.3 From 41cc7864b563271e29785988beeccfd72d02ee61 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:12:25 +0700 Subject: [PATCH 24/39] chore: downgrade grpcio-tools --- python/sdk/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index 49fd05335..c4c22fdba 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -18,5 +18,5 @@ urllib3>=1.26 numpy>=1.26.4 caraml-auth-google==0.0.0.post16.dev0 pydantic==2.5.3 -grpcio-tools==1.71.0 +grpcio-tools>=1.50.0,<1.63 grpcio==1.71.0 \ No newline at end of file From 9890e7cea9c5b335d26a0842ad19a8c7fa7c624f Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:16:03 +0700 Subject: [PATCH 25/39] chore: adjust numpy version --- python/observation-publisher/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/observation-publisher/requirements.txt b/python/observation-publisher/requirements.txt index 3722ef866..563ee6aa7 100644 --- a/python/observation-publisher/requirements.txt +++ b/python/observation-publisher/requirements.txt @@ -150,7 +150,7 @@ mlflow==1.26.1 # via merlin-sdk mypy-extensions==1.0.0 # via typing-inspect -numpy==1.23.5 +numpy>=1.26.4,<2.0 # via # merlin-sdk # mlflow From 2c590f5142c254f82fec0c92677fc4957a010542 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:18:00 +0700 Subject: [PATCH 26/39] chore: adjust caraml-auth-google version for observation-publisher --- python/observation-publisher/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/observation-publisher/requirements.txt b/python/observation-publisher/requirements.txt index 563ee6aa7..0803cd91e 100644 --- a/python/observation-publisher/requirements.txt +++ b/python/observation-publisher/requirements.txt @@ -30,7 +30,7 @@ botocore==1.35.39 # s3transfer cachetools==5.3.2 # via google-auth -caraml-auth-google==0.0.0.post7 +caraml-auth-google==0.0.0.post16.dev0 # via merlin-sdk caraml-upi-protos==1.0.0 # via From b1518e47681238618e693a62b1b7206d641d7bb8 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:20:03 +0700 Subject: [PATCH 27/39] chore: adjust grpcio version --- python/sdk/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/sdk/requirements.txt b/python/sdk/requirements.txt index c4c22fdba..d6cb0f8bc 100644 --- a/python/sdk/requirements.txt +++ b/python/sdk/requirements.txt @@ -19,4 +19,4 @@ numpy>=1.26.4 caraml-auth-google==0.0.0.post16.dev0 pydantic==2.5.3 grpcio-tools>=1.50.0,<1.63 -grpcio==1.71.0 \ No newline at end of file +grpcio>=1.60.1 \ No newline at end of file From be8a3e89c00d0df5d269f17bdbb8908aea89161f Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 14:42:28 +0700 Subject: [PATCH 28/39] chore: try to deprecate urllib3-mock and use responses instead --- python/sdk/Pipfile | 1 + python/sdk/requirements_test.txt | 1 - python/sdk/test/client_test.py | 50 ++++++++++++-------------------- python/sdk/test/conftest.py | 18 +++++------- 4 files changed, 27 insertions(+), 43 deletions(-) diff --git a/python/sdk/Pipfile b/python/sdk/Pipfile index 10a90835f..236b4f71f 100644 --- a/python/sdk/Pipfile +++ b/python/sdk/Pipfile @@ -12,3 +12,4 @@ pytest-xdist = "*" [packages] merlin-sdk = {extras = ["test"],path = "."} cookiecutter = '==2.1.1' +responses = ">=0.25,<1.0" diff --git a/python/sdk/requirements_test.txt b/python/sdk/requirements_test.txt index 2eface6ea..dda3e2045 100644 --- a/python/sdk/requirements_test.txt +++ b/python/sdk/requirements_test.txt @@ -13,6 +13,5 @@ types-python-dateutil types-PyYAML types-six types-protobuf -urllib3-mock>=0.3.3 xarray xgboost==1.6.2 \ No newline at end of file diff --git a/python/sdk/test/client_test.py b/python/sdk/test/client_test.py index 1f2a85dc2..a25559e7d 100644 --- a/python/sdk/test/client_test.py +++ b/python/sdk/test/client_test.py @@ -18,6 +18,7 @@ from unittest import mock import pytest +import responses import client as cl from client import ApiClient, Configuration @@ -27,9 +28,6 @@ from merlin.util import guess_mlp_ui_url from merlin.version import VERSION -# get global mock responses that configured in conftest -responses = pytest.responses - @pytest.fixture def mock_url(): @@ -81,9 +79,8 @@ def serialize_datetime(obj): return obj.isoformat() raise TypeError("Type is not serializable") -@responses.activate def test_get_project(mock_url, mock_oauth, use_google_oauth): - responses.add( + mock_responses.add( "GET", "/api/v1/projects", body=f"""[{{ @@ -100,9 +97,9 @@ def test_get_project(mock_url, mock_oauth, use_google_oauth): m = MerlinClient(mock_url, use_google_oauth=use_google_oauth) p = m.get_project("my-project") - assert responses.calls[-1].request.method == "GET" - assert responses.calls[-1].request.url == "/api/v1/projects?name=my-project" - assert responses.calls[-1].request.host == "merlin.dev" + assert mock_responses.calls[-1].request.method == "GET" + assert mock_responses.calls[-1].request.url == "/api/v1/projects?name=my-project" + assert mock_responses.calls[-1].request.host == "merlin.dev" assert p.id == 0 assert p.name == "my-project" @@ -112,7 +109,6 @@ def test_get_project(mock_url, mock_oauth, use_google_oauth): assert isinstance(p.updated_at, datetime.datetime) -@responses.activate def test_create_invalid_project_name( mock_url, api_client, mock_oauth, use_google_oauth ): @@ -125,7 +121,6 @@ def test_create_invalid_project_name( assert client.get_project(project_name) -@responses.activate def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1010 mlflow_experiment_id = 1 @@ -134,14 +129,14 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): model_type = ModelType.XGBOOST mlflow_url = "http://mlflow.api.merlin.dev" - responses.add( + mock_responses.add( "GET", f"/api/v1/projects/{project_id}/models", body="[]", status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", f"/api/v1/projects/{project_id}/models", body=f"""{{ @@ -173,7 +168,7 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): "my-model", project_name=project_name, model_type=model_type ) - assert json.loads(responses.calls[-1].request.body) == json.loads( + assert json.loads(mock_responses.calls[-1].request.body) == json.loads( f""" {{ "name" : "{model_name}", @@ -191,15 +186,14 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): assert isinstance(model.updated_at, datetime.datetime) assert model.project == project assert ( - f"merlin-sdk/{VERSION}" in responses.calls[-1].request.headers["User-Agent"] + f"merlin-sdk/{VERSION}" in mock_responses.calls[-1].request.headers["User-Agent"] ) assert ( f"python/{version_info.major}.{version_info.minor}.{version_info.micro}" - in responses.calls[-1].request.headers["User-Agent"] + in mock_responses.calls[-1].request.headers["User-Agent"] ) -@responses.activate def test_create_invalid_model_name(mock_url, api_client, mock_oauth, use_google_oauth): model_name = "invalidModelName" project_name = "my-project" @@ -212,7 +206,6 @@ def test_create_invalid_model_name(mock_url, api_client, mock_oauth, use_google_ assert client.get_or_create_model(model_name, project_name, model_type) -@responses.activate def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1010 mlflow_experiment_id = 1 @@ -221,7 +214,7 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): model_type = ModelType.XGBOOST mlflow_url = "http://mlflow.api.merlin.dev" - responses.add( + mock_responses.add( "GET", f"/api/v1/projects/{project_id}/models", body=f"""[{{ @@ -239,7 +232,7 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", f"/api/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()], default=serialize_datetime), @@ -275,7 +268,6 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): assert default_model_endpoint.environment_name == env_1.name -@responses.activate def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1 model_id = 1 @@ -290,7 +282,7 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): created_at = "2019-09-04T03:09:13.842Z" updated_at = "2019-09-04T03:09:13.843Z" - responses.add( + mock_responses.add( "POST", f"/api/v1/models/{model_id}/versions", body=f"""{{ @@ -343,9 +335,8 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): assert mv.url == f"{ui_url}/projects/1/models/{model_id}/versions" -@responses.activate def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): - responses.add( + mock_responses.add( "GET", "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -364,9 +355,8 @@ def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): assert envs[1].is_default == env_2.is_default -@responses.activate def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): - responses.add( + mock_responses..add( "GET", "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -385,9 +375,8 @@ def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): assert env is None -@responses.activate def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): - responses.add( + mock_responses.add( "GET", "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -401,9 +390,9 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa assert env.cluster == env_1.cluster assert env.is_default == env_1.is_default - responses.reset() + mock_responses.reset() - responses.add( + mock_responses.add( "GET", "/api/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -415,10 +404,9 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa assert env is None -@responses.activate def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): client = MerlinClient(mock_url, use_google_oauth=use_google_oauth) - responses.add( + mock_responses.add( "GET", "/api/v1/environments", body=json.dumps([env_2.to_dict()]), diff --git a/python/sdk/test/conftest.py b/python/sdk/test/conftest.py index b4ad8d7a5..e0c9e473f 100644 --- a/python/sdk/test/conftest.py +++ b/python/sdk/test/conftest.py @@ -19,22 +19,18 @@ import uuid import requests as requests_lib from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry -from urllib3_mock import Responses +import pytest +import responses +from urllib3.util.retry import Retry import client as cl from client import ApiClient, Configuration from merlin.model import Model, ModelType, ModelVersion, Project - -# From the documentation (https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure): -# Allow plugins and conftest files to perform initial configuration. -# This hook is called for every plugin and initial conftest file after command line options have been parsed. -# After that, the hook is called for other conftest files as they are imported. -def pytest_configure(): - # share mock responses as global variable so it can be reused and called as decorator on other files. - pytest.responses = Responses("requests.packages.urllib3") - +@pytest.fixture +def mock_responses(): + with responses.RequestsMock() as rsps: + yield rsps @pytest.fixture def url(): From 2311cae76c88b66aa66fa9c77ef6fe62eb72ae77 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 15:00:14 +0700 Subject: [PATCH 29/39] chore: fix invalid syntax and remove unnecessary changes --- .github/workflows/release.yml | 5 ++--- python/sdk/test/client_test.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 173d12eb8..2191a9806 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,6 +103,8 @@ jobs: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] + needs: + - publish-python-sdk steps: - uses: actions/checkout@v4 - name: Log in to the Container registry @@ -111,9 +113,6 @@ jobs: registry: ${{ env.DOCKER_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Install dependencies - working-directory: ./python/sdk - run: pip install "setuptools>=64,<75" "setuptools_scm>=8" "twine" "wheel" - name: Build Merlin SDK Docker py${{ matrix.python-version }} env: CONTAINER_REGISTRY: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }} diff --git a/python/sdk/test/client_test.py b/python/sdk/test/client_test.py index a25559e7d..ed845311e 100644 --- a/python/sdk/test/client_test.py +++ b/python/sdk/test/client_test.py @@ -356,7 +356,7 @@ def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): - mock_responses..add( + mock_responses.add( "GET", "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), From 65502d546d25695eb54b201ed5008b1fcd2b2d1b Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 15:39:16 +0700 Subject: [PATCH 30/39] chore: use PIPENV_VENV_IN_PROJECT --- .github/workflows/merlin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 54a678f3d..0eab8c6e1 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -46,6 +46,7 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} + PIPENV_VENV_IN_PROJECT: true steps: - uses: actions/checkout@v4 - name: Set up Google Cloud SDK From 2a3034bac7c2e181824bb6f551f6c07336988167 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 15:47:14 +0700 Subject: [PATCH 31/39] feat: use mock_responses instead of responses --- python/sdk/test/merlin_test.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/python/sdk/test/merlin_test.py b/python/sdk/test/merlin_test.py index 691a8b7fc..a3b28ce59 100644 --- a/python/sdk/test/merlin_test.py +++ b/python/sdk/test/merlin_test.py @@ -16,14 +16,12 @@ import mlflow import pytest +import responses import client as cl import merlin from merlin.model import ModelVersion -# get global mock responses that configured in conftest -responses = pytest.responses - default_resource_request = cl.ResourceRequest(min_replica=1, max_replica=1, cpu_request="100m", memory_request="128Mi") env_1 = cl.Environment( id=1, @@ -46,7 +44,6 @@ def test_set_url(url, use_google_oauth): assert url == merlin.get_url() -@responses.activate def test_set_project(url, project, mock_oauth, use_google_oauth): # expect exception when setting project but client is not set with pytest.raises(Exception): @@ -62,7 +59,6 @@ def test_set_project(url, project, mock_oauth, use_google_oauth): assert merlin.active_project().mlflow_tracking_url == project.mlflow_tracking_url -@responses.activate def test_set_model(url, project, model, mock_oauth, use_google_oauth): # expect exception when setting model but client and project is not set with pytest.raises(Exception): @@ -85,7 +81,6 @@ def test_set_model(url, project, model, mock_oauth, use_google_oauth): assert merlin.active_model().mlflow_experiment_id == model.mlflow_experiment_id -@responses.activate def test_new_model_version(url, project, model, version, mock_oauth, use_google_oauth): # expect exception when creating new model version but client and # project is not set @@ -117,7 +112,6 @@ def test_new_model_version(url, project, model, version, mock_oauth, use_google_ assert v.mlflow_run_id == version.mlflow_run_id -@responses.activate def test_new_model_version_with_labels( url, project, model, version, mock_oauth, use_google_oauth ): @@ -140,7 +134,6 @@ def test_new_model_version_with_labels( assert labels[key] == value -@responses.activate def test_list_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) @@ -153,7 +146,6 @@ def test_list_environment(url, mock_oauth, use_google_oauth): assert envs[1].name == env_2.name -@responses.activate def test_get_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) @@ -167,7 +159,6 @@ def test_get_environment(url, mock_oauth, use_google_oauth): assert env is None -@responses.activate def test_get_default_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) @@ -180,7 +171,6 @@ def test_get_default_environment(url, mock_oauth, use_google_oauth): assert env.is_default -@responses.activate def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oauth): _mock_get_project_call(project) _mock_get_model_call(project, model) @@ -202,7 +192,7 @@ def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oau def _mock_get_project_call(project): - responses.add( + mock_responses.add( "GET", "/v1/projects", body=f"""[{{ @@ -218,7 +208,7 @@ def _mock_get_project_call(project): def _mock_get_model_call(project, model): - responses.add( + mock_responses.add( "GET", f"/v1/projects/{project.id}/models", body=f"""[{{ @@ -247,7 +237,7 @@ def _mock_new_model_version_call(model, version, labels=None): if labels is not None: body["labels"] = labels - responses.add( + mock_responses.add( "POST", f"/v1/models/{model.id}/versions", body=json.dumps(body), @@ -257,7 +247,7 @@ def _mock_new_model_version_call(model, version, labels=None): def _mock_list_environment_call(): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), From e9577a9b515199b7634bc62d62998b1903d1c01b Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 15:53:20 +0700 Subject: [PATCH 32/39] feat: use PIPENV_VENV_IN_PROJECT: true in test-pyfunc-server --- .github/workflows/merlin.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 0eab8c6e1..b6c885dcc 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -81,6 +81,7 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} + PIPENV_VENV_IN_PROJECT: true steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From 20cf31704ecd1094b964598dc2e03d67049e059a Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 15:56:28 +0700 Subject: [PATCH 33/39] feat: use mock_responses in model_test.py --- python/sdk/test/model_test.py | 343 +++++++++++++++++----------------- 1 file changed, 169 insertions(+), 174 deletions(-) diff --git a/python/sdk/test/model_test.py b/python/sdk/test/model_test.py index 2ea2da9a7..636f4293a 100644 --- a/python/sdk/test/model_test.py +++ b/python/sdk/test/model_test.py @@ -20,6 +20,7 @@ import client import client as cl import pytest +import responses from merlin import AutoscalingPolicy, DeploymentMode, MetricsType from merlin.autoscaling import ( RAW_DEPLOYMENT_DEFAULT_AUTOSCALING_POLICY, @@ -34,9 +35,6 @@ from merlin.model_schema import InferenceSchema, ModelSchema, RankingOutput, ValueType from merlin.protocol import Protocol from merlin.model_observability import ModelObservability -from urllib3_mock import Responses - -responses = Responses("requests.packages.urllib3") default_resource_request = cl.ResourceRequest( min_replica=1, max_replica=1, cpu_request="100m", memory_request="128Mi" @@ -318,9 +316,8 @@ class TestProject: secret_1 = cl.Secret(id=1, name="secret-1", data="secret-data-1") secret_2 = cl.Secret(id=2, name="secret-2", data="secret-data-2") - @responses.activate def test_create_secret(self, project): - responses.add( + mock_responses.add( "POST", "/v1/projects/1/secrets", body=json.dumps(self.secret_1.to_dict()), @@ -329,20 +326,19 @@ def test_create_secret(self, project): ) project.create_secret(self.secret_1.name, self.secret_1.data) - actual_body = json.loads(responses.calls[0].request.body) + actual_body = json.loads(mock_responses.calls[0].request.body) assert actual_body["name"] == self.secret_1.name assert actual_body["data"] == self.secret_1.data - @responses.activate def test_update_secret(self, project): - responses.add( + mock_responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "PATCH", "/v1/projects/1/secrets/1", body=json.dumps(self.secret_1.to_dict()), @@ -352,14 +348,14 @@ def test_update_secret(self, project): project.update_secret(self.secret_1.name, "new-data") - actual_body = json.loads(responses.calls[1].request.body) + actual_body = json.loads(mock_responses.calls[1].request.body) assert actual_body["name"] == self.secret_1.name assert actual_body["data"] == "new-data" - responses.reset() + mock_responses.reset() # test secret not found - responses.add( + mock_responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict()]), @@ -373,16 +369,15 @@ def test_update_secret(self, project): ): project.update_secret(self.secret_2.name, "new-data") - @responses.activate def test_delete_secret(self, project): - responses.add( + mock_responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "DELETE", "/v1/projects/1/secrets/1", status=204, @@ -391,10 +386,10 @@ def test_delete_secret(self, project): project.delete_secret(self.secret_1.name) - responses.reset() + mock_responses.reset() # test secret not found - responses.add( + mock_responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict()]), @@ -408,9 +403,9 @@ def test_delete_secret(self, project): ): project.delete_secret(self.secret_2.name) - @responses.activate + def test_list_secret(self, project): - responses.add( + mock_responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), @@ -423,9 +418,9 @@ def test_list_secret(self, project): class TestModelVersion: - @responses.activate + def test_list_endpoint(self, version): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), @@ -438,9 +433,9 @@ def test_list_endpoint(self, version): assert endpoints[0].id == ep1.id assert endpoints[1].id == ep2.id - @responses.activate + def test_deploy(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -448,28 +443,28 @@ def test_deploy(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep1.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict()]), @@ -488,9 +483,9 @@ def test_deploy(self, version): assert endpoint.autoscaling_policy == SERVERLESS_DEFAULT_AUTOSCALING_POLICY assert endpoint.protocol == Protocol.HTTP_JSON - @responses.activate + def test_deploy_upiv1(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -498,28 +493,28 @@ def test_deploy_upiv1(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(upi_ep.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(upi_ep.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([upi_ep.to_dict()]), @@ -538,9 +533,9 @@ def test_deploy_upiv1(self, version): assert endpoint.autoscaling_policy == SERVERLESS_DEFAULT_AUTOSCALING_POLICY assert endpoint.protocol == Protocol.UPI_V1 - @responses.activate + def test_deploy_using_raw_deployment_mode(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -548,28 +543,28 @@ def test_deploy_using_raw_deployment_mode(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep3.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep3.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep3.to_dict()]), @@ -589,9 +584,9 @@ def test_deploy_using_raw_deployment_mode(self, version): assert endpoint.deployment_mode == DeploymentMode.RAW_DEPLOYMENT assert endpoint.autoscaling_policy == RAW_DEPLOYMENT_DEFAULT_AUTOSCALING_POLICY - @responses.activate + def test_deploy_with_autoscaling_policy(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -599,28 +594,28 @@ def test_deploy_with_autoscaling_policy(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep4.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep4.to_dict()]), @@ -644,10 +639,10 @@ def test_deploy_with_autoscaling_policy(self, version): assert endpoint.autoscaling_policy.metrics_type == MetricsType.CPU_UTILIZATION assert endpoint.autoscaling_policy.target_value == 10 - @responses.activate + def test_deploy_default_env(self, version): # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), @@ -655,7 +650,7 @@ def test_deploy_default_env(self, version): content_type="application/json", ) # no default environment - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -666,8 +661,8 @@ def test_deploy_default_env(self, version): version.deploy() # default environment exists - responses.reset() - responses.add( + mock_responses.reset() + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -675,28 +670,28 @@ def test_deploy_default_env(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep1.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict()]), @@ -712,9 +707,9 @@ def test_deploy_default_env(self, version): assert endpoint.environment.cluster == env_1.cluster assert endpoint.environment.name == env_1.name - @responses.activate + def test_redeploy_model(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -722,28 +717,28 @@ def test_redeploy_model(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep3.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "PUT", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep4.to_dict()]), @@ -768,9 +763,9 @@ def test_redeploy_model(self, version): assert endpoint.autoscaling_policy.metrics_type == MetricsType.CPU_UTILIZATION assert endpoint.autoscaling_policy.target_value == 10 - @responses.activate + def test_deploy_with_gpu(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -778,28 +773,28 @@ def test_deploy_with_gpu(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep5.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/789", body=json.dumps(ep5.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep5.to_dict()]), @@ -821,9 +816,9 @@ def test_deploy_with_gpu(self, version): == resource_request_with_gpu.gpu_request ) - @responses.activate + def test_deploy_with_model_observability_enabled(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -831,28 +826,28 @@ def test_deploy_with_model_observability_enabled(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(observability_enabled_ep.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([observability_enabled_ep.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/7899", body=json.dumps(observability_enabled_ep.to_dict()), @@ -873,9 +868,9 @@ def test_deploy_with_model_observability_enabled(self, version): assert endpoint.enable_model_observability == True - @responses.activate + def test_deploy_with_more_granular_model_observability_cfg(self, version): - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -883,28 +878,28 @@ def test_deploy_with_more_granular_model_observability_cfg(self, version): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(more_granular_observability_cfg_ep.to_dict()), status=201, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([more_granular_observability_cfg_ep.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint/8000", body=json.dumps(more_granular_observability_cfg_ep.to_dict()), @@ -925,9 +920,9 @@ def test_deploy_with_more_granular_model_observability_cfg(self, version): assert endpoint.deployment_mode == DeploymentMode.SERVERLESS assert endpoint.model_observability == model_observability - @responses.activate + def test_undeploy(self, version): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep2.to_dict()]), @@ -936,17 +931,17 @@ def test_undeploy(self, version): ) version.undeploy(environment_name=env_1.name) - assert len(responses.calls) == 1 + assert len(mock_responses.calls) == 1 - responses.reset() - responses.add( + mock_responses.reset() + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "DELETE", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), @@ -955,12 +950,12 @@ def test_undeploy(self, version): ) version.undeploy(environment_name=env_1.name) - assert len(responses.calls) == 2 + assert len(mock_responses.calls) == 2 - @responses.activate + def test_undeploy_default_env(self, version): # This is the additional check which deploy makes to determine if there are any existing endpoints associated - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), @@ -968,7 +963,7 @@ def test_undeploy_default_env(self, version): content_type="application/json", ) # no default environment - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -978,8 +973,8 @@ def test_undeploy_default_env(self, version): with pytest.raises(ValueError): version.deploy() - responses.reset() - responses.add( + mock_responses.reset() + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -987,7 +982,7 @@ def test_undeploy_default_env(self, version): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep2.to_dict()]), @@ -996,24 +991,24 @@ def test_undeploy_default_env(self, version): ) version.undeploy() - assert len(responses.calls) == 2 + assert len(mock_responses.calls) == 2 - responses.reset() - responses.add( + mock_responses.reset() + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "DELETE", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), @@ -1022,11 +1017,11 @@ def test_undeploy_default_env(self, version): ) version.undeploy() - assert len(responses.calls) == 3 + assert len(mock_responses.calls) == 3 - @responses.activate + def test_list_prediction_job(self, version): - responses.add( + mock_responses.add( method="GET", url="/v1/models/1/versions/1/jobs-by-page?page=1", body=json.dumps({ @@ -1041,7 +1036,7 @@ def test_list_prediction_job(self, version): content_type="application/json", match_querystring=True, ) - responses.add( + mock_responses.add( method="GET", url="/v1/models/1/versions/1/jobs-by-page?page=2", body=json.dumps({ @@ -1068,10 +1063,10 @@ def test_list_prediction_job(self, version): assert jobs[1].status == JobStatus(job_2.status) assert jobs[1].error == job_2.error - @responses.activate + def test_create_prediction_job(self, version): job_1.status = "completed" - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1106,7 +1101,7 @@ def test_create_prediction_job(self, version): assert j.error == job_1.error assert j.name == job_1.name - actual_req = json.loads(responses.calls[0].request.body) + actual_req = json.loads(mock_responses.calls[0].request.body) assert actual_req["config"]["job_config"]["bigquery_source"] == bq_src.to_dict() assert actual_req["config"]["job_config"]["bigquery_sink"] == bq_sink.to_dict() assert ( @@ -1125,10 +1120,10 @@ def test_create_prediction_job(self, version): @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - @responses.activate + def test_create_prediction_job_with_retry_failed(self, version): job_1.status = "pending" - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1137,7 +1132,7 @@ def test_create_prediction_job_with_retry_failed(self, version): ) for i in range(5): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1171,14 +1166,14 @@ def test_create_prediction_job_with_retry_failed(self, version): assert j.id == job_1.id assert j.error == job_1.error assert j.name == job_1.name - assert len(responses.calls) == 6 + assert len(mock_responses.calls) == 6 @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - @responses.activate + def test_create_prediction_job_with_retry_success(self, version): job_1.status = "pending" - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1208,10 +1203,10 @@ def _find_match_patched(self, request): else: return match - responses._find_match = types.MethodType(_find_match_patched, responses) + mock_responses._find_match = types.MethodType(_find_match_patched, responses) for i in range(4): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1220,7 +1215,7 @@ def _find_match_patched(self, request): ) job_1.status = "completed" - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1255,7 +1250,7 @@ def _find_match_patched(self, request): assert j.error == job_1.error assert j.name == job_1.name - actual_req = json.loads(responses.calls[0].request.body) + actual_req = json.loads(mock_responses.calls[0].request.body) assert actual_req["config"]["job_config"]["bigquery_source"] == bq_src.to_dict() assert actual_req["config"]["job_config"]["bigquery_sink"] == bq_sink.to_dict() assert ( @@ -1271,17 +1266,17 @@ def _find_match_patched(self, request): == ModelType.PYFUNC_V2.value.upper() ) assert actual_req["config"]["service_account_name"] == "my-service-account" - assert len(responses.calls) == 6 + assert len(mock_responses.calls) == 6 # unpatch - responses._find_match = types.MethodType(_find_match, responses) + mock_responses._find_match = types.MethodType(_find_match, responses) @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - @responses.activate + def test_create_prediction_job_with_retry_pending_then_failed(self, version): job_1.status = "pending" - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1311,10 +1306,10 @@ def _find_match_patched(self, request): else: return match - responses._find_match = types.MethodType(_find_match_patched, responses) + mock_responses._find_match = types.MethodType(_find_match_patched, responses) for i in range(3): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1322,7 +1317,7 @@ def _find_match_patched(self, request): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1332,7 +1327,7 @@ def _find_match_patched(self, request): job_1.status = "failed" for i in range(5): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1368,13 +1363,13 @@ def _find_match_patched(self, request): assert j.name == job_1.name # unpatch - responses._find_match = types.MethodType(_find_match, responses) - assert len(responses.calls) == 10 + mock_responses._find_match = types.MethodType(_find_match, responses) + assert len(mock_responses.calls) == 10 - @responses.activate + def test_stop_prediction_job(self, version): job_1.status = "pending" - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1382,7 +1377,7 @@ def test_stop_prediction_job(self, version): content_type="application/json", ) - responses.add( + mock_responses.add( "PUT", "/v1/models/1/versions/1/jobs/1/stop", status=204, @@ -1390,7 +1385,7 @@ def test_stop_prediction_job(self, version): ) job_1.status = "terminated" - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1426,9 +1421,9 @@ def test_stop_prediction_job(self, version): assert j.error == job_1.error assert j.name == job_1.name - @responses.activate + def test_model_version_deletion(self, version): - responses.add( + mock_responses.add( "DELETE", "/v1/models/1/versions/1", body=json.dumps(1), @@ -1491,9 +1486,9 @@ class TestModel: model_schema=schema, ) - @responses.activate + def test_list_version(self, model): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=", match_querystring=True, @@ -1502,7 +1497,7 @@ def test_list_version(self, model): adding_headers={"Next-Cursor": "abcdef"}, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=abcdef&search=", match_querystring=True, @@ -1515,9 +1510,9 @@ def test_list_version(self, model): assert versions[0].id == 1 assert versions[1].id == 2 - @responses.activate + def test_list_version_with_labels(self, model): - responses.add( + mock_responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=labels%3Amodel+in+%28T-800%29", body=json.dumps([self.v3.to_dict()]), @@ -1530,9 +1525,9 @@ def test_list_version_with_labels(self, model): assert versions[0].id == 3 assert versions[0].labels["model"] == "T-800" - @responses.activate + def test_list_endpoint(self, model): - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict(), mdl_endpoint_2.to_dict()]), @@ -1545,9 +1540,9 @@ def test_list_endpoint(self, model): assert endpoints[0].id == mdl_endpoint_1.id assert endpoints[1].id == mdl_endpoint_2.id - @responses.activate + def test_new_model_version(self, model): - responses.add( + mock_responses.add( "POST", "/v1/models/1/versions", body=json.dumps(self.v4.to_dict()), @@ -1564,7 +1559,7 @@ def test_new_model_version(self, model): assert mv._labels == {"model": "T-800"} assert mv._model_schema == self.merlin_model_schema - @responses.activate + def test_serve_traffic(self, model): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): @@ -1582,14 +1577,14 @@ def test_serve_traffic(self, model): ) # test create - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1603,24 +1598,24 @@ def test_serve_traffic(self, model): ) assert endpoint.protocol == Protocol.HTTP_JSON - responses.reset() + mock_responses.reset() # test update - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1633,7 +1628,7 @@ def test_serve_traffic(self, model): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - @responses.activate + def test_stop_serving_traffic(self, model): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): @@ -1651,14 +1646,14 @@ def test_stop_serving_traffic(self, model): ) # test create - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1671,39 +1666,39 @@ def test_stop_serving_traffic(self, model): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - responses.reset() + mock_responses.reset() # test DELETE - responses.reset() - responses.add( + mock_responses.reset() + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "DELETE", "/v1/models/1/endpoints/1", status=200, content_type="application/json", ) model.stop_serving_traffic(endpoint.environment_name) - assert len(responses.calls) == 2 + assert len(mock_responses.calls) == 2 - @responses.activate + def test_serve_traffic_default_env(self, model): ve = VersionEndpoint(ep1) # no default environment - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -1713,24 +1708,24 @@ def test_serve_traffic_default_env(self, model): with pytest.raises(ValueError): model.serve_traffic({ve: 100}) - responses.reset() + mock_responses.reset() # test create - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1743,31 +1738,31 @@ def test_serve_traffic_default_env(self, model): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - responses.reset() + mock_responses.reset() # test update - responses.add( + mock_responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1780,18 +1775,18 @@ def test_serve_traffic_default_env(self, model): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - @responses.activate + def test_serve_traffic_upi(self, model): ve = VersionEndpoint(upi_ep) # test create - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_upi.to_dict()), @@ -1805,24 +1800,24 @@ def test_serve_traffic_upi(self, model): ) assert endpoint.protocol == Protocol.UPI_V1 - responses.reset() + mock_responses.reset() # test update - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_upi.to_dict()]), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_upi.to_dict()), status=200, content_type="application/json", ) - responses.add( + mock_responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_upi.to_dict()), @@ -1836,9 +1831,9 @@ def test_serve_traffic_upi(self, model): ) assert endpoint.protocol == Protocol.UPI_V1 - @responses.activate + def test_model_deletion(self, model): - responses.add( + mock_responses.add( "DELETE", "/v1/projects/1/models/1", body=json.dumps(1), From 35091dd3a2a40f91872faf0b0d874428ec34dbb7 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 16:06:06 +0700 Subject: [PATCH 34/39] feat: use mock_responses in conftest.py --- python/sdk/test/conftest.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/python/sdk/test/conftest.py b/python/sdk/test/conftest.py index e0c9e473f..0fb325d36 100644 --- a/python/sdk/test/conftest.py +++ b/python/sdk/test/conftest.py @@ -154,10 +154,8 @@ def gpu_config(): @pytest.fixture -def mock_oauth(): - responses = pytest.responses - - responses.add( +def mock_oauth(mock_responses): + mock_responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/", body=""" @@ -171,7 +169,7 @@ def mock_oauth(): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/computeengine@google.com/token", body=""" @@ -187,7 +185,7 @@ def mock_oauth(): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/?recursive=true", body=""" @@ -201,7 +199,7 @@ def mock_oauth(): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/identity?audience=sdk.caraml&format=full", body="""eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg""", @@ -210,7 +208,7 @@ def mock_oauth(): match_querystring=True, ) - responses.add( + mock_responses.add( "GET", "/computeMetadata/api/v1/instance/service-accounts/default/", body=""" @@ -224,7 +222,7 @@ def mock_oauth(): content_type="application/json", ) - responses.add( + mock_responses.add( "GET", "/computeMetadata/api/v1/instance/service-accounts/computeengine@google.com/token", body=""" @@ -240,7 +238,7 @@ def mock_oauth(): content_type="application/json", ) - responses.add( + mock_responses.add( "POST", "/token", body=""" From d21c0b4ace98344d4210471a7fedafd05c8d7001 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 16:23:40 +0700 Subject: [PATCH 35/39] chore: add mock_response in param --- python/sdk/test/client_test.py | 12 +++---- python/sdk/test/merlin_test.py | 54 ++++++++++++++-------------- python/sdk/test/model_test.py | 64 +++++++++++++++++----------------- 3 files changed, 65 insertions(+), 65 deletions(-) diff --git a/python/sdk/test/client_test.py b/python/sdk/test/client_test.py index ed845311e..a03c2be92 100644 --- a/python/sdk/test/client_test.py +++ b/python/sdk/test/client_test.py @@ -79,7 +79,7 @@ def serialize_datetime(obj): return obj.isoformat() raise TypeError("Type is not serializable") -def test_get_project(mock_url, mock_oauth, use_google_oauth): +def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", "/api/v1/projects", @@ -121,7 +121,7 @@ def test_create_invalid_project_name( assert client.get_project(project_name) -def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): +def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): project_id = 1010 mlflow_experiment_id = 1 model_name = "my-model" @@ -206,7 +206,7 @@ def test_create_invalid_model_name(mock_url, api_client, mock_oauth, use_google_ assert client.get_or_create_model(model_name, project_name, model_type) -def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): +def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): project_id = 1010 mlflow_experiment_id = 1 model_name = "my-model" @@ -355,7 +355,7 @@ def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): assert envs[1].is_default == env_2.is_default -def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): +def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", "/api/v1/environments", @@ -375,7 +375,7 @@ def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): assert env is None -def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): +def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", "/api/v1/environments", @@ -404,7 +404,7 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa assert env is None -def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): +def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): client = MerlinClient(mock_url, use_google_oauth=use_google_oauth) mock_responses.add( "GET", diff --git a/python/sdk/test/merlin_test.py b/python/sdk/test/merlin_test.py index a3b28ce59..a0e92ac7d 100644 --- a/python/sdk/test/merlin_test.py +++ b/python/sdk/test/merlin_test.py @@ -44,12 +44,12 @@ def test_set_url(url, use_google_oauth): assert url == merlin.get_url() -def test_set_project(url, project, mock_oauth, use_google_oauth): +def test_set_project(url, project, mock_oauth, use_google_oauth, mock_responses): # expect exception when setting project but client is not set with pytest.raises(Exception): merlin.set_project(project.name) - _mock_get_project_call(project) + _mock_get_project_call(project, mock_responses) merlin.set_url(url, use_google_oauth=use_google_oauth) merlin.set_project(project.name) @@ -59,7 +59,7 @@ def test_set_project(url, project, mock_oauth, use_google_oauth): assert merlin.active_project().mlflow_tracking_url == project.mlflow_tracking_url -def test_set_model(url, project, model, mock_oauth, use_google_oauth): +def test_set_model(url, project, model, mock_oauth, use_google_oauth, mock_responses): # expect exception when setting model but client and project is not set with pytest.raises(Exception): merlin.set_model(model.name, model.type) @@ -69,10 +69,10 @@ def test_set_model(url, project, model, mock_oauth, use_google_oauth): with pytest.raises(Exception): merlin.set_model(model.name, model.type) - _mock_get_project_call(project) + _mock_get_project_call(project, mock_responses) merlin.set_project(project.name) - _mock_get_model_call(project, model) + _mock_get_model_call(project, model, mock_responses) merlin.set_model(model.name, model.type) assert merlin.active_model().name == model.name @@ -81,7 +81,7 @@ def test_set_model(url, project, model, mock_oauth, use_google_oauth): assert merlin.active_model().mlflow_experiment_id == model.mlflow_experiment_id -def test_new_model_version(url, project, model, version, mock_oauth, use_google_oauth): +def test_new_model_version(url, project, model, version, mock_oauth, use_google_oauth, mock_responses): # expect exception when creating new model version but client and # project is not set with pytest.raises(Exception): @@ -94,17 +94,17 @@ def test_new_model_version(url, project, model, version, mock_oauth, use_google_ with merlin.new_model_version() as v: print(v) - _mock_get_project_call(project) + _mock_get_project_call(project, mock_responses) merlin.set_project(project.name) with pytest.raises(Exception): with merlin.new_model_version() as v: print(v) - _mock_get_model_call(project, model) + _mock_get_model_call(project, model, mock_responses) merlin.set_model(model.name, model.type) - _mock_new_model_version_call(model, version) + _mock_new_model_version_call(model, version, mock_responses) with merlin.new_model_version() as v: assert v is not None assert isinstance(v, ModelVersion) @@ -113,17 +113,17 @@ def test_new_model_version(url, project, model, version, mock_oauth, use_google_ def test_new_model_version_with_labels( - url, project, model, version, mock_oauth, use_google_oauth + url, project, model, version, mock_oauth, use_google_oauth, mock_responses ): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_get_project_call(project) + _mock_get_project_call(project, mock_responses) merlin.set_project(project.name) - _mock_get_model_call(project, model) + _mock_get_model_call(project, model, mock_responses) merlin.set_model(model.name, model.type) # Insert labels labels = {"model": "T-800", "software": "skynet"} - _mock_new_model_version_call(model, version, labels) + _mock_new_model_version_call(model, version, mock_responses, labels) with merlin.new_model_version(labels=labels) as v: assert v is not None @@ -134,10 +134,10 @@ def test_new_model_version_with_labels( assert labels[key] == value -def test_list_environment(url, mock_oauth, use_google_oauth): +def test_list_environment(url, mock_oauth, use_google_oauth, mock_responses): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call() + _mock_list_environment_call(mock_responses) envs = merlin.list_environment() @@ -146,10 +146,10 @@ def test_list_environment(url, mock_oauth, use_google_oauth): assert envs[1].name == env_2.name -def test_get_environment(url, mock_oauth, use_google_oauth): +def test_get_environment(url, mock_oauth, use_google_oauth, mock_responses): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call() + _mock_list_environment_call(mock_responses) env = merlin.get_environment(env_1.name) assert env is not None @@ -159,10 +159,10 @@ def test_get_environment(url, mock_oauth, use_google_oauth): assert env is None -def test_get_default_environment(url, mock_oauth, use_google_oauth): +def test_get_default_environment(url, mock_oauth, use_google_oauth, mock_responses): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call() + _mock_list_environment_call(mock_responses) env = merlin.get_default_environment() @@ -171,10 +171,10 @@ def test_get_default_environment(url, mock_oauth, use_google_oauth): assert env.is_default -def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oauth): - _mock_get_project_call(project) - _mock_get_model_call(project, model) - _mock_new_model_version_call(model, version) +def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oauth, mock_responses): + _mock_get_project_call(project, mock_responses) + _mock_get_model_call(project, model, mock_responses) + _mock_new_model_version_call(model, version, mock_responses) merlin.set_url(url, use_google_oauth=use_google_oauth) merlin.set_project(project.name) @@ -191,7 +191,7 @@ def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oau assert run.data.tags["tag"] == "value" -def _mock_get_project_call(project): +def _mock_get_project_call(project, mock_responses): mock_responses.add( "GET", "/v1/projects", @@ -207,7 +207,7 @@ def _mock_get_project_call(project): ) -def _mock_get_model_call(project, model): +def _mock_get_model_call(project, model, mock_responses): mock_responses.add( "GET", f"/v1/projects/{project.id}/models", @@ -225,7 +225,7 @@ def _mock_get_model_call(project, model): ) -def _mock_new_model_version_call(model, version, labels=None): +def _mock_new_model_version_call(model, version, mock_responses, labels=None): body = { "id": version.id, "mlflow_run_id": version.mlflow_run_id, @@ -246,7 +246,7 @@ def _mock_new_model_version_call(model, version, labels=None): ) -def _mock_list_environment_call(): +def _mock_list_environment_call(mock_responses): mock_responses.add( "GET", "/v1/environments", diff --git a/python/sdk/test/model_test.py b/python/sdk/test/model_test.py index 636f4293a..aa4b028e7 100644 --- a/python/sdk/test/model_test.py +++ b/python/sdk/test/model_test.py @@ -316,7 +316,7 @@ class TestProject: secret_1 = cl.Secret(id=1, name="secret-1", data="secret-data-1") secret_2 = cl.Secret(id=2, name="secret-2", data="secret-data-2") - def test_create_secret(self, project): + def test_create_secret(self, project, mock_responses): mock_responses.add( "POST", "/v1/projects/1/secrets", @@ -330,7 +330,7 @@ def test_create_secret(self, project): assert actual_body["name"] == self.secret_1.name assert actual_body["data"] == self.secret_1.data - def test_update_secret(self, project): + def test_update_secret(self, project, mock_responses): mock_responses.add( "GET", "/v1/projects/1/secrets", @@ -369,7 +369,7 @@ def test_update_secret(self, project): ): project.update_secret(self.secret_2.name, "new-data") - def test_delete_secret(self, project): + def test_delete_secret(self, project, mock_responses): mock_responses.add( "GET", "/v1/projects/1/secrets", @@ -404,7 +404,7 @@ def test_delete_secret(self, project): project.delete_secret(self.secret_2.name) - def test_list_secret(self, project): + def test_list_secret(self, project, mock_responses): mock_responses.add( "GET", "/v1/projects/1/secrets", @@ -419,7 +419,7 @@ def test_list_secret(self, project): class TestModelVersion: - def test_list_endpoint(self, version): + def test_list_endpoint(self, version, mock_responses): mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", @@ -434,7 +434,7 @@ def test_list_endpoint(self, version): assert endpoints[1].id == ep2.id - def test_deploy(self, version): + def test_deploy(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -484,7 +484,7 @@ def test_deploy(self, version): assert endpoint.protocol == Protocol.HTTP_JSON - def test_deploy_upiv1(self, version): + def test_deploy_upiv1(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -534,7 +534,7 @@ def test_deploy_upiv1(self, version): assert endpoint.protocol == Protocol.UPI_V1 - def test_deploy_using_raw_deployment_mode(self, version): + def test_deploy_using_raw_deployment_mode(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -585,7 +585,7 @@ def test_deploy_using_raw_deployment_mode(self, version): assert endpoint.autoscaling_policy == RAW_DEPLOYMENT_DEFAULT_AUTOSCALING_POLICY - def test_deploy_with_autoscaling_policy(self, version): + def test_deploy_with_autoscaling_policy(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -640,7 +640,7 @@ def test_deploy_with_autoscaling_policy(self, version): assert endpoint.autoscaling_policy.target_value == 10 - def test_deploy_default_env(self, version): + def test_deploy_default_env(self, version, mock_responses): # This is the additional check which deploy makes to determine if there are any existing endpoints associated mock_responses.add( "GET", @@ -708,7 +708,7 @@ def test_deploy_default_env(self, version): assert endpoint.environment.name == env_1.name - def test_redeploy_model(self, version): + def test_redeploy_model(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -764,7 +764,7 @@ def test_redeploy_model(self, version): assert endpoint.autoscaling_policy.target_value == 10 - def test_deploy_with_gpu(self, version): + def test_deploy_with_gpu(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -817,7 +817,7 @@ def test_deploy_with_gpu(self, version): ) - def test_deploy_with_model_observability_enabled(self, version): + def test_deploy_with_model_observability_enabled(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -869,7 +869,7 @@ def test_deploy_with_model_observability_enabled(self, version): - def test_deploy_with_more_granular_model_observability_cfg(self, version): + def test_deploy_with_more_granular_model_observability_cfg(self, version, mock_responses): mock_responses.add( "GET", "/v1/environments", @@ -921,7 +921,7 @@ def test_deploy_with_more_granular_model_observability_cfg(self, version): assert endpoint.model_observability == model_observability - def test_undeploy(self, version): + def test_undeploy(self, version, mock_responses): mock_responses.add( "GET", "/v1/models/1/versions/1/endpoint", @@ -953,7 +953,7 @@ def test_undeploy(self, version): assert len(mock_responses.calls) == 2 - def test_undeploy_default_env(self, version): + def test_undeploy_default_env(self, version, mock_responses): # This is the additional check which deploy makes to determine if there are any existing endpoints associated mock_responses.add( "GET", @@ -1020,7 +1020,7 @@ def test_undeploy_default_env(self, version): assert len(mock_responses.calls) == 3 - def test_list_prediction_job(self, version): + def test_list_prediction_job(self, version, mock_responses): mock_responses.add( method="GET", url="/v1/models/1/versions/1/jobs-by-page?page=1", @@ -1064,7 +1064,7 @@ def test_list_prediction_job(self, version): assert jobs[1].error == job_2.error - def test_create_prediction_job(self, version): + def test_create_prediction_job(self, version, mock_responses): job_1.status = "completed" mock_responses.add( "POST", @@ -1121,7 +1121,7 @@ def test_create_prediction_job(self, version): @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - def test_create_prediction_job_with_retry_failed(self, version): + def test_create_prediction_job_with_retry_failed(self, version, mock_responses): job_1.status = "pending" mock_responses.add( "POST", @@ -1171,7 +1171,7 @@ def test_create_prediction_job_with_retry_failed(self, version): @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - def test_create_prediction_job_with_retry_success(self, version): + def test_create_prediction_job_with_retry_success(self, version, mock_responses): job_1.status = "pending" mock_responses.add( "POST", @@ -1274,7 +1274,7 @@ def _find_match_patched(self, request): @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - def test_create_prediction_job_with_retry_pending_then_failed(self, version): + def test_create_prediction_job_with_retry_pending_then_failed(self, version, mock_responses): job_1.status = "pending" mock_responses.add( "POST", @@ -1367,7 +1367,7 @@ def _find_match_patched(self, request): assert len(mock_responses.calls) == 10 - def test_stop_prediction_job(self, version): + def test_stop_prediction_job(self, version, mock_responses): job_1.status = "pending" mock_responses.add( "POST", @@ -1422,7 +1422,7 @@ def test_stop_prediction_job(self, version): assert j.name == job_1.name - def test_model_version_deletion(self, version): + def test_model_version_deletion(self, version, mock_responses): mock_responses.add( "DELETE", "/v1/models/1/versions/1", @@ -1487,7 +1487,7 @@ class TestModel: ) - def test_list_version(self, model): + def test_list_version(self, model, mock_responses): mock_responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=", @@ -1511,7 +1511,7 @@ def test_list_version(self, model): assert versions[1].id == 2 - def test_list_version_with_labels(self, model): + def test_list_version_with_labels(self, model, mock_responses): mock_responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=labels%3Amodel+in+%28T-800%29", @@ -1526,7 +1526,7 @@ def test_list_version_with_labels(self, model): assert versions[0].labels["model"] == "T-800" - def test_list_endpoint(self, model): + def test_list_endpoint(self, model, mock_responses): mock_responses.add( "GET", "/v1/models/1/endpoints", @@ -1541,7 +1541,7 @@ def test_list_endpoint(self, model): assert endpoints[1].id == mdl_endpoint_2.id - def test_new_model_version(self, model): + def test_new_model_version(self, model, mock_responses): mock_responses.add( "POST", "/v1/models/1/versions", @@ -1560,7 +1560,7 @@ def test_new_model_version(self, model): assert mv._model_schema == self.merlin_model_schema - def test_serve_traffic(self, model): + def test_serve_traffic(self, model, mock_responses): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): model.serve_traffic([ve], environment_name=env_1.name) @@ -1629,7 +1629,7 @@ def test_serve_traffic(self, model): ) - def test_stop_serving_traffic(self, model): + def test_stop_serving_traffic(self, model, mock_responses): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): model.serve_traffic([ve], environment_name=env_1.name) @@ -1694,7 +1694,7 @@ def test_stop_serving_traffic(self, model): assert len(mock_responses.calls) == 2 - def test_serve_traffic_default_env(self, model): + def test_serve_traffic_default_env(self, model, mock_responses): ve = VersionEndpoint(ep1) # no default environment @@ -1776,7 +1776,7 @@ def test_serve_traffic_default_env(self, model): ) - def test_serve_traffic_upi(self, model): + def test_serve_traffic_upi(self, model, mock_responses): ve = VersionEndpoint(upi_ep) # test create mock_responses.add( @@ -1832,7 +1832,7 @@ def test_serve_traffic_upi(self, model): assert endpoint.protocol == Protocol.UPI_V1 - def test_model_deletion(self, model): + def test_model_deletion(self, model, mock_responses): mock_responses.add( "DELETE", "/v1/projects/1/models/1", From 31677106b95011f839d761b57912edcf851c69c0 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 16:46:41 +0700 Subject: [PATCH 36/39] chore: adjust url in mock_responses.add() --- python/sdk/test/client_test.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/python/sdk/test/client_test.py b/python/sdk/test/client_test.py index a03c2be92..50263c728 100644 --- a/python/sdk/test/client_test.py +++ b/python/sdk/test/client_test.py @@ -82,7 +82,7 @@ def serialize_datetime(obj): def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", - "/api/v1/projects", + f"{mock_url}/v1/projects?name=my-project", body=f"""[{{ "id": 0, "name": "my-project", @@ -98,7 +98,7 @@ def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): p = m.get_project("my-project") assert mock_responses.calls[-1].request.method == "GET" - assert mock_responses.calls[-1].request.url == "/api/v1/projects?name=my-project" + assert mock_responses.calls[-1].request.url == f"{mock_url}/v1/projects?name=my-project" assert mock_responses.calls[-1].request.host == "merlin.dev" assert p.id == 0 @@ -131,14 +131,14 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_r mock_responses.add( "GET", - f"/api/v1/projects/{project_id}/models", + f"{mock_url}/v1/projects/{project_id}/models", body="[]", status=200, content_type="application/json", ) mock_responses.add( "POST", - f"/api/v1/projects/{project_id}/models", + f"{mock_url}/v1/projects/{project_id}/models", body=f"""{{ "id": 0, "project_id": {project_id}, @@ -216,7 +216,7 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_resp mock_responses.add( "GET", - f"/api/v1/projects/{project_id}/models", + f"{mock_url}/v1/projects/{project_id}/models", body=f"""[{{ "id": 1, "project_id": {project_id}, @@ -234,7 +234,7 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_resp mock_responses.add( "GET", - f"/api/v1/models/1/endpoints", + f"{mock_url}/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()], default=serialize_datetime), status=200, content_type="application/json", @@ -284,7 +284,7 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): mock_responses.add( "POST", - f"/api/v1/models/{model_id}/versions", + f"{mock_url}/v1/models/{model_id}/versions", body=f"""{{ "id": {version_id}, "model_id": {model_id}, @@ -338,7 +338,7 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): mock_responses.add( "GET", - "/api/v1/environments", + f"{mock_url}/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -358,7 +358,7 @@ def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", - "/api/v1/environments", + f"{mock_url}/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -378,7 +378,7 @@ def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth, moc def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): mock_responses.add( "GET", - "/api/v1/environments", + f"{mock_url}/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -394,7 +394,7 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa mock_responses.add( "GET", - "/api/v1/environments", + f"{mock_url}/v1/environments", body=json.dumps([env_2.to_dict()]), status=200, content_type="application/json", @@ -408,7 +408,7 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa client = MerlinClient(mock_url, use_google_oauth=use_google_oauth) mock_responses.add( "GET", - "/api/v1/environments", + f"{mock_url}/v1/environments", body=json.dumps([env_2.to_dict()]), status=200, content_type="application/json", From b8344266d57508d127f83627a5001803eaba9c8b Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Wed, 16 Apr 2025 17:14:57 +0700 Subject: [PATCH 37/39] chore: revert all changes related to unittest --- python/sdk/Pipfile | 1 - python/sdk/requirements_test.txt | 3 +- python/sdk/test/client_test.py | 86 ++++--- python/sdk/test/conftest.py | 38 +-- python/sdk/test/merlin_test.py | 76 +++--- python/sdk/test/model_test.py | 409 ++++++++++++++++--------------- 6 files changed, 323 insertions(+), 290 deletions(-) diff --git a/python/sdk/Pipfile b/python/sdk/Pipfile index 236b4f71f..10a90835f 100644 --- a/python/sdk/Pipfile +++ b/python/sdk/Pipfile @@ -12,4 +12,3 @@ pytest-xdist = "*" [packages] merlin-sdk = {extras = ["test"],path = "."} cookiecutter = '==2.1.1' -responses = ">=0.25,<1.0" diff --git a/python/sdk/requirements_test.txt b/python/sdk/requirements_test.txt index dda3e2045..7121ac75c 100644 --- a/python/sdk/requirements_test.txt +++ b/python/sdk/requirements_test.txt @@ -14,4 +14,5 @@ types-PyYAML types-six types-protobuf xarray -xgboost==1.6.2 \ No newline at end of file +xgboost==1.6.2 +urllib3-mock>=0.3.3 \ No newline at end of file diff --git a/python/sdk/test/client_test.py b/python/sdk/test/client_test.py index 50263c728..2a92b261c 100644 --- a/python/sdk/test/client_test.py +++ b/python/sdk/test/client_test.py @@ -18,7 +18,6 @@ from unittest import mock import pytest -import responses import client as cl from client import ApiClient, Configuration @@ -28,6 +27,9 @@ from merlin.util import guess_mlp_ui_url from merlin.version import VERSION +# get global mock responses that configured in conftest +responses = pytest.responses + @pytest.fixture def mock_url(): @@ -79,10 +81,11 @@ def serialize_datetime(obj): return obj.isoformat() raise TypeError("Type is not serializable") -def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): - mock_responses.add( +@responses.activate +def test_get_project(mock_url, mock_oauth, use_google_oauth): + responses.add( "GET", - f"{mock_url}/v1/projects?name=my-project", + "/api/v1/projects", body=f"""[{{ "id": 0, "name": "my-project", @@ -97,9 +100,9 @@ def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): m = MerlinClient(mock_url, use_google_oauth=use_google_oauth) p = m.get_project("my-project") - assert mock_responses.calls[-1].request.method == "GET" - assert mock_responses.calls[-1].request.url == f"{mock_url}/v1/projects?name=my-project" - assert mock_responses.calls[-1].request.host == "merlin.dev" + assert responses.calls[-1].request.method == "GET" + assert responses.calls[-1].request.url == "/api/v1/projects?name=my-project" + assert responses.calls[-1].request.host == "merlin.dev" assert p.id == 0 assert p.name == "my-project" @@ -109,6 +112,7 @@ def test_get_project(mock_url, mock_oauth, use_google_oauth, mock_responses): assert isinstance(p.updated_at, datetime.datetime) +@responses.activate def test_create_invalid_project_name( mock_url, api_client, mock_oauth, use_google_oauth ): @@ -121,7 +125,8 @@ def test_create_invalid_project_name( assert client.get_project(project_name) -def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1010 mlflow_experiment_id = 1 model_name = "my-model" @@ -129,16 +134,16 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_r model_type = ModelType.XGBOOST mlflow_url = "http://mlflow.api.merlin.dev" - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/projects/{project_id}/models", + f"/api/v1/projects/{project_id}/models", body="[]", status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", - f"{mock_url}/v1/projects/{project_id}/models", + f"/api/v1/projects/{project_id}/models", body=f"""{{ "id": 0, "project_id": {project_id}, @@ -168,7 +173,7 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_r "my-model", project_name=project_name, model_type=model_type ) - assert json.loads(mock_responses.calls[-1].request.body) == json.loads( + assert json.loads(responses.calls[-1].request.body) == json.loads( f""" {{ "name" : "{model_name}", @@ -186,14 +191,15 @@ def test_create_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_r assert isinstance(model.updated_at, datetime.datetime) assert model.project == project assert ( - f"merlin-sdk/{VERSION}" in mock_responses.calls[-1].request.headers["User-Agent"] + f"merlin-sdk/{VERSION}" in responses.calls[-1].request.headers["User-Agent"] ) assert ( f"python/{version_info.major}.{version_info.minor}.{version_info.micro}" - in mock_responses.calls[-1].request.headers["User-Agent"] + in responses.calls[-1].request.headers["User-Agent"] ) +@responses.activate def test_create_invalid_model_name(mock_url, api_client, mock_oauth, use_google_oauth): model_name = "invalidModelName" project_name = "my-project" @@ -206,7 +212,8 @@ def test_create_invalid_model_name(mock_url, api_client, mock_oauth, use_google_ assert client.get_or_create_model(model_name, project_name, model_type) -def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1010 mlflow_experiment_id = 1 model_name = "my-model" @@ -214,9 +221,9 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_resp model_type = ModelType.XGBOOST mlflow_url = "http://mlflow.api.merlin.dev" - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/projects/{project_id}/models", + f"/api/v1/projects/{project_id}/models", body=f"""[{{ "id": 1, "project_id": {project_id}, @@ -232,9 +239,9 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_resp content_type="application/json", ) - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/models/1/endpoints", + f"/api/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()], default=serialize_datetime), status=200, content_type="application/json", @@ -268,6 +275,7 @@ def test_get_model(mock_url, api_client, mock_oauth, use_google_oauth, mock_resp assert default_model_endpoint.environment_name == env_1.name +@responses.activate def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): project_id = 1 model_id = 1 @@ -282,9 +290,9 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): created_at = "2019-09-04T03:09:13.842Z" updated_at = "2019-09-04T03:09:13.843Z" - mock_responses.add( + responses.add( "POST", - f"{mock_url}/v1/models/{model_id}/versions", + f"/api/v1/models/{model_id}/versions", body=f"""{{ "id": {version_id}, "model_id": {model_id}, @@ -335,10 +343,11 @@ def test_new_model_version(mock_url, api_client, mock_oauth, use_google_oauth): assert mv.url == f"{ui_url}/projects/1/models/{model_id}/versions" +@responses.activate def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/environments", + "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -355,10 +364,11 @@ def test_list_environments(mock_url, api_client, mock_oauth, use_google_oauth): assert envs[1].is_default == env_2.is_default -def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): - mock_responses.add( +@responses.activate +def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth): + responses.add( "GET", - f"{mock_url}/v1/environments", + "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -375,10 +385,11 @@ def test_get_environment(mock_url, api_client, mock_oauth, use_google_oauth, moc assert env is None -def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): - mock_responses.add( +@responses.activate +def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): + responses.add( "GET", - f"{mock_url}/v1/environments", + "/api/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", @@ -390,11 +401,11 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa assert env.cluster == env_1.cluster assert env.is_default == env_1.is_default - mock_responses.reset() + responses.reset() - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/environments", + "/api/v1/environments", body=json.dumps([env_2.to_dict()]), status=200, content_type="application/json", @@ -404,15 +415,16 @@ def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oa assert env is None -def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_get_default_environment(mock_url, api_client, mock_oauth, use_google_oauth): client = MerlinClient(mock_url, use_google_oauth=use_google_oauth) - mock_responses.add( + responses.add( "GET", - f"{mock_url}/v1/environments", + "/api/v1/environments", body=json.dumps([env_2.to_dict()]), status=200, content_type="application/json", ) env = client.get_default_environment() - assert env is None + assert env is None \ No newline at end of file diff --git a/python/sdk/test/conftest.py b/python/sdk/test/conftest.py index 0fb325d36..b1c6f99a5 100644 --- a/python/sdk/test/conftest.py +++ b/python/sdk/test/conftest.py @@ -19,18 +19,22 @@ import uuid import requests as requests_lib from requests.adapters import HTTPAdapter -import pytest -import responses -from urllib3.util.retry import Retry +from requests.packages.urllib3.util.retry import Retry +from urllib3_mock import Responses import client as cl from client import ApiClient, Configuration from merlin.model import Model, ModelType, ModelVersion, Project -@pytest.fixture -def mock_responses(): - with responses.RequestsMock() as rsps: - yield rsps + +# From the documentation (https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_configure): +# Allow plugins and conftest files to perform initial configuration. +# This hook is called for every plugin and initial conftest file after command line options have been parsed. +# After that, the hook is called for other conftest files as they are imported. +def pytest_configure(): + # share mock responses as global variable so it can be reused and called as decorator on other files. + pytest.responses = Responses("requests.packages.urllib3") + @pytest.fixture def url(): @@ -154,8 +158,10 @@ def gpu_config(): @pytest.fixture -def mock_oauth(mock_responses): - mock_responses.add( +def mock_oauth(): + responses = pytest.responses + + responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/", body=""" @@ -169,7 +175,7 @@ def mock_oauth(mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/computeengine@google.com/token", body=""" @@ -185,7 +191,7 @@ def mock_oauth(mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/?recursive=true", body=""" @@ -199,7 +205,7 @@ def mock_oauth(mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/computeMetadata/v1/instance/service-accounts/default/identity?audience=sdk.caraml&format=full", body="""eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg""", @@ -208,7 +214,7 @@ def mock_oauth(mock_responses): match_querystring=True, ) - mock_responses.add( + responses.add( "GET", "/computeMetadata/api/v1/instance/service-accounts/default/", body=""" @@ -222,7 +228,7 @@ def mock_oauth(mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/computeMetadata/api/v1/instance/service-accounts/computeengine@google.com/token", body=""" @@ -238,7 +244,7 @@ def mock_oauth(mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/token", body=""" @@ -252,4 +258,4 @@ def mock_oauth(mock_responses): """, status=200, content_type="application/json", - ) + ) \ No newline at end of file diff --git a/python/sdk/test/merlin_test.py b/python/sdk/test/merlin_test.py index a0e92ac7d..c93f94c6f 100644 --- a/python/sdk/test/merlin_test.py +++ b/python/sdk/test/merlin_test.py @@ -16,12 +16,14 @@ import mlflow import pytest -import responses import client as cl import merlin from merlin.model import ModelVersion +# get global mock responses that configured in conftest +responses = pytest.responses + default_resource_request = cl.ResourceRequest(min_replica=1, max_replica=1, cpu_request="100m", memory_request="128Mi") env_1 = cl.Environment( id=1, @@ -44,12 +46,13 @@ def test_set_url(url, use_google_oauth): assert url == merlin.get_url() -def test_set_project(url, project, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_set_project(url, project, mock_oauth, use_google_oauth): # expect exception when setting project but client is not set with pytest.raises(Exception): merlin.set_project(project.name) - _mock_get_project_call(project, mock_responses) + _mock_get_project_call(project) merlin.set_url(url, use_google_oauth=use_google_oauth) merlin.set_project(project.name) @@ -59,7 +62,8 @@ def test_set_project(url, project, mock_oauth, use_google_oauth, mock_responses) assert merlin.active_project().mlflow_tracking_url == project.mlflow_tracking_url -def test_set_model(url, project, model, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_set_model(url, project, model, mock_oauth, use_google_oauth): # expect exception when setting model but client and project is not set with pytest.raises(Exception): merlin.set_model(model.name, model.type) @@ -69,10 +73,10 @@ def test_set_model(url, project, model, mock_oauth, use_google_oauth, mock_respo with pytest.raises(Exception): merlin.set_model(model.name, model.type) - _mock_get_project_call(project, mock_responses) + _mock_get_project_call(project) merlin.set_project(project.name) - _mock_get_model_call(project, model, mock_responses) + _mock_get_model_call(project, model) merlin.set_model(model.name, model.type) assert merlin.active_model().name == model.name @@ -81,7 +85,8 @@ def test_set_model(url, project, model, mock_oauth, use_google_oauth, mock_respo assert merlin.active_model().mlflow_experiment_id == model.mlflow_experiment_id -def test_new_model_version(url, project, model, version, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_new_model_version(url, project, model, version, mock_oauth, use_google_oauth): # expect exception when creating new model version but client and # project is not set with pytest.raises(Exception): @@ -94,17 +99,17 @@ def test_new_model_version(url, project, model, version, mock_oauth, use_google_ with merlin.new_model_version() as v: print(v) - _mock_get_project_call(project, mock_responses) + _mock_get_project_call(project) merlin.set_project(project.name) with pytest.raises(Exception): with merlin.new_model_version() as v: print(v) - _mock_get_model_call(project, model, mock_responses) + _mock_get_model_call(project, model) merlin.set_model(model.name, model.type) - _mock_new_model_version_call(model, version, mock_responses) + _mock_new_model_version_call(model, version) with merlin.new_model_version() as v: assert v is not None assert isinstance(v, ModelVersion) @@ -112,18 +117,19 @@ def test_new_model_version(url, project, model, version, mock_oauth, use_google_ assert v.mlflow_run_id == version.mlflow_run_id +@responses.activate def test_new_model_version_with_labels( - url, project, model, version, mock_oauth, use_google_oauth, mock_responses + url, project, model, version, mock_oauth, use_google_oauth ): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_get_project_call(project, mock_responses) + _mock_get_project_call(project) merlin.set_project(project.name) - _mock_get_model_call(project, model, mock_responses) + _mock_get_model_call(project, model) merlin.set_model(model.name, model.type) # Insert labels labels = {"model": "T-800", "software": "skynet"} - _mock_new_model_version_call(model, version, mock_responses, labels) + _mock_new_model_version_call(model, version, labels) with merlin.new_model_version(labels=labels) as v: assert v is not None @@ -134,10 +140,11 @@ def test_new_model_version_with_labels( assert labels[key] == value -def test_list_environment(url, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_list_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call(mock_responses) + _mock_list_environment_call() envs = merlin.list_environment() @@ -146,10 +153,11 @@ def test_list_environment(url, mock_oauth, use_google_oauth, mock_responses): assert envs[1].name == env_2.name -def test_get_environment(url, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_get_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call(mock_responses) + _mock_list_environment_call() env = merlin.get_environment(env_1.name) assert env is not None @@ -159,10 +167,11 @@ def test_get_environment(url, mock_oauth, use_google_oauth, mock_responses): assert env is None -def test_get_default_environment(url, mock_oauth, use_google_oauth, mock_responses): +@responses.activate +def test_get_default_environment(url, mock_oauth, use_google_oauth): merlin.set_url(url, use_google_oauth=use_google_oauth) - _mock_list_environment_call(mock_responses) + _mock_list_environment_call() env = merlin.get_default_environment() @@ -171,10 +180,11 @@ def test_get_default_environment(url, mock_oauth, use_google_oauth, mock_respons assert env.is_default -def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oauth, mock_responses): - _mock_get_project_call(project, mock_responses) - _mock_get_model_call(project, model, mock_responses) - _mock_new_model_version_call(model, version, mock_responses) +@responses.activate +def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oauth): + _mock_get_project_call(project) + _mock_get_model_call(project, model) + _mock_new_model_version_call(model, version) merlin.set_url(url, use_google_oauth=use_google_oauth) merlin.set_project(project.name) @@ -191,8 +201,8 @@ def test_mlflow_methods(url, project, model, version, mock_oauth, use_google_oau assert run.data.tags["tag"] == "value" -def _mock_get_project_call(project, mock_responses): - mock_responses.add( +def _mock_get_project_call(project): + responses.add( "GET", "/v1/projects", body=f"""[{{ @@ -207,8 +217,8 @@ def _mock_get_project_call(project, mock_responses): ) -def _mock_get_model_call(project, model, mock_responses): - mock_responses.add( +def _mock_get_model_call(project, model): + responses.add( "GET", f"/v1/projects/{project.id}/models", body=f"""[{{ @@ -225,7 +235,7 @@ def _mock_get_model_call(project, model, mock_responses): ) -def _mock_new_model_version_call(model, version, mock_responses, labels=None): +def _mock_new_model_version_call(model, version, labels=None): body = { "id": version.id, "mlflow_run_id": version.mlflow_run_id, @@ -237,7 +247,7 @@ def _mock_new_model_version_call(model, version, mock_responses, labels=None): if labels is not None: body["labels"] = labels - mock_responses.add( + responses.add( "POST", f"/v1/models/{model.id}/versions", body=json.dumps(body), @@ -246,11 +256,11 @@ def _mock_new_model_version_call(model, version, mock_responses, labels=None): ) -def _mock_list_environment_call(mock_responses): - mock_responses.add( +def _mock_list_environment_call(): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", - ) + ) \ No newline at end of file diff --git a/python/sdk/test/model_test.py b/python/sdk/test/model_test.py index aa4b028e7..1fd19031e 100644 --- a/python/sdk/test/model_test.py +++ b/python/sdk/test/model_test.py @@ -20,7 +20,6 @@ import client import client as cl import pytest -import responses from merlin import AutoscalingPolicy, DeploymentMode, MetricsType from merlin.autoscaling import ( RAW_DEPLOYMENT_DEFAULT_AUTOSCALING_POLICY, @@ -35,6 +34,9 @@ from merlin.model_schema import InferenceSchema, ModelSchema, RankingOutput, ValueType from merlin.protocol import Protocol from merlin.model_observability import ModelObservability +from urllib3_mock import Responses + +responses = Responses("requests.packages.urllib3") default_resource_request = cl.ResourceRequest( min_replica=1, max_replica=1, cpu_request="100m", memory_request="128Mi" @@ -316,8 +318,9 @@ class TestProject: secret_1 = cl.Secret(id=1, name="secret-1", data="secret-data-1") secret_2 = cl.Secret(id=2, name="secret-2", data="secret-data-2") - def test_create_secret(self, project, mock_responses): - mock_responses.add( + @responses.activate + def test_create_secret(self, project): + responses.add( "POST", "/v1/projects/1/secrets", body=json.dumps(self.secret_1.to_dict()), @@ -326,19 +329,20 @@ def test_create_secret(self, project, mock_responses): ) project.create_secret(self.secret_1.name, self.secret_1.data) - actual_body = json.loads(mock_responses.calls[0].request.body) + actual_body = json.loads(responses.calls[0].request.body) assert actual_body["name"] == self.secret_1.name assert actual_body["data"] == self.secret_1.data - def test_update_secret(self, project, mock_responses): - mock_responses.add( + @responses.activate + def test_update_secret(self, project): + responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "PATCH", "/v1/projects/1/secrets/1", body=json.dumps(self.secret_1.to_dict()), @@ -348,14 +352,14 @@ def test_update_secret(self, project, mock_responses): project.update_secret(self.secret_1.name, "new-data") - actual_body = json.loads(mock_responses.calls[1].request.body) + actual_body = json.loads(responses.calls[1].request.body) assert actual_body["name"] == self.secret_1.name assert actual_body["data"] == "new-data" - mock_responses.reset() + responses.reset() # test secret not found - mock_responses.add( + responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict()]), @@ -369,15 +373,16 @@ def test_update_secret(self, project, mock_responses): ): project.update_secret(self.secret_2.name, "new-data") - def test_delete_secret(self, project, mock_responses): - mock_responses.add( + @responses.activate + def test_delete_secret(self, project): + responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "DELETE", "/v1/projects/1/secrets/1", status=204, @@ -386,10 +391,10 @@ def test_delete_secret(self, project, mock_responses): project.delete_secret(self.secret_1.name) - mock_responses.reset() + responses.reset() # test secret not found - mock_responses.add( + responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict()]), @@ -403,9 +408,9 @@ def test_delete_secret(self, project, mock_responses): ): project.delete_secret(self.secret_2.name) - - def test_list_secret(self, project, mock_responses): - mock_responses.add( + @responses.activate + def test_list_secret(self, project): + responses.add( "GET", "/v1/projects/1/secrets", body=json.dumps([self.secret_1.to_dict(), self.secret_2.to_dict()]), @@ -418,9 +423,9 @@ def test_list_secret(self, project, mock_responses): class TestModelVersion: - - def test_list_endpoint(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_list_endpoint(self, version): + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), @@ -433,9 +438,9 @@ def test_list_endpoint(self, version, mock_responses): assert endpoints[0].id == ep1.id assert endpoints[1].id == ep2.id - - def test_deploy(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -443,28 +448,28 @@ def test_deploy(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep1.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict()]), @@ -483,9 +488,9 @@ def test_deploy(self, version, mock_responses): assert endpoint.autoscaling_policy == SERVERLESS_DEFAULT_AUTOSCALING_POLICY assert endpoint.protocol == Protocol.HTTP_JSON - - def test_deploy_upiv1(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_upiv1(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -493,28 +498,28 @@ def test_deploy_upiv1(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(upi_ep.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(upi_ep.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([upi_ep.to_dict()]), @@ -533,9 +538,9 @@ def test_deploy_upiv1(self, version, mock_responses): assert endpoint.autoscaling_policy == SERVERLESS_DEFAULT_AUTOSCALING_POLICY assert endpoint.protocol == Protocol.UPI_V1 - - def test_deploy_using_raw_deployment_mode(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_using_raw_deployment_mode(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -543,28 +548,28 @@ def test_deploy_using_raw_deployment_mode(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep3.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep3.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep3.to_dict()]), @@ -584,9 +589,9 @@ def test_deploy_using_raw_deployment_mode(self, version, mock_responses): assert endpoint.deployment_mode == DeploymentMode.RAW_DEPLOYMENT assert endpoint.autoscaling_policy == RAW_DEPLOYMENT_DEFAULT_AUTOSCALING_POLICY - - def test_deploy_with_autoscaling_policy(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_with_autoscaling_policy(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -594,28 +599,28 @@ def test_deploy_with_autoscaling_policy(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep4.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep4.to_dict()]), @@ -639,10 +644,10 @@ def test_deploy_with_autoscaling_policy(self, version, mock_responses): assert endpoint.autoscaling_policy.metrics_type == MetricsType.CPU_UTILIZATION assert endpoint.autoscaling_policy.target_value == 10 - - def test_deploy_default_env(self, version, mock_responses): + @responses.activate + def test_deploy_default_env(self, version): # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), @@ -650,7 +655,7 @@ def test_deploy_default_env(self, version, mock_responses): content_type="application/json", ) # no default environment - mock_responses.add( + responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -661,8 +666,8 @@ def test_deploy_default_env(self, version, mock_responses): version.deploy() # default environment exists - mock_responses.reset() - mock_responses.add( + responses.reset() + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -670,28 +675,28 @@ def test_deploy_default_env(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep1.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict()]), @@ -707,9 +712,9 @@ def test_deploy_default_env(self, version, mock_responses): assert endpoint.environment.cluster == env_1.cluster assert endpoint.environment.name == env_1.name - - def test_redeploy_model(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_redeploy_model(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -717,28 +722,28 @@ def test_redeploy_model(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep3.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "PUT", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep4.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep4.to_dict()]), @@ -763,9 +768,9 @@ def test_redeploy_model(self, version, mock_responses): assert endpoint.autoscaling_policy.metrics_type == MetricsType.CPU_UTILIZATION assert endpoint.autoscaling_policy.target_value == 10 - - def test_deploy_with_gpu(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_with_gpu(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -773,28 +778,28 @@ def test_deploy_with_gpu(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(ep5.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/789", body=json.dumps(ep5.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep5.to_dict()]), @@ -816,9 +821,9 @@ def test_deploy_with_gpu(self, version, mock_responses): == resource_request_with_gpu.gpu_request ) - - def test_deploy_with_model_observability_enabled(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_with_model_observability_enabled(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -826,28 +831,28 @@ def test_deploy_with_model_observability_enabled(self, version, mock_responses): content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(observability_enabled_ep.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([observability_enabled_ep.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/7899", body=json.dumps(observability_enabled_ep.to_dict()), @@ -868,9 +873,9 @@ def test_deploy_with_model_observability_enabled(self, version, mock_responses): assert endpoint.enable_model_observability == True - - def test_deploy_with_more_granular_model_observability_cfg(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_deploy_with_more_granular_model_observability_cfg(self, version): + responses.add( "GET", "/v1/environments", body=json.dumps([env_3.to_dict()]), @@ -878,28 +883,28 @@ def test_deploy_with_more_granular_model_observability_cfg(self, version, mock_r content_type="application/json", ) # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/endpoint", body=json.dumps(more_granular_observability_cfg_ep.to_dict()), status=201, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([more_granular_observability_cfg_ep.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint/8000", body=json.dumps(more_granular_observability_cfg_ep.to_dict()), @@ -920,9 +925,9 @@ def test_deploy_with_more_granular_model_observability_cfg(self, version, mock_r assert endpoint.deployment_mode == DeploymentMode.SERVERLESS assert endpoint.model_observability == model_observability - - def test_undeploy(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_undeploy(self, version): + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep2.to_dict()]), @@ -931,17 +936,17 @@ def test_undeploy(self, version, mock_responses): ) version.undeploy(environment_name=env_1.name) - assert len(mock_responses.calls) == 1 + assert len(responses.calls) == 1 - mock_responses.reset() - mock_responses.add( + responses.reset() + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "DELETE", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), @@ -950,12 +955,12 @@ def test_undeploy(self, version, mock_responses): ) version.undeploy(environment_name=env_1.name) - assert len(mock_responses.calls) == 2 + assert len(responses.calls) == 2 - - def test_undeploy_default_env(self, version, mock_responses): + @responses.activate + def test_undeploy_default_env(self, version): # This is the additional check which deploy makes to determine if there are any existing endpoints associated - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([]), @@ -963,7 +968,7 @@ def test_undeploy_default_env(self, version, mock_responses): content_type="application/json", ) # no default environment - mock_responses.add( + responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -973,8 +978,8 @@ def test_undeploy_default_env(self, version, mock_responses): with pytest.raises(ValueError): version.deploy() - mock_responses.reset() - mock_responses.add( + responses.reset() + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), @@ -982,7 +987,7 @@ def test_undeploy_default_env(self, version, mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep2.to_dict()]), @@ -991,24 +996,24 @@ def test_undeploy_default_env(self, version, mock_responses): ) version.undeploy() - assert len(mock_responses.calls) == 2 + assert len(responses.calls) == 2 - mock_responses.reset() - mock_responses.add( + responses.reset() + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/endpoint", body=json.dumps([ep1.to_dict(), ep2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "DELETE", "/v1/models/1/versions/1/endpoint/1234", body=json.dumps(ep1.to_dict()), @@ -1017,11 +1022,11 @@ def test_undeploy_default_env(self, version, mock_responses): ) version.undeploy() - assert len(mock_responses.calls) == 3 + assert len(responses.calls) == 3 - - def test_list_prediction_job(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_list_prediction_job(self, version): + responses.add( method="GET", url="/v1/models/1/versions/1/jobs-by-page?page=1", body=json.dumps({ @@ -1036,7 +1041,7 @@ def test_list_prediction_job(self, version, mock_responses): content_type="application/json", match_querystring=True, ) - mock_responses.add( + responses.add( method="GET", url="/v1/models/1/versions/1/jobs-by-page?page=2", body=json.dumps({ @@ -1063,10 +1068,10 @@ def test_list_prediction_job(self, version, mock_responses): assert jobs[1].status == JobStatus(job_2.status) assert jobs[1].error == job_2.error - - def test_create_prediction_job(self, version, mock_responses): + @responses.activate + def test_create_prediction_job(self, version): job_1.status = "completed" - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1101,7 +1106,7 @@ def test_create_prediction_job(self, version, mock_responses): assert j.error == job_1.error assert j.name == job_1.name - actual_req = json.loads(mock_responses.calls[0].request.body) + actual_req = json.loads(responses.calls[0].request.body) assert actual_req["config"]["job_config"]["bigquery_source"] == bq_src.to_dict() assert actual_req["config"]["job_config"]["bigquery_sink"] == bq_sink.to_dict() assert ( @@ -1120,10 +1125,10 @@ def test_create_prediction_job(self, version, mock_responses): @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - - def test_create_prediction_job_with_retry_failed(self, version, mock_responses): + @responses.activate + def test_create_prediction_job_with_retry_failed(self, version): job_1.status = "pending" - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1132,7 +1137,7 @@ def test_create_prediction_job_with_retry_failed(self, version, mock_responses): ) for i in range(5): - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1166,14 +1171,14 @@ def test_create_prediction_job_with_retry_failed(self, version, mock_responses): assert j.id == job_1.id assert j.error == job_1.error assert j.name == job_1.name - assert len(mock_responses.calls) == 6 + assert len(responses.calls) == 6 @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - - def test_create_prediction_job_with_retry_success(self, version, mock_responses): + @responses.activate + def test_create_prediction_job_with_retry_success(self, version): job_1.status = "pending" - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1203,10 +1208,10 @@ def _find_match_patched(self, request): else: return match - mock_responses._find_match = types.MethodType(_find_match_patched, responses) + responses._find_match = types.MethodType(_find_match_patched, responses) for i in range(4): - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1215,7 +1220,7 @@ def _find_match_patched(self, request): ) job_1.status = "completed" - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1250,7 +1255,7 @@ def _find_match_patched(self, request): assert j.error == job_1.error assert j.name == job_1.name - actual_req = json.loads(mock_responses.calls[0].request.body) + actual_req = json.loads(responses.calls[0].request.body) assert actual_req["config"]["job_config"]["bigquery_source"] == bq_src.to_dict() assert actual_req["config"]["job_config"]["bigquery_sink"] == bq_sink.to_dict() assert ( @@ -1266,17 +1271,17 @@ def _find_match_patched(self, request): == ModelType.PYFUNC_V2.value.upper() ) assert actual_req["config"]["service_account_name"] == "my-service-account" - assert len(mock_responses.calls) == 6 + assert len(responses.calls) == 6 # unpatch - mock_responses._find_match = types.MethodType(_find_match, responses) + responses._find_match = types.MethodType(_find_match, responses) @patch("merlin.model.DEFAULT_PREDICTION_JOB_DELAY", 0) @patch("merlin.model.DEFAULT_PREDICTION_JOB_RETRY_DELAY", 0) - - def test_create_prediction_job_with_retry_pending_then_failed(self, version, mock_responses): + @responses.activate + def test_create_prediction_job_with_retry_pending_then_failed(self, version): job_1.status = "pending" - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1306,10 +1311,10 @@ def _find_match_patched(self, request): else: return match - mock_responses._find_match = types.MethodType(_find_match_patched, responses) + responses._find_match = types.MethodType(_find_match_patched, responses) for i in range(3): - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1317,7 +1322,7 @@ def _find_match_patched(self, request): content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1327,7 +1332,7 @@ def _find_match_patched(self, request): job_1.status = "failed" for i in range(5): - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1363,13 +1368,13 @@ def _find_match_patched(self, request): assert j.name == job_1.name # unpatch - mock_responses._find_match = types.MethodType(_find_match, responses) - assert len(mock_responses.calls) == 10 + responses._find_match = types.MethodType(_find_match, responses) + assert len(responses.calls) == 10 - - def test_stop_prediction_job(self, version, mock_responses): + @responses.activate + def test_stop_prediction_job(self, version): job_1.status = "pending" - mock_responses.add( + responses.add( "POST", "/v1/models/1/versions/1/jobs", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1377,7 +1382,7 @@ def test_stop_prediction_job(self, version, mock_responses): content_type="application/json", ) - mock_responses.add( + responses.add( "PUT", "/v1/models/1/versions/1/jobs/1/stop", status=204, @@ -1385,7 +1390,7 @@ def test_stop_prediction_job(self, version, mock_responses): ) job_1.status = "terminated" - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions/1/jobs/1", body=json.dumps(job_1.to_dict(), default=serialize_datetime), @@ -1421,9 +1426,9 @@ def test_stop_prediction_job(self, version, mock_responses): assert j.error == job_1.error assert j.name == job_1.name - - def test_model_version_deletion(self, version, mock_responses): - mock_responses.add( + @responses.activate + def test_model_version_deletion(self, version): + responses.add( "DELETE", "/v1/models/1/versions/1", body=json.dumps(1), @@ -1486,9 +1491,9 @@ class TestModel: model_schema=schema, ) - - def test_list_version(self, model, mock_responses): - mock_responses.add( + @responses.activate + def test_list_version(self, model): + responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=", match_querystring=True, @@ -1497,7 +1502,7 @@ def test_list_version(self, model, mock_responses): adding_headers={"Next-Cursor": "abcdef"}, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=abcdef&search=", match_querystring=True, @@ -1510,9 +1515,9 @@ def test_list_version(self, model, mock_responses): assert versions[0].id == 1 assert versions[1].id == 2 - - def test_list_version_with_labels(self, model, mock_responses): - mock_responses.add( + @responses.activate + def test_list_version_with_labels(self, model): + responses.add( "GET", "/v1/models/1/versions?limit=50&cursor=&search=labels%3Amodel+in+%28T-800%29", body=json.dumps([self.v3.to_dict()]), @@ -1525,9 +1530,9 @@ def test_list_version_with_labels(self, model, mock_responses): assert versions[0].id == 3 assert versions[0].labels["model"] == "T-800" - - def test_list_endpoint(self, model, mock_responses): - mock_responses.add( + @responses.activate + def test_list_endpoint(self, model): + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict(), mdl_endpoint_2.to_dict()]), @@ -1540,9 +1545,9 @@ def test_list_endpoint(self, model, mock_responses): assert endpoints[0].id == mdl_endpoint_1.id assert endpoints[1].id == mdl_endpoint_2.id - - def test_new_model_version(self, model, mock_responses): - mock_responses.add( + @responses.activate + def test_new_model_version(self, model): + responses.add( "POST", "/v1/models/1/versions", body=json.dumps(self.v4.to_dict()), @@ -1559,8 +1564,8 @@ def test_new_model_version(self, model, mock_responses): assert mv._labels == {"model": "T-800"} assert mv._model_schema == self.merlin_model_schema - - def test_serve_traffic(self, model, mock_responses): + @responses.activate + def test_serve_traffic(self, model): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): model.serve_traffic([ve], environment_name=env_1.name) @@ -1577,14 +1582,14 @@ def test_serve_traffic(self, model, mock_responses): ) # test create - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1598,24 +1603,24 @@ def test_serve_traffic(self, model, mock_responses): ) assert endpoint.protocol == Protocol.HTTP_JSON - mock_responses.reset() + responses.reset() # test update - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1628,8 +1633,8 @@ def test_serve_traffic(self, model, mock_responses): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - - def test_stop_serving_traffic(self, model, mock_responses): + @responses.activate + def test_stop_serving_traffic(self, model): ve = VersionEndpoint(ep1) with pytest.raises(ValueError): model.serve_traffic([ve], environment_name=env_1.name) @@ -1646,14 +1651,14 @@ def test_stop_serving_traffic(self, model, mock_responses): ) # test create - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1666,39 +1671,39 @@ def test_stop_serving_traffic(self, model, mock_responses): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - mock_responses.reset() + responses.reset() # test DELETE - mock_responses.reset() - mock_responses.add( + responses.reset() + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "DELETE", "/v1/models/1/endpoints/1", status=200, content_type="application/json", ) model.stop_serving_traffic(endpoint.environment_name) - assert len(mock_responses.calls) == 2 + assert len(responses.calls) == 2 - - def test_serve_traffic_default_env(self, model, mock_responses): + @responses.activate + def test_serve_traffic_default_env(self, model): ve = VersionEndpoint(ep1) # no default environment - mock_responses.add( + responses.add( "GET", "/v1/environments", body=json.dumps([env_2.to_dict()]), @@ -1708,24 +1713,24 @@ def test_serve_traffic_default_env(self, model, mock_responses): with pytest.raises(ValueError): model.serve_traffic({ve: 100}) - mock_responses.reset() + responses.reset() # test create - mock_responses.add( + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1738,31 +1743,31 @@ def test_serve_traffic_default_env(self, model, mock_responses): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - mock_responses.reset() + responses.reset() # test update - mock_responses.add( + responses.add( "GET", "/v1/environments", body=json.dumps([env_1.to_dict(), env_2.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_1.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_1.to_dict()), @@ -1775,18 +1780,18 @@ def test_serve_traffic_default_env(self, model, mock_responses): endpoint.environment_name == env_1.name == mdl_endpoint_1.environment_name ) - - def test_serve_traffic_upi(self, model, mock_responses): + @responses.activate + def test_serve_traffic_upi(self, model): ve = VersionEndpoint(upi_ep) # test create - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "POST", "/v1/models/1/endpoints", body=json.dumps(mdl_endpoint_upi.to_dict()), @@ -1800,24 +1805,24 @@ def test_serve_traffic_upi(self, model, mock_responses): ) assert endpoint.protocol == Protocol.UPI_V1 - mock_responses.reset() + responses.reset() # test update - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints", body=json.dumps([mdl_endpoint_upi.to_dict()]), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "GET", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_upi.to_dict()), status=200, content_type="application/json", ) - mock_responses.add( + responses.add( "PUT", "/v1/models/1/endpoints/1", body=json.dumps(mdl_endpoint_upi.to_dict()), @@ -1831,9 +1836,9 @@ def test_serve_traffic_upi(self, model, mock_responses): ) assert endpoint.protocol == Protocol.UPI_V1 - - def test_model_deletion(self, model, mock_responses): - mock_responses.add( + @responses.activate + def test_model_deletion(self, model): + responses.add( "DELETE", "/v1/projects/1/models/1", body=json.dumps(1), @@ -1842,4 +1847,4 @@ def test_model_deletion(self, model, mock_responses): ) response = model.delete_model() - assert response == 1 + assert response == 1 \ No newline at end of file From 894a3fde0e7706b7301ec59091b20f82931e5162 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Thu, 17 Apr 2025 13:57:25 +0700 Subject: [PATCH 38/39] chore: try to add python 3.13 to matrix --- .github/workflows/merlin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index b6c885dcc..03666f876 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} PIPENV_VENV_IN_PROJECT: true @@ -78,7 +78,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} PIPENV_VENV_IN_PROJECT: true @@ -111,7 +111,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] env: PIPENV_DEFAULT_PYTHON_VERSION: ${{ matrix.python-version }} steps: From 3b618e7bcafca704b7818d4f54513590cc99e6d1 Mon Sep 17 00:00:00 2001 From: Muhammad Naufal Andika Natsir Putra Date: Thu, 17 Apr 2025 14:05:26 +0700 Subject: [PATCH 39/39] chore: install rust to allow building pydantic-core wheel manually from CI --- .github/workflows/merlin.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/merlin.yml b/.github/workflows/merlin.yml index 03666f876..f2d5066b6 100644 --- a/.github/workflows/merlin.yml +++ b/.github/workflows/merlin.yml @@ -54,6 +54,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable - uses: actions/cache@v4 with: path: ~/.cache/pip