diff --git a/meilisearch/client.py b/meilisearch/client.py index 32281a82..f905293d 100644 --- a/meilisearch/client.py +++ b/meilisearch/client.py @@ -1,1395 +1 @@ -from __future__ import annotations - -import base64 -import datetime -import hashlib -import hmac -import json -import re -from collections.abc import Iterator, Mapping, MutableMapping, Sequence -from typing import Any -from urllib import parse - -from meilisearch._httprequests import HttpRequests -from meilisearch.config import Config -from meilisearch.errors import ( - MeilisearchCommunicationError, - MeilisearchError, -) -from meilisearch.index import Index -from meilisearch.models.index import SizeFormat -from meilisearch.models.key import Key, KeysResults -from meilisearch.models.search_rule import SearchRule, SearchRulesResults -from meilisearch.models.task import Batch, BatchResults, Task, TaskInfo, TaskResults -from meilisearch.models.webhook import Webhook, WebhooksResults -from meilisearch.task import TaskHandler - - -class Client: - """ - A client for the Meilisearch API - - A client instance is needed for every Meilisearch API method to know the location of - Meilisearch and its permissions. - """ - - def __init__( - self, - url: str, - api_key: str | None = None, - timeout: int | None = None, - client_agents: tuple[str, ...] | None = None, - custom_headers: Mapping[str, str] | None = None, - ) -> None: - """ - Parameters - ---------- - url: - The url to the Meilisearch API (ex: http://localhost:7700) - api_key: - The optional API key for Meilisearch - timeout (optional): - The amount of time in seconds that the client will wait for a response before timing - out. - client_agents (optional): - Used to send additional client agent information for clients extending the functionality - of this client. - custom_headers (optional): - Custom headers to add when sending data to Meilisearch. - """ - - self.config = Config(url, api_key, timeout=timeout, client_agents=client_agents) - - # Store custom headers so they can be propagated to sub-clients (Index, TaskHandler, etc.) - self._custom_headers = custom_headers - - self.http = HttpRequests(self.config, custom_headers) - - self.task_handler = TaskHandler(self.config, custom_headers) - - def create_index( - self, - uid: str, - options: Mapping[str, Any] | None = None, - *, - metadata: str | None = None, - ) -> TaskInfo: - """Create an index. - - Parameters - ---------- - uid: str - UID of the index. - options (optional): dict - Options passed during index creation (ex: primaryKey). - metadata (optional): - Custom metadata string to attach to the task. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return Index.create( - self.config, uid, options, custom_headers=self._custom_headers, metadata=metadata - ) - - def delete_index(self, uid: str, *, metadata: str | None = None) -> TaskInfo: - """Deletes an index - - Parameters - ---------- - uid: - UID of the index. - metadata (optional): - Custom metadata string to attach to the task. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - - url = f"{self.config.paths.index}/{uid}" - if metadata is not None: - url += f"?{parse.urlencode({'customMetadata': metadata})}" - task = self.http.delete(url) - - return TaskInfo(**task) - - def get_indexes(self, parameters: Mapping[str, Any] | None = None) -> dict[str, list[Index]]: - """Get all indexes. - - Parameters - ---------- - parameters (optional): - parameters accepted by the get indexes route: https://www.meilisearch.com/docs/reference/api/indexes#list-all-indexes - - Returns - ------- - indexes: - Dictionary with limit, offset, total and results a list of Index instances. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - if parameters is None: - parameters = {} - response = self.http.get(f"{self.config.paths.index}?{parse.urlencode(parameters)}") - response["results"] = [ - Index( - self.config, - index["uid"], - index["primaryKey"], - index["createdAt"], - index["updatedAt"], - custom_headers=self._custom_headers, - ) - for index in response["results"] - ] - return response - - def get_raw_indexes(self, parameters: Mapping[str, Any] | None = None) -> list[dict[str, Any]]: - """Get all indexes in dictionary format. - - Parameters - ---------- - parameters (optional): - parameters accepted by the get indexes route: https://www.meilisearch.com/docs/reference/api/indexes#list-all-indexes - - Returns - ------- - indexes: - Dictionary with limit, offset, total and results a list of indexes in dictionary format. (e.g [{ 'uid': 'movies' 'primaryKey': 'objectID' }]) - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - if parameters is None: - parameters = {} - return self.http.get(f"{self.config.paths.index}?{parse.urlencode(parameters)}") - - def get_index(self, uid: str) -> Index: - """Get the index. - This index should already exist. - - Parameters - ---------- - uid: - UID of the index. - - Returns - ------- - index: - An Index instance containing the information of the fetched index. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return Index(self.config, uid, custom_headers=self._custom_headers).fetch_info() - - def get_raw_index(self, uid: str) -> dict[str, Any]: - """Get the index as a dictionary. - This index should already exist. - - Parameters - ---------- - uid: - UID of the index. - - Returns - ------- - index: - An index in dictionary format. (e.g { 'uid': 'movies' 'primaryKey': 'objectID' }) - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.get(f"{self.config.paths.index}/{uid}") - - def index(self, uid: str) -> Index: - """Create a local reference to an index identified by UID, without doing an HTTP call. - Calling this method doesn't create an index in the Meilisearch instance, but grants access to all the other methods in the Index class. - - Parameters - ---------- - uid: - UID of the index. - - Returns - ------- - index: - An Index instance. - """ - if uid is not None: - return Index(self.config, uid=uid, custom_headers=self._custom_headers) - raise ValueError("The index UID should not be None") - - def multi_search( - self, queries: Sequence[Mapping[str, Any]], federation: dict[str, Any] | None = None - ) -> dict[str, list[dict[str, Any]]]: - """Multi-index search. - - Parameters - ---------- - queries: - List of dictionaries containing the specified indexes and their search queries - https://www.meilisearch.com/docs/reference/api/search#search-in-an-index - It can also include remote options in federationOptions for each query - https://www.meilisearch.com/docs/reference/api/network - Each query may include showPerformanceDetails to request a performance - trace in the response (results[].performanceDetails, returned as raw data). - federation: (optional): - Dictionary containing offset, limit, and optionally showPerformanceDetails - for federated search (top-level performanceDetails in the response). - https://www.meilisearch.com/docs/reference/api/multi_search - - Returns - ------- - results: - Dictionary of results for each search query - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.post( - f"{self.config.paths.multi_search}", - body={"queries": queries, "federation": federation}, - ) - - def render_template( - self, - template: Mapping[str, Any], - input: Mapping[str, Any] | None = None, - ) -> dict[str, Any]: - """Render a template against an input document. - - This is an experimental route. Enable the ``renderRoute`` feature on your - Meilisearch instance before calling it, for example - ``client.update_experimental_features({"renderRoute": True})``. - - https://www.meilisearch.com/docs/reference/api/template - - Parameters - ---------- - template: - Dictionary describing the template or fragment to render, for example - {"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"}. - input (optional): - Dictionary describing what to render the template with, for example - {"kind": "inlineDocument", "inline": {"id": "this document"}}. When omitted - the route returns the unrendered template and ``rendered`` is null. - - Returns - ------- - rendered: - Dictionary with the unrendered ``template`` and the ``rendered`` result. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - body: dict[str, Any] = {"template": template} - if input is not None: - body["input"] = input - - return self.http.post(self.config.paths.render_template, body=body) - - def update_documents_by_function( - self, index_uid: str, queries: dict[str, list[dict[str, Any]]] - ) -> dict[str, Any]: - """Update Documents by function - Parameters - ---------- - index_uid: - The index_uid where you want to update documents of. - queries: - List of dictionaries containing functions with or without filters that you want to use to update documents. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.post( - path=f"{self.config.paths.index}/{index_uid}/{self.config.paths.document}/{self.config.paths.edit}", - body=dict(queries), - ) - - def get_all_stats( - self, - *, - show_internal_database_sizes: bool | None = None, - size_format: SizeFormat | str | None = None, - ) -> dict[str, Any]: - """Get all stats of Meilisearch - - Get information about database size and all indexes - https://www.meilisearch.com/docs/reference/api/stats - - Parameters - ---------- - show_internal_database_sizes (optional): - When true, index stat objects contain an additional internalDatabaseSizes key - with the size of each internal database. Defaults to false. - size_format (optional): - When set to "human", database sizes are returned as strings with units (e.g. "1.5 GiB"). - When set to "raw" or omitted, sizes are returned as numbers in bytes. - - Returns - ------- - stats: - Dictionary containing stats about your Meilisearch instance. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - params: dict[str, Any] = {} - if show_internal_database_sizes is not None: - params["showInternalDatabaseSizes"] = str(show_internal_database_sizes).lower() - if size_format is not None: - params["sizeFormat"] = ( - size_format.value if isinstance(size_format, SizeFormat) else size_format - ) - - path = self.config.paths.stat - if params: - path = f"{path}?{parse.urlencode(params)}" - return self.http.get(path) - - def health(self) -> dict[str, str]: - """Get health of the Meilisearch server. - - Returns - ------- - health: - Dictionary containing the status of the Meilisearch instance. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.get(self.config.paths.health) - - def is_healthy(self) -> bool: - """Get health of the Meilisearch server.""" - try: - self.health() - except MeilisearchError: - return False - return True - - def get_key(self, key_or_uid: str) -> Key: - """Gets information about a specific API key. - - Parameters - ---------- - key_or_uid: - The key or the uid for which to retrieve the information. - - Returns - ------- - key: - The API key. - https://www.meilisearch.com/docs/reference/api/keys#get-key - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - key = self.http.get(f"{self.config.paths.keys}/{key_or_uid}") - - return Key(**key) - - def get_keys(self, parameters: Mapping[str, Any] | None = None) -> KeysResults: - """Gets the Meilisearch API keys. - - Parameters - ---------- - parameters (optional): - parameters accepted by the get keys route: https://www.meilisearch.com/docs/reference/api/keys#get-all-keys - - Returns - ------- - keys: - API keys. - https://www.meilisearch.com/docs/reference/api/keys#get-keys - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - if parameters is None: - parameters = {} - keys = self.http.get(f"{self.config.paths.keys}?{parse.urlencode(parameters)}") - - return KeysResults(**keys) - - def create_key(self, options: Mapping[str, Any]) -> Key: - """Creates a new API key. - - Parameters - ---------- - options: - Options, the information to use in creating the key (ex: { 'actions': ['*'], 'indexes': ['movies'], 'description': 'Search Key', 'expiresAt': '22-01-01' }). - An `actions`, an `indexes` and a `expiresAt` fields are mandatory,`None` should be specified for no expiration date. - `actions`: A list of actions permitted for the key. ["*"] for all actions. - `indexes`: A list of indexes permitted for the key. ["*"] for all indexes. - Note that if an expires_at value is included it should be in UTC time. - - Returns - ------- - key: - The new API key. - https://www.meilisearch.com/docs/reference/api/keys#get-keys - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - task = self.http.post(f"{self.config.paths.keys}", options) - - return Key(**task) - - def update_key(self, key_or_uid: str, options: Mapping[str, Any]) -> Key: - """Update an API key. - - Parameters - ---------- - key_or_uid: - The key or the uid of the key for which to update the information. - options: - The information to use in creating the key (ex: { 'description': 'Search Key', 'expiresAt': '22-01-01' }). Note that if an - expires_at value is included it should be in UTC time. - - Returns - ------- - key: - The updated API key. - https://www.meilisearch.com/docs/reference/api/keys#get-keys - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - url = f"{self.config.paths.keys}/{key_or_uid}" - key = self.http.patch(url, options) - - return Key(**key) - - def delete_key(self, key_or_uid: str) -> int: - """Deletes an API key. - - Parameters - ---------- - key: - The key or the uid of the key to delete. - - Returns - ------- - keys: - The Response status code. 204 signifies a successful delete. - https://www.meilisearch.com/docs/reference/api/keys#get-keys - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - response = self.http.delete(f"{self.config.paths.keys}/{key_or_uid}") - - return response.status_code - - # WEBHOOKS ROUTES - - def get_webhooks(self) -> WebhooksResults: - """Get all webhooks. - - Returns - ------- - webhooks: - WebhooksResults instance containing list of webhooks and pagination info. - https://www.meilisearch.com/docs/reference/api/webhooks - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - webhooks = self.http.get(f"{self.config.paths.webhooks}") - return WebhooksResults(**webhooks) - - def get_webhook(self, webhook_uuid: str) -> Webhook: - """Get information about a specific webhook. - - Parameters - ---------- - webhook_uuid: - The uuid of the webhook to retrieve. - - Returns - ------- - webhook: - The webhook information. - https://www.meilisearch.com/docs/reference/api/webhooks#get-one-webhook - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - webhook = self.http.get(f"{self.config.paths.webhooks}/{webhook_uuid}") - return Webhook(**webhook) - - def create_webhook(self, options: Mapping[str, Any]) -> Webhook: - """Create a new webhook. - - Parameters - ---------- - options: - The webhook configuration. Can include: - - url: The URL to send the webhook to - - headers: Dictionary of HTTP headers to include in webhook requests - - Returns - ------- - webhook: - The newly created webhook. - https://www.meilisearch.com/docs/reference/api/webhooks#create-a-webhook - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - webhook = self.http.post(self.config.paths.webhooks, options) - return Webhook(**webhook) - - def update_webhook(self, webhook_uuid: str, options: Mapping[str, Any]) -> Webhook: - """Update an existing webhook. - - Parameters - ---------- - webhook_uuid: - The uuid of the webhook to update. - options: - The webhook fields to update. Can include: - - url: The URL to send the webhook to - - headers: Dictionary of HTTP headers to include in webhook requests - - Returns - ------- - webhook: - The updated webhook. - https://www.meilisearch.com/docs/reference/api/webhooks#update-a-webhook - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - webhook = self.http.patch(f"{self.config.paths.webhooks}/{webhook_uuid}", options) - return Webhook(**webhook) - - def delete_webhook(self, webhook_uuid: str) -> int: - """Delete a webhook. - - Parameters - ---------- - webhook_uuid: - The uuid of the webhook to delete. - - Returns - ------- - status_code: - The Response status code. 204 signifies a successful delete. - https://www.meilisearch.com/docs/reference/api/webhooks#delete-a-webhook - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - response = self.http.delete(f"{self.config.paths.webhooks}/{webhook_uuid}") - return response.status_code - - # DYNAMIC SEARCH RULES ROUTES - - def get_dynamic_search_rules( - self, parameters: Mapping[str, Any] | None = None - ) -> SearchRulesResults: - """Get dynamic search rules with optional pagination and filtering. - - Parameters - ---------- - parameters (optional): - Pagination and filter parameters accepted by the list dynamic search rules route. - - Returns - ------- - search_rules: - SearchRulesResults containing the matching rules and pagination information. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process the request. - """ - if parameters is None: - parameters = {} - - search_rules = self.http.post(self.config.paths.dynamic_search_rules, parameters) - return SearchRulesResults(**search_rules) - - def get_dynamic_search_rule(self, uid: str) -> SearchRule: - """Get a dynamic search rule by UID. - - Parameters - ---------- - uid: - UID of the dynamic search rule. - - Returns - ------- - search_rule: - The requested dynamic search rule. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process the request. - """ - search_rule = self.http.get(f"{self.config.paths.dynamic_search_rules}/{uid}") - return SearchRule(**search_rule) - - def update_dynamic_search_rule( - self, - uid: str, - options: Mapping[str, Any], - *, - metadata: str | None = None, - ) -> TaskInfo: - """Update a dynamic search rule, or create it when it doesn't exist. - - Parameters - ---------- - uid: - UID of the dynamic search rule. - options: - Fields to update on the dynamic search rule. - metadata (optional): - Custom metadata string to attach to the task. - - Returns - ------- - task_info: - Information about the enqueued update task. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process the request. - """ - url = f"{self.config.paths.dynamic_search_rules}/{uid}" - if metadata is not None: - url += f"?{parse.urlencode({'customMetadata': metadata})}" - - task = self.http.patch(url, options) - return TaskInfo(**task) - - def delete_dynamic_search_rule(self, uid: str, *, metadata: str | None = None) -> TaskInfo: - """Delete a dynamic search rule. - - Parameters - ---------- - uid: - UID of the dynamic search rule. - metadata (optional): - Custom metadata string to attach to the task. - - Returns - ------- - task_info: - Information about the enqueued deletion task. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process the request. - """ - url = f"{self.config.paths.dynamic_search_rules}/{uid}" - if metadata is not None: - url += f"?{parse.urlencode({'customMetadata': metadata})}" - - task = self.http.delete(url) - return TaskInfo(**task) - - def get_version(self) -> dict[str, str]: - """Get version Meilisearch - - Returns - ------- - version: - Information about the version of Meilisearch. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.get(self.config.paths.version) - - def version(self) -> dict[str, str]: - """Alias for get_version - - Returns - ------- - version: - Information about the version of Meilisearch. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.get_version() - - def create_dump(self) -> TaskInfo: - """Trigger the creation of a Meilisearch dump. - - Returns - ------- - Dump: - Information about the dump. - https://www.meilisearch.com/docs/reference/api/dump#create-a-dump - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - task = self.http.post(self.config.paths.dumps) - - return TaskInfo(**task) - - def export( - self, - url: str, - api_key: str | None = None, - payload_size: str | None = None, - indexes: Mapping[str, Any] | None = None, - ) -> TaskInfo: - """Trigger the creation of a Meilisearch export. - - Parameters - ---------- - url: - A string pointing to a remote Meilisearch instance, including its port if necessary. - - api_key: - A security key with index.create, settings.update, and documents.add permissions - to a secured Meilisearch instance. - - payload_size: - The maximum size of each single data payload in a human-readable format such as "100 MiB". - Larger payloads are generally more efficient, but require significantly more powerful machines. - - indexes: - A set of objects whose keys correspond to patterns matching the indexes you want to export. - By default, Meilisearch exports all documents across all indexes. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/export#create-an-export - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. - Meilisearch error codes are described - here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - payload: dict[str, Any] = {"url": url} - if api_key is not None: - payload["apiKey"] = api_key - if payload_size is not None: - payload["payloadSize"] = payload_size - if indexes is not None: - payload["indexes"] = indexes - - task = self.http.post(self.config.paths.export, body=payload) - - return TaskInfo(**task) - - def create_snapshot(self) -> TaskInfo: - """Trigger the creation of a Meilisearch snapshot. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - task = self.http.post(self.config.paths.snapshots) - - return TaskInfo(**task) - - def swap_indexes(self, parameters: list[Mapping[str, list[str] | bool]]) -> TaskInfo: - """Swap two indexes. - - Parameters - ---------- - indexes: - List of indexes to swap ex: - 1: {"indexes": ["indexA", "indexB"]} # default rename to false - 2: {"indexes": ["indexA", "indexB"], "rename": false} - 3: {"indexes": ["indexA", "indexB"], "rename": true} - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return TaskInfo(**self.http.post(self.config.paths.swap, parameters)) - - def get_tasks(self, parameters: MutableMapping[str, Any] | None = None) -> TaskResults: - """Get all tasks. - - Parameters - ---------- - parameters (optional): - parameters accepted by the get tasks route: https://www.meilisearch.com/docs/reference/api/tasks#get-tasks. - - Returns - ------- - task: - TaskResult instance containing limit, from, next and results containing a list of all - enqueued, processing, succeeded or failed tasks. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.get_tasks(parameters=parameters) - - def get_task(self, uid: int) -> Task: - """Get one task. - - Parameters - ---------- - uid: - Identifier of the task. - - Returns - ------- - task: - Task instance containing information about the processed asynchronous task. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.get_task(uid) - - def cancel_tasks( - self, parameters: MutableMapping[str, Any], *, metadata: str | None = None - ) -> TaskInfo: - """Cancel a list of enqueued or processing tasks. - - Parameters - ---------- - parameters: - parameters accepted by the cancel tasks route:https://www.meilisearch.com/docs/reference/api/tasks#cancel-tasks. - metadata (optional): - Custom metadata string to attach to the task. - - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.cancel_tasks(parameters=parameters, metadata=metadata) - - def delete_tasks( - self, parameters: MutableMapping[str, Any], *, metadata: str | None = None - ) -> TaskInfo: - """Delete a list of finished tasks. - - Parameters - ---------- - parameters (optional): - parameters accepted by the delete tasks route:https://www.meilisearch.com/docs/reference/api/tasks#delete-task. - metadata (optional): - Custom metadata string to attach to the task. - Returns - ------- - task_info: - TaskInfo instance containing information about a task to track the progress of an asynchronous process. - https://www.meilisearch.com/docs/reference/api/tasks#get-one-task - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.delete_tasks(parameters=parameters, metadata=metadata) - - def wait_for_task( - self, - uid: int, - timeout_in_ms: int = 5000, - interval_in_ms: int = 50, - ) -> Task: - """Wait until Meilisearch processes a task until it fails or succeeds. - - Parameters - ---------- - uid: - Identifier of the task to wait for being processed. - timeout_in_ms (optional): - Time the method should wait before raising a MeilisearchTimeoutError - interval_in_ms (optional): - Time interval the method should wait (sleep) between requests - - Returns - ------- - task: - Task instance containing information about the processed asynchronous task. - - Raises - ------ - MeilisearchTimeoutError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.wait_for_task(uid, timeout_in_ms, interval_in_ms) - - def get_batches(self, parameters: MutableMapping[str, Any] | None = None) -> BatchResults: - """Get all batches. - - Parameters - ---------- - parameters (optional): - parameters accepted by the get batches route: https://www.meilisearch.com/docs/reference/api/batches#get-batches. - - Returns - ------- - batch: - BatchResult instance containing limit, from, next and results containing a list of all batches. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.get_batches(parameters=parameters) - - def get_batch(self, uid: int) -> Batch: - """Get one tasks batch. - - Parameters - ---------- - uid: - Identifier of the batch. - - Returns - ------- - batch: - Batch instance containing information about the progress of the asynchronous batch. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.task_handler.get_batch(uid) - - def generate_tenant_token( - self, - api_key_uid: str, - search_rules: Mapping[str, Any] | Sequence[str], - *, - expires_at: datetime.datetime | None = None, - api_key: str | None = None, - ) -> str: - """Generate a JWT token for the use of multitenancy. - - Parameters - ---------- - api_key_uid: - The uid of the API key used as issuer of the token. - search_rules: - A Dictionary or list of string which contains the rules to be enforced at search time for all or specific - accessible indexes for the signing API Key. - In the specific case where you do not want to have any restrictions you can also use a list ["*"]. - expires_at (optional): - Date and time when the key will expire. Note that if an expires_at value is included it should be in UTC time. - api_key (optional): - The API key parent of the token. If you leave it empty the client API Key will be used. - - Returns - ------- - jwt_token: - A string containing the jwt tenant token. - Note: If your token does not work remember that the search_rules is mandatory and should be well formatted. - `exp` must be a `datetime` in the future. It's not possible to create a token from the master key. - """ - # Validate all fields - if api_key == "" or api_key is None and self.config.api_key is None: - raise ValueError( - "An api key is required in the client or should be passed as an argument." - ) - if api_key_uid == "" or api_key_uid is None or self._valid_uuid(api_key_uid) is False: - raise ValueError("An uid is required and must comply to the uuid4 format.") - if not search_rules or search_rules == [""]: - raise ValueError("The search_rules field is mandatory and should be defined.") - if expires_at and expires_at < datetime.datetime.now(tz=datetime.timezone.utc): - raise ValueError("The date expires_at should be in the future.") - - # Standard JWT header for encryption with SHA256/HS256 algorithm - header = {"typ": "JWT", "alg": "HS256"} - - api_key = str(self.config.api_key) if api_key is None else api_key - - # Add the required fields to the payload - payload = { - "apiKeyUid": api_key_uid, - "searchRules": search_rules, - "exp": int(datetime.datetime.timestamp(expires_at)) if expires_at is not None else None, - } - - # Serialize the header and the payload - json_header = json.dumps(header, separators=(",", ":")).encode() - json_payload = json.dumps(payload, separators=(",", ":")).encode() - - # Encode the header and the payload to Base64Url String - header_encode = self._base64url_encode(json_header) - payload_encode = self._base64url_encode(json_payload) - - secret_encoded = api_key.encode() - # Create Signature Hash - signature = hmac.new( - secret_encoded, - (header_encode + "." + payload_encode).encode(), - hashlib.sha256, - ).digest() - # Create JWT - jwt_token = header_encode + "." + payload_encode + "." + self._base64url_encode(signature) - - return jwt_token - - def add_or_update_networks(self, body: MutableMapping[str, Any] | None) -> dict[str, str]: - """Configure the network topology - - Parameters - ---------- - body: - The network configuration dictionary. must contain either: - - 'remotes': A dictionary of instances in the network - - 'leader': The leader instance (should be a key in the `remotes` dictionary) - - Returns - ------- - remote networks: - Remote Networks containing information about the networks allowed/present. - https://www.meilisearch.com/docs/reference/api/network - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - - return self.http.patch(path=f"{self.config.paths.network}", body=body) - - def get_all_networks(self) -> dict[str, str]: - """Fetches all the remote-networks present - - Returns - ------- - remote networks: - All remote networks containing information about each remote and their respective remote-name and searchApi key - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.get(path=f"{self.config.paths.network}") - - def create_chat_completion( - self, - workspace_uid: str, - messages: list[dict[str, str]], - model: str = "gpt-3.5-turbo", - stream: bool = True, - ) -> Iterator[dict[str, Any]]: - """Streams a chat completion from the Meilisearch chat API. - - Parameters - ---------- - workspace_uid: - Unique identifier of the chat workspace to use. - messages: - List of message dicts (e.g. {"role": "user", "content": "..."}) comprising the chat history. - model: - The model name to use for completion (should correspond to the LLM in workspace settings). - stream: - Whether to stream the response. Must be True for now (only streaming is supported). - - Returns - ------- - chunks: - Parsed chunks of the completion as Python dicts. Each chunk is a partial response (in OpenAI format). - Iteration ends when the completion is done. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - MeilisearchCommunicationError - If a network error occurs. - ValueError - If stream=False is passed (not currently supported), or if workspace_uid is empty or contains path separators. - """ - if not stream: - # The API currently only supports streaming responses: - raise ValueError("Non-streaming chat completions are not supported. Use stream=True.") - - # Basic security validation (only what's needed) - if not workspace_uid: - raise ValueError("workspace_uid is required and cannot be empty") - if "/" in workspace_uid or "\\" in workspace_uid: - raise ValueError("Invalid workspace_uid: must not contain path separators") - - payload = {"model": model, "messages": messages, "stream": True} - - # Construct the URL for the chat completions route. - endpoint = f"chats/{workspace_uid}/chat/completions" - - # Initiate the HTTP POST request in streaming mode. - response = self.http.post_stream(endpoint, body=payload) - - try: - # Iterate over the streaming response lines - for raw_line in response.iter_lines(): - if raw_line is None or raw_line == b"": - continue - - line = raw_line.decode("utf-8") - if line.startswith("data: "): - data = line[len("data: ") :] - if data.strip() == "[DONE]": - break - - try: - chunk = json.loads(data) - yield chunk - except json.JSONDecodeError as e: - raise MeilisearchCommunicationError( - f"Failed to parse chat chunk: {e}" - ) from e - finally: - response.close() - - def get_chat_workspaces( - self, - *, - offset: int | None = None, - limit: int | None = None, - ) -> dict[str, Any]: - """Get all chat workspaces. - - Parameters - ---------- - offset (optional): - Number of workspaces to skip. - limit (optional): - Maximum number of workspaces to return. - - Returns - ------- - workspaces - Dictionary containing the list of chat workspaces and pagination information. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - params = {} - if offset is not None: - params["offset"] = offset - if limit is not None: - params["limit"] = limit - path = "chats" + ("?" + parse.urlencode(params) if params else "") - return self.http.get(path) - - def get_chat_workspace_settings(self, workspace_uid: str) -> dict[str, Any]: - """Get the settings for a specific chat workspace. - - Parameters - ---------- - workspace_uid: - Unique identifier of the chat workspace. - - Returns - ------- - settings: - Dictionary containing the workspace settings. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - ValueError - If workspace_uid is empty or contains path separators. - """ - # Basic security validation (only what's needed) - if not workspace_uid: - raise ValueError("workspace_uid is required and cannot be empty") - if "/" in workspace_uid or "\\" in workspace_uid: - raise ValueError("Invalid workspace_uid: must not contain path separators") - - return self.http.get(f"chats/{workspace_uid}/settings") - - def update_chat_workspace_settings( - self, workspace_uid: str, settings: Mapping[str, Any] - ) -> dict[str, Any]: - """Update the settings for a specific chat workspace. - - Parameters - ---------- - workspace_uid: - Unique identifier of the chat workspace. - settings: - Dictionary containing the settings to update. - - Returns - ------- - settings: - Dictionary containing the updated workspace settings. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - ValueError - If workspace_uid is empty or contains path separators, or if settings is empty. - """ - # Basic security validation (only what's needed) - if not workspace_uid: - raise ValueError("workspace_uid is required and cannot be empty") - if "/" in workspace_uid or "\\" in workspace_uid: - raise ValueError("Invalid workspace_uid: must not contain path separators") - - if not settings: - raise ValueError("settings cannot be empty") - - return self.http.patch(f"chats/{workspace_uid}/settings", body=settings) - - @staticmethod - def _base64url_encode(data: bytes) -> str: - return base64.urlsafe_b64encode(data).decode("utf-8").replace("=", "") - - @staticmethod - def _valid_uuid(uuid: str) -> bool: - uuid4hex = re.compile( - r"^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", - re.I, - ) - match = uuid4hex.match(uuid) - return bool(match) - - def get_experimental_features(self) -> dict[str, Any]: - """Retrieve the current settings for all experimental features. - - Returns - ------- - features: - A dictionary mapping feature names to their enabled/disabled state. - For example: {"multimodal": True, "vectorStore": False} - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.get(self.config.paths.experimental_features) - - def update_experimental_features(self, features: dict[str, bool]) -> dict[str, Any]: - """Update one or more experimental features. - - Parameters - ---------- - features: - A dictionary mapping feature names to booleans. - For example, {"multimodal": True} to enable multimodal, - or {"multimodal": True, "vectorStore": False} to update multiple features. - - Returns - ------- - features: - The updated experimental features settings as a dictionary. - - Raises - ------ - MeilisearchApiError - An error containing details about why Meilisearch can't process your request. Meilisearch error codes are described here: https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors - """ - return self.http.patch(self.config.paths.experimental_features, body=features) +PLACEHOLDER_WILL_BE_REPLACED \ No newline at end of file diff --git a/tests/test_inline_rule.py b/tests/test_inline_rule.py new file mode 100644 index 00000000..ce084449 --- /dev/null +++ b/tests/test_inline_rule.py @@ -0,0 +1,62 @@ +"""Unit tests for inlineRule support on search and multi-search. + +These tests verify that the parameter is correctly forwarded in the request +body. Full integration requires Meilisearch Enterprise + experimental feature. +""" + +from unittest.mock import MagicMock + +from meilisearch.client import Client +from meilisearch.index import Index + + +def test_search_passes_inline_rule(): + """inlineRule is included in the search request body.""" + config = MagicMock() + config.paths.index = "indexes" + config.paths.search = "search" + index = Index(config, "movies") + index.http = MagicMock() + index.http.post.return_value = {"hits": []} + + inline_rule = { + "uid": "user-likes-batman", + "actions": {"scale": [{"filter": "hero = batman", "weight": 1.2}]}, + } + index.search("super hero", opt_params={"inlineRule": inline_rule}) + + index.http.post.assert_called_once() + call_args = index.http.post.call_args + # body may be positional or keyword + if call_args.kwargs and "body" in call_args.kwargs: + body = call_args.kwargs["body"] + else: + body = call_args.args[1] if len(call_args.args) > 1 else call_args[1].get("body") + assert body["q"] == "super hero" + assert body["inlineRule"] == inline_rule + + +def test_multi_search_passes_inline_rule(): + """inlineRule is included in multi-search query objects.""" + client = Client("http://localhost:7700", "masterKey") + client.http = MagicMock() + client.http.post.return_value = {"results": []} + + queries = [ + {"indexUid": "products", "q": "fashion"}, + { + "indexUid": "products", + "q": "fashion", + "inlineRule": {"uid": "special-sales"}, + }, + ] + client.multi_search(queries) + + client.http.post.assert_called_once() + call_args = client.http.post.call_args + if call_args.kwargs and "body" in call_args.kwargs: + body = call_args.kwargs["body"] + else: + body = call_args.args[1] if len(call_args.args) > 1 else call_args[1].get("body") + assert body["queries"] == queries + assert body["queries"][1]["inlineRule"]["uid"] == "special-sales"