diff --git a/docs/v2/parsing/index.rst b/docs/v2/parsing/index.rst index af087dae..f4707c02 100644 --- a/docs/v2/parsing/index.rst +++ b/docs/v2/parsing/index.rst @@ -8,3 +8,4 @@ V2 Parsing ./inference/index ./error ./job + ./search diff --git a/docs/v2/parsing/search.rst b/docs/v2/parsing/search.rst new file mode 100644 index 00000000..565c8f3a --- /dev/null +++ b/docs/v2/parsing/search.rst @@ -0,0 +1,36 @@ +--------- +V2 Search +--------- + +Model Webhook +############# +.. autoclass:: mindee.v2.parsing.search.model_webhook.ModelWebhook + :members: + :inherited-members: + + +Pagination +########## +.. autoclass:: mindee.v2.parsing.search.pagination.Pagination + :members: + :inherited-members: + + +Search Model +############ +.. autoclass:: mindee.v2.parsing.search.search_model.SearchModel + :members: + :inherited-members: + +Search Models +############# +.. autoclass:: mindee.v2.parsing.search.search_models.SearchModels + :members: + :inherited-members: + +Search Response +############### +.. autoclass:: mindee.v2.parsing.search.search_response.SearchResponse + :members: + :inherited-members: + diff --git a/mindee/v2/client.py b/mindee/v2/client.py index b98b9aba..d4c61209 100644 --- a/mindee/v2/client.py +++ b/mindee/v2/client.py @@ -1,42 +1,23 @@ from time import sleep from typing import TypeVar -import requests - from mindee.client_mixin import ClientMixin from mindee.client_options.polling_options import PollingOptions from mindee.error.mindee_error import MindeeError from mindee.input import URLInputSource from mindee.input.local_input_source import LocalInputSource from mindee.logger import logger -from mindee.parsing.common import StringDict from mindee.parsing.common.common_response import CommonStatus from mindee.v2.client_options.base_parameters import BaseParameters -from mindee.v2.error.mindee_http_error_v2 import ( - MindeeHTTPUnknownErrorV2, - handle_error_v2, -) from mindee.v2.mindee_http.mindee_api_v2 import MindeeAPIV2 -from mindee.v2.mindee_http.response_validation_v2 import ( - is_valid_get_response, - is_valid_post_response, -) from mindee.v2.parsing.inference.base_response import BaseResponse from mindee.v2.parsing.job.job_response import JobResponse +from mindee.v2.parsing.search.search_response import SearchResponse from mindee.v2.product.extraction.extraction_response import ExtractionResponse TypeBaseResponse = TypeVar("TypeBaseResponse", bound=BaseResponse) -def _response_json(response: requests.Response) -> StringDict: - try: - return response.json() - except ValueError as exc: - raise MindeeHTTPUnknownErrorV2( - f"HTTP {response.status_code} response is not valid JSON: {response.text}" - ) from exc - - class Client(ClientMixin): """ Mindee API Client. @@ -70,14 +51,7 @@ def enqueue( :return: A valid inference response. """ logger.debug("Enqueuing inference using model: %s", params.model_id) - response = self.mindee_api.req_post_inference_enqueue( - input_source=input_source, params=params, slug=params.get_enqueue_slug() - ) - dict_response = _response_json(response) - - if not is_valid_post_response(response): - handle_error_v2(dict_response) - return JobResponse(dict_response) + return self.mindee_api.enqueue(input_source, params) def get_job(self, job_id: str) -> JobResponse: """ @@ -90,11 +64,7 @@ def get_job(self, job_id: str) -> JobResponse: """ logger.debug("Fetching job: %s", job_id) - response = self.mindee_api.req_get_job(job_id) - dict_response = _response_json(response) - if not is_valid_get_response(response): - handle_error_v2(dict_response) - return JobResponse(dict_response) + return self.mindee_api.get_job(job_id) def get_result( self, @@ -112,13 +82,7 @@ def get_result( """ logger.debug("Fetching result: %s", inference_id) - response = self.mindee_api.req_get_inference( - inference_id, response_type.get_result_slug() - ) - dict_response = _response_json(response) - if not is_valid_get_response(response): - handle_error_v2(dict_response) - return response_type(dict_response) + return self.mindee_api.get_result(response_type, inference_id) def enqueue_and_get_result( self, @@ -176,3 +140,15 @@ def enqueue_and_get_result( sleep(params.polling_options.delay_sec) raise MindeeError(f"Couldn't retrieve document after {try_counter + 1} tries.") + + def search_models( + self, name: str | None = None, model_type: str | None = None + ) -> SearchResponse: + """ + Get a list of models matching the provided name and type. + + :param name: Name of the model to filter by. + :param model_type: Type of the model to filter by. + :return: A list of models matching the provided criteria. + """ + return self.mindee_api.get_models(name, model_type) diff --git a/mindee/v2/mindee_http/mindee_api_v2.py b/mindee/v2/mindee_http/mindee_api_v2.py index c1ab501a..9e5387ee 100644 --- a/mindee/v2/mindee_http/mindee_api_v2.py +++ b/mindee/v2/mindee_http/mindee_api_v2.py @@ -6,9 +6,20 @@ from mindee.input.url_input_source import URLInputSource from mindee.logger import logger from mindee.mindee_http.settings_mixin import SettingsMixin +from mindee.parsing.common.string_dict import StringDict from mindee.v1.mindee_http.base_settings import USER_AGENT from mindee.v2.client_options.base_parameters import BaseParameters from mindee.v2.error.mindee_api_v2_error import MindeeAPIV2Error +from mindee.v2.error.mindee_http_error_v2 import ( + MindeeHTTPUnknownErrorV2, + handle_error_v2, +) +from mindee.v2.mindee_http.response_validation_v2 import ( + is_valid_get_response, + is_valid_post_response, +) +from mindee.v2.parsing.job.job_response import JobResponse +from mindee.v2.parsing.search.search_response import SearchResponse API_KEY_V2_ENV_NAME = "MINDEE_V2_API_KEY" API_KEY_V2_DEFAULT = "" @@ -135,3 +146,92 @@ def req_get_inference(self, inference_id: str, slug: str) -> requests.Response: timeout=self.request_timeout, allow_redirects=False, ) + + def req_get_search_models( + self, model_name: str | None, model_type: str | None + ) -> requests.Response: + """ + Searches for a list of models matching criteria. + :param model_name: Name pattern to search for. + :param model_type: Type of model to search for (exact match). + :return: Response object containing search results. + """ + url = f"{self.url_root}/v2/search/models" + return requests.get( + url, + headers=self.base_headers, + params={"name": model_name, "model_type": model_type}, + timeout=self.request_timeout, + ) + + def enqueue( + self, input_source: LocalInputSource | URLInputSource, params: BaseParameters + ) -> JobResponse: + """ + Enqueues a document to a given model. + :param input_source: Input object. + :param params: Parameters + :return: A valid inference Response. + """ + + response = self.req_post_inference_enqueue( + input_source=input_source, params=params, slug=params.get_enqueue_slug() + ) + dict_response = self._response_json(response) + + if not is_valid_post_response(response): + handle_error_v2(dict_response) + return JobResponse(dict_response) + + def get_job(self, job_id: str) -> JobResponse: + """ + Get the status of an inference that was previously enqueued. + + Can be used for polling. + + :param job_id: UUID of the job to retrieve. + :return: A job response. + """ + response = self.req_get_job(job_id) + dict_response = self._response_json(response) + if not is_valid_get_response(response): + handle_error_v2(dict_response) + return JobResponse(dict_response) + + def get_result(self, response_type, inference_id: str): + """ + Get the result of an inference that was previously enqueued. + + :param response_type: Type of the response to return. + :param inference_id: UUID of the inference to retrieve. + :return: The result of the inference. + """ + response = self.req_get_inference(inference_id, response_type.get_result_slug()) + dict_response = self._response_json(response) + if not is_valid_get_response(response): + handle_error_v2(dict_response) + return response_type(dict_response) + + def get_models(self, name: str | None, model_type: str | None): + """ + Get a list of models matching the provided name and type. + + :param name: Name of the model to filter by. + :param model_type: Type of the model to filter by. + :return: A list of models matching the provided criteria. + """ + logger.debug("Fetching models matching: name=%s and type=%s", name, model_type) + response = self.req_get_search_models(name, model_type) + dict_response = self._response_json(response) + if not is_valid_get_response(response): + handle_error_v2(dict_response) + return SearchResponse(dict_response) + + @staticmethod + def _response_json(response: requests.Response) -> StringDict: + try: + return response.json() + except ValueError as exc: + raise MindeeHTTPUnknownErrorV2( + f"HTTP {response.status_code} response is not valid JSON: {response.text}" + ) from exc diff --git a/mindee/v2/mindee_http/response_validation_v2.py b/mindee/v2/mindee_http/response_validation_v2.py index f76d1407..30f3e259 100644 --- a/mindee/v2/mindee_http/response_validation_v2.py +++ b/mindee/v2/mindee_http/response_validation_v2.py @@ -30,4 +30,8 @@ def is_valid_get_response(response: requests.Response) -> bool: if not is_valid_sync_response(response): return False response_json = json.loads(response.content) - return "inference" in response_json or "job" in response_json + return ( + "inference" in response_json + or "job" in response_json + or "models" in response_json + ) diff --git a/mindee/v2/parsing/search/__init__.py b/mindee/v2/parsing/search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mindee/v2/parsing/search/model_webhook.py b/mindee/v2/parsing/search/model_webhook.py new file mode 100644 index 00000000..d4d3bd88 --- /dev/null +++ b/mindee/v2/parsing/search/model_webhook.py @@ -0,0 +1,17 @@ +class ModelWebhook: + """Model webhook information.""" + + id: str + """ID of the webhook.""" + name: str + """Name of the webhook.""" + url: str + """URL of the webhook.""" + + def __init__(self, server_response: dict) -> None: + self.id = server_response["id"] + self.name = server_response["name"] + self.url = server_response["url"] + + def __str__(self) -> str: + return f":Name: {self.name}\n:ID: {self.id}\n:URL: {self.url}" diff --git a/mindee/v2/parsing/search/pagination.py b/mindee/v2/parsing/search/pagination.py new file mode 100644 index 00000000..34b63a3d --- /dev/null +++ b/mindee/v2/parsing/search/pagination.py @@ -0,0 +1,28 @@ +class Pagination: + """Pagination metadata.""" + + per_page: int + """Number of results per page.""" + page: int + """1-indexed page number.""" + total_items: int + """Total number of items.""" + total_pages: int + """Total number of pages.""" + total_items_unfiltered: int | None + """Total number of items, including unfiltered results.""" + + def __init__(self, server_response: dict) -> None: + self.per_page = server_response["per_page"] + self.page = server_response["page"] + self.total_items = server_response["total_items"] + self.total_pages = server_response["total_pages"] + self.total_items_unfiltered = server_response.get("total_items_unfiltered") + + def __str__(self) -> str: + return ( + f":Per Page: {self.per_page}\n" + f":Page: {self.page}\n" + f":Total Items: {self.total_items}\n" + f":Total Pages: {self.total_pages}\n" + ) diff --git a/mindee/v2/parsing/search/search_model.py b/mindee/v2/parsing/search/search_model.py new file mode 100644 index 00000000..97be009c --- /dev/null +++ b/mindee/v2/parsing/search/search_model.py @@ -0,0 +1,25 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.search.model_webhook import ModelWebhook + + +class SearchModel: + """Individual model information.""" + + id: str + """Model ID.""" + name: str + """Model name.""" + model_type: str + """Model type.""" + webhooks: list[ModelWebhook] + """Webhooks associated with the model.""" + + def __init__(self, server_response: StringDict) -> None: + self.id = server_response["id"] + self.name = server_response["name"] + self.model_type = server_response["model_type"] + self.webhooks = ( + [ModelWebhook(webhook) for webhook in server_response["webhooks"]] + if "webhooks" in server_response + else [] + ) diff --git a/mindee/v2/parsing/search/search_models.py b/mindee/v2/parsing/search/search_models.py new file mode 100644 index 00000000..6ca05542 --- /dev/null +++ b/mindee/v2/parsing/search/search_models.py @@ -0,0 +1,24 @@ +from mindee.v2.parsing.search.search_model import SearchModel + + +class SearchModels(list[SearchModel]): + """List of models.""" + + def __init__(self, raw_response: list[dict]) -> None: + super().__init__([SearchModel(model) for model in raw_response]) + + def __str__(self) -> str: + """ + Default string representation. + """ + if len(self) == 0: + return "\n" + + lines = [] + for model in self: + lines.append(f"* :Name: {model.name}") + lines.append(f" :ID: {model.id}") + lines.append(f" :Model Type: {model.model_type}") + lines.append(f" :Webhooks: {len(model.webhooks)}") + + return "\n".join(lines) + "\n" diff --git a/mindee/v2/parsing/search/search_response.py b/mindee/v2/parsing/search/search_response.py new file mode 100644 index 00000000..233be58d --- /dev/null +++ b/mindee/v2/parsing/search/search_response.py @@ -0,0 +1,32 @@ +from mindee.parsing.common.string_dict import StringDict +from mindee.v2.parsing.search.pagination import Pagination +from mindee.v2.parsing.search.search_models import SearchModels + + +class SearchResponse: + """Models search response.""" + + models: SearchModels + """Parsed search payload.""" + pagination: Pagination + """Pagination metadata for the search results.""" + + def __init__(self, raw_response: StringDict) -> None: + self.models = SearchModels(raw_response["models"]) + self.pagination = Pagination(raw_response["pagination"]) + + def __str__(self) -> str: + """ + String representation. + """ + return "\n".join( + [ + "Models", + "######", + str(self.models), + "Pagination Metadata", + "###################", + str(self.pagination), + "", + ] + ) diff --git a/tests/v2/search/__init__.py b/tests/v2/search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/v2/search/test_search_models.py b/tests/v2/search/test_search_models.py new file mode 100644 index 00000000..2173f552 --- /dev/null +++ b/tests/v2/search/test_search_models.py @@ -0,0 +1,33 @@ +import json + +import pytest + +from mindee.v2.parsing.search.search_response import SearchResponse +from tests.utils import V2_DATA_DIR + + +@pytest.mark.v2 +def test_search_models(): + data_file = V2_DATA_DIR / "search" / "models.json" + json_file = data_file.read_text() + models_json = json.loads(json_file) + models_response = SearchResponse(models_json) + + assert len(models_response.models) == models_response.pagination.total_items == 5 + assert models_response.pagination.page == 1 + assert models_response.pagination.per_page == 50 + assert models_response.pagination.total_pages == 1 + assert models_response.pagination.total_items_unfiltered is None + + assert models_response.models[0].name == "Extraction With Webhooks" + assert models_response.models[0].id == "afde5151-aa11-aa11-9289-fa04e50ca3b9" + assert models_response.models[0].model_type == "extraction" + assert len(models_response.models[0].webhooks) == 2 + assert ( + models_response.models[0].webhooks[0].id + == "a2286ed9-aa11-aa11-bdc5-2f8496c5641a" + ) + assert models_response.models[0].webhooks[0].name == "FAILURE" + assert models_response.models[0].webhooks[0].url == "https://failure.mindee.com" + assert models_response.models[-1].name == "Extraction Without Webhooks Key" + assert models_response.models[-1].id == "e14e0923-ee55-ee55-a335-8d2110917d7b"