-
Notifications
You must be signed in to change notification settings - Fork 10
Refactor Upload Endpoint, Add Validation, and Ensure PVC/MariaDB Parity #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
NeriCarcasci
wants to merge
14
commits into
trustyai-explainability:main
Choose a base branch
from
NeriCarcasci:ncarcasc/data-upload-and-storage_refactor
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 13 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
34e127b
:construction: working on the /data/download and /data/upload endpoints
m-misiura bd06ee1
Merge
RobGeada cb24c43
Refactor endpoints based on storage interface, add maria testing
RobGeada 73f38ab
merge main
RobGeada b689e8e
Fix typo in pyproject
RobGeada 5ec2c50
Address review items
RobGeada 737b7a4
Clean unused imports
RobGeada 9393d36
Fix failing tests
RobGeada bf371d1
Fix failing migration test
RobGeada ed2e46a
Rebase main
RobGeada 60909a6
Fix fastapi utils bug
RobGeada 6529324
Refactor data upload/storage: unify PVC and Maria backends, async int…
NeriCarcasci 24e2c0c
added bash script for e2e testing of upload & storage endpoint
NeriCarcasci fd04a81
Merge branch 'main' into ncarcasc/data-upload-and-storage_refactor
NeriCarcasci File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| #!/usr/bin/env bash | ||
| # scripts/test_upload_endpoint.sh | ||
| # | ||
| # KServe-strict endpoint test for /data/upload | ||
| # | ||
| # | ||
| # Usage: | ||
| # ENDPOINT="https://<your-service-route>/data/upload" \ | ||
| # MODEL="gaussian-credit-model" \ | ||
| # TAG="TRAINING" \ | ||
| # ./scripts/test_upload_endpoint.sh | ||
|
|
||
| set -uo pipefail | ||
|
|
||
| # --- Config via env vars (no secrets hardcoded) --- | ||
| : "${ENDPOINT:?ENDPOINT is required, e.g. https://.../data/upload}" | ||
| MODEL="${MODEL:-gaussian-credit-model}" | ||
| # Separate model for BYTES to avoid mixing with an existing numeric dataset | ||
| MODEL_BYTES="${MODEL_BYTES:-${MODEL}-bytes}" | ||
| TAG="${TAG:-TRAINING}" | ||
| AUTH_HEADER="${AUTH_HEADER:-}" # e.g. 'Authorization: Bearer <token>' | ||
|
|
||
| CURL_OPTS=( --silent --show-error -H "Content-Type: application/json" ) | ||
| [[ -n "$AUTH_HEADER" ]] && CURL_OPTS+=( -H "$AUTH_HEADER" ) | ||
|
|
||
| RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; CYAN=$'\033[36m'; RESET=$'\033[0m' | ||
| pass_cnt=0; fail_cnt=0; results=() | ||
| have_jq=1; command -v jq >/dev/null 2>&1 || have_jq=0 | ||
|
|
||
| line(){ printf '%s\n' "--------------------------------------------------------------------------------"; } | ||
| snippet(){ if (( have_jq )); then echo "$1" | jq -r 'tostring' 2>/dev/null | head -c 240; else echo "$1" | head -c 240; fi; } | ||
|
|
||
| # ---------- payload builders ---------- | ||
| mk_inputs_2x4_int32() { | ||
| cat <<JSON | ||
| { | ||
| "inputs": [ | ||
| { | ||
| "name": "credit_inputs", | ||
| "shape": [2, 4], | ||
| "datatype": "INT32", | ||
| "data": [[1,2,3,4],[5,6,7,8]] | ||
| } | ||
| ] | ||
| } | ||
| JSON | ||
| } | ||
|
|
||
| mk_inputs_empty(){ echo '{ "inputs": [] }'; } | ||
|
|
||
| mk_outputs_col(){ # name dtype nested_data_json ; shape [2,1] | ||
| local name="$1" dtype="$2" data="$3" | ||
| cat <<JSON | ||
| [ | ||
| { "name": "$name", "datatype": "$dtype", "shape": [2, 1], "data": $data } | ||
| ] | ||
| JSON | ||
| } | ||
|
|
||
| mk_body(){ # request_json outputs_json -> uses MODEL | ||
| local req="$1" out="$2" | ||
| cat <<JSON | ||
| { | ||
| "model_name": "$MODEL", | ||
| "data_tag": "$TAG", | ||
| "request": $req, | ||
| "response": { "model_name": "test-model", "outputs": $out } | ||
| } | ||
| JSON | ||
| } | ||
|
|
||
| mk_body_for_model(){ # model request_json outputs_json | ||
| local mdl="$1" req="$2" out="$3" | ||
| cat <<JSON | ||
| { | ||
| "model_name": "$mdl", | ||
| "data_tag": "$TAG", | ||
| "request": $req, | ||
| "response": { "model_name": "test-model", "outputs": $out } | ||
| } | ||
| JSON | ||
| } | ||
|
|
||
| sample_data_for_dtype(){ | ||
| case "$1" in | ||
| BOOL) echo '[[1],[0]]' ;; | ||
| INT8|INT16|INT32|INT64|UINT8|UINT16|UINT32|UINT64) echo '[[1],[2]]' ;; | ||
| FP16|FP32|FP64) echo '[[0.1],[0.2]]' ;; | ||
| BYTES) echo '[["a"],["b"]]' ;; | ||
| *) echo '[]' ;; | ||
| esac | ||
| } | ||
|
|
||
| # ---------- runner ---------- | ||
| run_test () { | ||
| local name="$1" want="$2" sub="$3" payload="$4" | ||
| local tmp http body | ||
| tmp="$(mktemp)" | ||
| http=$(curl -X POST "$ENDPOINT" "${CURL_OPTS[@]}" -d "$payload" -o "$tmp" -w "%{http_code}" || true) | ||
| body="$(cat "$tmp")"; rm -f "$tmp" | ||
|
|
||
| local ok=1 | ||
| [[ "$http" == "$want" ]] || ok=0 | ||
| if [[ -n "$sub" ]] && ! echo "$body" | grep -qi -- "$sub"; then ok=0; fi | ||
|
|
||
| if (( ok )); then | ||
| pass_cnt=$((pass_cnt+1)); results+=("PASS|$name|$http|$(snippet "$body")") | ||
| printf "%s[PASS]%s %s (HTTP %s)\n" "$GREEN" "$RESET" "$name" "$http" | ||
| else | ||
| fail_cnt=$((fail_cnt+1)); results+=("FAIL|$name|$http|$(snippet "$body")") | ||
| printf "%s[FAIL]%s %s (HTTP %s)\n" "$RED" "$RESET" "$name" "$http" | ||
| [[ -n "$sub" ]] && printf " expected code=%s and body to contain: %q\n" "$want" "$sub" || true | ||
| printf " body: %s\n" "$(snippet "$body")" | ||
| fi | ||
| } | ||
|
|
||
| # ---------- edge cases ---------- | ||
| line; echo "${CYAN}Running edge cases...${RESET}" | ||
|
|
||
| run_test "valid_int32_bool" "200" '"status":"success"' \ | ||
| "$(mk_body "$(mk_inputs_2x4_int32)" "$(mk_outputs_col predict BOOL '[[1],[0]]')")" | ||
|
|
||
| run_test "missing_model_name" "422" '"Field required"' \ | ||
| "$(cat <<JSON | ||
| { | ||
| "data_tag": "$TAG", | ||
| "request": $(mk_inputs_2x4_int32), | ||
| "response": { "model_name": "test-model", "outputs": $(mk_outputs_col predict FP32 '[[0.1],[0.2]]') } | ||
| } | ||
| JSON | ||
| )" | ||
|
|
||
| run_test "empty_inputs" "400" "data field was empty" \ | ||
| "$(mk_body "$(mk_inputs_empty)" "$(mk_outputs_col predict FP32 '[[0.1],[0.2]]')")" | ||
|
|
||
| run_test "missing_request_block" "422" '"Field required"' \ | ||
| "$(cat <<JSON | ||
| { | ||
| "model_name": "$MODEL", | ||
| "data_tag": "$TAG", | ||
| "response": { "model_name": "test-model", "outputs": $(mk_outputs_col predict FP32 '[[0.1],[0.2]]') } | ||
| } | ||
| JSON | ||
| )" | ||
|
|
||
| run_test "incorrect_shape" "422" "Declared shape (3, 3) does not match data shape (2, 4)" \ | ||
| "$(mk_body \ | ||
| "$(cat <<JSON | ||
| { | ||
| "inputs": [ | ||
| { "name":"credit_inputs", "shape":[3,3], "datatype":"FP64", "data": [[1,2,3,4],[5,6,7,8]] } | ||
| ] | ||
| } | ||
| JSON | ||
| )" \ | ||
| "$(mk_outputs_col predict FP32 '[[0.1],[0.2]]')" \ | ||
| )" | ||
|
|
||
| run_test "different_model_names" "200" '"status":"success"' \ | ||
| "$(cat <<JSON | ||
| { | ||
| "model_name": "$MODEL", | ||
| "request": $(mk_inputs_2x4_int32), | ||
| "response": { "model_name": "fake-name-123", "outputs": $(mk_outputs_col predict FP32 '[[0.1],[0.2]]') } | ||
| } | ||
| JSON | ||
| )" | ||
|
|
||
| run_test "wrong_bool_values" "422" "must be bool or 0/1" \ | ||
| "$(mk_body "$(mk_inputs_2x4_int32)" "$(mk_outputs_col predict BOOL '[[7],[3]]')")" | ||
|
|
||
| # ---------- dtype sweep (incl. BYTES) ---------- | ||
| line; echo "${CYAN}KServe dtype sweep (outputs)...${RESET}" | ||
|
|
||
| for dt in BOOL INT8 INT16 INT32 INT64 UINT8 UINT16 UINT32 UINT64 FP16 FP32 FP64; do | ||
| data="$(sample_data_for_dtype "$dt")" | ||
| run_test "dtype_${dt}" "200" '"status":"success"' \ | ||
| "$(mk_body "$(mk_inputs_2x4_int32)" "$(mk_outputs_col predict "$dt" "$data")")" | ||
| done | ||
|
|
||
| # BYTES uses a separate model to avoid dtype/storage mixing in existing datasets | ||
| data_bytes='[["a"],["b"]]' | ||
| run_test "dtype_BYTES" "200" '"status":"success"' \ | ||
| "$(mk_body_for_model "$MODEL_BYTES" "$(mk_inputs_2x4_int32)" "$(mk_outputs_col predict BYTES "$data_bytes")")" | ||
|
|
||
| # ---------- summary ---------- | ||
| line | ||
| echo "${CYAN}Summary:${RESET}" | ||
| total=$((pass_cnt+fail_cnt)) | ||
| printf " Total: %d %sPass:%s %d %sFail:%s %d\n" "$total" "$GREEN" "$RESET" "$pass_cnt" "$RED" "$RESET" "$fail_cnt" | ||
| line | ||
|
|
||
| if (( fail_cnt > 0 )); then | ||
| echo "${YELLOW}Details for failures:${RESET}" | ||
| for r in "${results[@]}"; do | ||
| IFS='|' read -r status name http body <<<"$r" | ||
| if [[ "$status" == "FAIL" ]]; then | ||
| printf "%s[FAIL]%s %s (HTTP %s)\n" "$RED" "$RESET" "$name" "$http" | ||
| printf " body: %s\n" "$body" | ||
| line | ||
| fi | ||
| done | ||
| fi |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| from typing import Optional, Dict, List, Literal, Any | ||
| from enum import Enum | ||
| from pydantic import BaseModel, model_validator, ConfigDict | ||
| import numpy as np | ||
|
|
||
|
|
||
| PartialKind = Literal["request", "response"] | ||
|
|
||
| class PartialPayloadId(BaseModel): | ||
| prediction_id: Optional[str] = None | ||
| kind: Optional[PartialKind] = None | ||
|
|
||
| def get_prediction_id(self) -> str: | ||
| return self.prediction_id | ||
|
|
||
| def set_prediction_id(self, id: str): | ||
| self.prediction_id = id | ||
|
|
||
| def get_kind(self) -> PartialKind: | ||
| return self.kind | ||
|
|
||
| def set_kind(self, kind: PartialKind): | ||
| self.kind = kind | ||
|
|
||
|
|
||
| class InferencePartialPayload(BaseModel): | ||
| partialPayloadId: Optional[PartialPayloadId] = None | ||
| metadata: Optional[Dict[str, str]] = {} | ||
| data: Optional[str] = None | ||
| modelid: Optional[str] = None | ||
|
|
||
| def get_id(self) -> str: | ||
| return self.partialPayloadId.prediction_id if self.partialPayloadId else None | ||
|
|
||
| def set_id(self, id: str): | ||
| if not self.partialPayloadId: | ||
| self.partialPayloadId = PartialPayloadId() | ||
| self.partialPayloadId.prediction_id = id | ||
|
|
||
| def get_kind(self) -> PartialKind: | ||
| return self.partialPayloadId.kind if self.partialPayloadId else None | ||
|
|
||
| def set_kind(self, kind: PartialKind): | ||
| if not self.partialPayloadId: | ||
| self.partialPayloadId = PartialPayloadId() | ||
| self.partialPayloadId.kind = kind | ||
|
|
||
| def get_model_id(self) -> str: | ||
| return self.modelid | ||
|
|
||
| def set_model_id(self, model_id: str): | ||
| self.modelid = model_id | ||
|
|
||
|
|
||
| class KServeDataType(str, Enum): | ||
| BOOL = "BOOL" | ||
| INT8 = "INT8" | ||
| INT16 = "INT16" | ||
| INT32 = "INT32" | ||
| INT64 = "INT64" | ||
| UINT8 = "UINT8" | ||
| UINT16 = "UINT16" | ||
| UINT32 = "UINT32" | ||
| UINT64 = "UINT64" | ||
| FP16 = "FP16" | ||
| FP32 = "FP32" | ||
| FP64 = "FP64" | ||
| BYTES = "BYTES" | ||
|
|
||
| K_SERVE_NUMPY_DTYPES = { | ||
| KServeDataType.INT8: np.int8, | ||
| KServeDataType.INT16: np.int16, | ||
| KServeDataType.INT32: np.int32, | ||
| KServeDataType.INT64: np.int64, | ||
| KServeDataType.UINT8: np.uint8, | ||
| KServeDataType.UINT16: np.uint16, | ||
| KServeDataType.UINT32: np.uint32, | ||
| KServeDataType.UINT64: np.uint64, | ||
| KServeDataType.FP16: np.float16, | ||
| KServeDataType.FP32: np.float32, | ||
| KServeDataType.FP64: np.float64, | ||
| } | ||
|
|
||
| class KServeData(BaseModel): | ||
|
|
||
| model_config = ConfigDict(use_enum_values=True) | ||
|
|
||
| name: str | ||
| shape: List[int] | ||
| datatype: KServeDataType | ||
| parameters: Optional[Dict[str, str]] = None | ||
| data: List[Any] | ||
|
|
||
| @model_validator(mode="after") | ||
| def _validate_shape(self) -> "KServeData": | ||
| raw = np.array(self.data, dtype=object) | ||
| actual = tuple(raw.shape) | ||
| declared = tuple(self.shape) | ||
| if declared != actual: | ||
| raise ValueError( | ||
| f"Declared shape {declared} does not match data shape {actual}" | ||
| ) | ||
| return self | ||
|
|
||
| @model_validator(mode="after") | ||
| def validate_data_matches_type(self) -> "KServeData": | ||
| flat = np.array(self.data, dtype=object).flatten() | ||
|
|
||
| if self.datatype == KServeDataType.BYTES: | ||
| for v in flat: | ||
| if not isinstance(v, str): | ||
| raise ValueError( | ||
| f"All values must be JSON strings for datatype {self.datatype}; " | ||
| f"found {type(v).__name__}: {v}" | ||
| ) | ||
| return self | ||
|
|
||
| if self.datatype == KServeDataType.BOOL: | ||
| for v in flat: | ||
| if not (isinstance(v, (bool, int)) and v in (0, 1, True, False)): | ||
| raise ValueError( | ||
| f"All values must be bool or 0/1 for datatype {self.datatype}; found {v}" | ||
| ) | ||
| return self | ||
|
|
||
| np_dtype = K_SERVE_NUMPY_DTYPES.get(self.datatype) | ||
| if np_dtype is None: | ||
| raise ValueError(f"Unsupported datatype: {self.datatype}") | ||
|
|
||
| if np.dtype(np_dtype).kind == "u": | ||
| for v in flat: | ||
| if isinstance(v, (int, float)) and v < 0: | ||
| raise ValueError( | ||
| f"Negative value {v} not allowed for unsigned type {self.datatype}" | ||
| ) | ||
|
|
||
| try: | ||
| np.array(flat, dtype=np_dtype) | ||
| except (ValueError, TypeError) as e: | ||
| raise ValueError(f"Data cannot be cast to {self.datatype}: {e}") | ||
|
|
||
| return self | ||
|
|
||
| class KServeInferenceRequest(BaseModel): | ||
| id: Optional[str] = None | ||
| parameters: Optional[Dict[str, str]] = None | ||
| inputs: List[KServeData] | ||
| outputs: Optional[List[KServeData]] = None | ||
|
|
||
|
|
||
| class KServeInferenceResponse(BaseModel): | ||
| model_name: str = None | ||
| model_version: Optional[str] = None | ||
| id: Optional[str] = None | ||
| parameters: Optional[Dict[str, str]] = None | ||
| outputs: List[KServeData] | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (code-quality): Explicitly raise from a previous error (
raise-from-previous-error)