diff --git a/.github/workflows/build-vector-serve.yml b/.github/workflows/build-vector-serve.yml index 47e4912..f83972f 100644 --- a/.github/workflows/build-vector-serve.yml +++ b/.github/workflows/build-vector-serve.yml @@ -25,8 +25,25 @@ defaults: working-directory: ./vector-serve/ jobs: + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11.1 + uses: actions/setup-python@v5 + with: + python-version: 3.11.1 + - name: Setup + run: make setup + - name: Init Model Cache + run: make download.models + - name: Lints + run: make check + - name: Tests + run: make test build_and_push: name: Build and push images + needs: tests runs-on: - self-hosted - dind diff --git a/vector-serve/Makefile b/vector-serve/Makefile index 399bcc0..75197be 100644 --- a/vector-serve/Makefile +++ b/vector-serve/Makefile @@ -1,5 +1,5 @@ SOURCE_OBJECTS=app - +POETRY_VERSION:=1.7.1 format: poetry run black ${SOURCE_OBJECTS} @@ -14,3 +14,14 @@ run: download.models run.docker: docker build -t vector-serve . docker run -p 3000:3000 vector-serve + +test: + poetry run pytest + +setup: + curl -sSL https://install.python-poetry.org | POETRY_VERSION=${POETRY_VERSION} python3 - + poetry install + +check: + poetry run ruff check ${SOURCE_OBJECTS} + poetry run black --check ${SOURCE_OBJECTS} diff --git a/vector-serve/app/app.py b/vector-serve/app/app.py index 427d95f..c317e58 100644 --- a/vector-serve/app/app.py +++ b/vector-serve/app/app.py @@ -6,9 +6,10 @@ from app.routes.transform import router as transform_router from app.routes.info import router as info_router +from app.routes.health import router as health_router + from app.models import load_model_cache -import logging logging.basicConfig(level=logging.DEBUG) @@ -20,8 +21,10 @@ allow_methods=["*"], allow_headers=["*"], ) + app.include_router(transform_router) app.include_router(info_router) +app.include_router(health_router) def start_app_handler(app: FastAPI) -> Callable: diff --git a/vector-serve/app/routes/health.py b/vector-serve/app/routes/health.py new file mode 100644 index 0000000..4043c86 --- /dev/null +++ b/vector-serve/app/routes/health.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter +from pydantic import BaseModel +import logging + +router = APIRouter(tags=["health"]) + + +class ReadyResponse(BaseModel): + ready: bool + + +class AliveResponse(BaseModel): + alive: bool + + +@router.get("/ready", response_model=ReadyResponse) +def ready() -> ReadyResponse: + logging.debug("Health check") + return ReadyResponse( + ready=True, + ) + + +@router.get("/alive", response_model=AliveResponse) +def alive() -> AliveResponse: + logging.debug("Health check") + return AliveResponse( + alive=True, + ) diff --git a/vector-serve/poetry.lock b/vector-serve/poetry.lock index 435c4ec..309bfc5 100644 --- a/vector-serve/poetry.lock +++ b/vector-serve/poetry.lock @@ -329,6 +329,27 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "httpcore" +version = "1.0.4" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] + [[package]] name = "httptools" version = "0.6.1" @@ -377,6 +398,30 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "huggingface-hub" version = "0.17.3" @@ -421,6 +466,17 @@ files = [ {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jinja2" version = "3.1.2" @@ -868,6 +924,21 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pydantic" version = "2.4.2" @@ -1005,6 +1076,28 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pytest" +version = "8.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dotenv" version = "1.0.0" @@ -2155,4 +2248,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "586659770475a699e281ed9cdb212e9bbeae7cebfac8979db21ed24ead72957f" +content-hash = "4d1cbc5c4e85f02ae66f3a242f534a00d72f4982ceabf35e67daffdb601f0692" diff --git a/vector-serve/pyproject.toml b/vector-serve/pyproject.toml index 7b3684c..af767f3 100644 --- a/vector-serve/pyproject.toml +++ b/vector-serve/pyproject.toml @@ -17,6 +17,8 @@ torch = "^2.1.2" [tool.poetry.group.dev.dependencies] ruff = "^0.1.13" black = "*" +pytest = "^8.0.2" +httpx = "^0.27.0" [build-system] requires = ["poetry-core"] diff --git a/vector-serve/tests/conftest.py b/vector-serve/tests/conftest.py new file mode 100644 index 0000000..f0a3600 --- /dev/null +++ b/vector-serve/tests/conftest.py @@ -0,0 +1,9 @@ +import pytest +from starlette.testclient import TestClient + +from app.app import app + +@pytest.fixture() +def test_client(): + with TestClient(app) as test_client: + yield test_client diff --git a/vector-serve/tests/test_endpoints.py b/vector-serve/tests/test_endpoints.py new file mode 100644 index 0000000..4b0c33e --- /dev/null +++ b/vector-serve/tests/test_endpoints.py @@ -0,0 +1,16 @@ +from fastapi.testclient import TestClient +from fastapi import FastAPI + +def test_ready_endpoint(test_client): + response = test_client.get("/ready") + assert response.status_code == 200 + assert response.json() == {"ready": True} + +def test_alive_endpoint(test_client): + response = test_client.get("/alive") + assert response.status_code == 200 + assert response.json() == {"alive": True} + +def test_model_info(test_client): + response = test_client.get("/v1/info", params={"model_name": "all-MiniLM-L12-v2"}) + assert response.status_code == 200