├── .code-samples.meilisearch.yaml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── release-draft-template.yml ├── scripts │ └── check-release.sh └── workflows │ ├── documentation.yml │ ├── pre-release-tests.yml │ ├── pypi-publish.yml │ ├── release-drafter.yml │ ├── tests.yml │ └── update-version.yml ├── .gitignore ├── .yamllint.yml ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── bors.toml ├── datasets ├── movies.json ├── nested_movies.json ├── small_movies.json ├── songs.csv ├── songs.ndjson └── songs_custom_delimiter.csv ├── docker-compose.yml ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── meilisearch.models.rst ├── meilisearch.rst └── modules.rst ├── meilisearch ├── __init__.py ├── _httprequests.py ├── _utils.py ├── client.py ├── config.py ├── errors.py ├── index.py ├── models │ ├── __init__.py │ ├── document.py │ ├── embedders.py │ ├── index.py │ ├── key.py │ └── task.py ├── py.typed ├── task.py └── version.py ├── pyproject.toml ├── tests ├── __init__.py ├── client │ ├── __init__.py │ ├── test_client.py │ ├── test_client_dumps.py │ ├── test_client_health_meilisearch.py │ ├── test_client_key_meilisearch.py │ ├── test_client_multi_search_meilisearch.py │ ├── test_client_stats_meilisearch.py │ ├── test_client_swap_meilisearch.py │ ├── test_client_task_meilisearch.py │ ├── test_client_tenant_token.py │ ├── test_client_update_document_by_functions.py │ ├── test_client_version_meilisearch.py │ ├── test_clinet_snapshots.py │ ├── test_http_requests.py │ └── test_version.py ├── common.py ├── conftest.py ├── errors │ ├── __init__.py │ ├── test_api_error_meilisearch.py │ ├── test_communication_error_meilisearch.py │ └── test_timeout_error_meilisearch.py ├── index │ ├── __init__.py │ ├── test_index.py │ ├── test_index_document_meilisearch.py │ ├── test_index_facet_search_meilisearch.py │ ├── test_index_search_meilisearch.py │ ├── test_index_stats_meilisearch.py │ ├── test_index_task_meilisearch.py │ └── test_index_wait_for_task.py ├── models │ ├── __init__.py │ ├── test_document.py │ └── test_index.py ├── settings │ ├── __init__.py │ ├── test_setting_faceting.py │ ├── test_settings.py │ ├── test_settings_dictionary_meilisearch.py │ ├── test_settings_displayed_attributes_meilisearch.py │ ├── test_settings_distinct_attribute_meilisearch.py │ ├── test_settings_embedders.py │ ├── test_settings_filterable_attributes_meilisearch.py │ ├── test_settings_localized_attributes_meilisearch.py │ ├── test_settings_pagination.py │ ├── test_settings_proximity_precision_meilisearch.py │ ├── test_settings_ranking_rules_meilisearch.py │ ├── test_settings_search_cutoff_meilisearch.py │ ├── test_settings_searchable_attributes_meilisearch.py │ ├── test_settings_sortable_attributes_meilisearch.py │ ├── test_settings_stop_words_meilisearch.py │ ├── test_settings_synonyms_meilisearch.py │ ├── test_settings_text_separators_meilisearch.py │ └── test_settings_typo_tolerance_meilisearch.py └── test_utils.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | charset = utf-8 12 | 13 | # 4 space indentation 14 | [*.{py,java,r,R}] 15 | indent_style = space 16 | indent_size = 4 17 | 18 | # 2 space indentation 19 | [*.{js,json,y{a,}ml,html,cwl}] 20 | indent_style = space 21 | indent_size = 2 22 | 23 | [*.{md,Rmd,rst}] 24 | trim_trailing_whitespace = false 25 | indent_style = space 26 | indent_size = 2 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Description of what the bug is about. 13 | 14 | **Expected behavior** 15 | What you expected to happen. 16 | 17 | **Current behavior** 18 | What happened. 19 | 20 | **Screenshots or Logs** 21 | If applicable, add screenshots or logs to help explain your problem. 22 | 23 | **Environment (please complete the following information):** 24 | - OS: [e.g. Debian GNU/Linux] 25 | - Meilisearch version: [e.g. v.0.20.0] 26 | - meilisearch-python version: [e.g v0.14.1] 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support questions & other 4 | url: https://discord.meilisearch.com/ 5 | about: Support is not handled here but on our Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request & Enhancement 💡 3 | about: Suggest a new idea for the project. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Brief explanation of the feature. 13 | 14 | **Basic example** 15 | If the proposal involves something new or a change, include a basic example. How would you use the feature? In which context? 16 | 17 | **Other** 18 | Any other things you want to add. 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - 'skip-changelog' 9 | - 'dependencies' 10 | rebase-strategy: disabled 11 | 12 | - package-ecosystem: pip 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | time: "04:00" 17 | open-pull-requests-limit: 20 18 | labels: 19 | - skip-changelog 20 | - dependencies 21 | rebase-strategy: disabled 22 | -------------------------------------------------------------------------------- /.github/release-draft-template.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🐍' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'skip-changelog' 5 | version-resolver: 6 | minor: 7 | labels: 8 | - 'breaking-change' 9 | default: patch 10 | categories: 11 | - title: '⚠️ Breaking changes' 12 | label: 'breaking-change' 13 | - title: '🚀 Enhancements' 14 | label: 'enhancement' 15 | - title: '🐛 Bug Fixes' 16 | label: 'bug' 17 | - title: '⚙️ Maintenance/misc' 18 | label: 19 | - 'maintenance' 20 | - 'documentation' 21 | - title: '🔒 Security' 22 | label: 'security' 23 | template: | 24 | $CHANGES 25 | 26 | Thanks again to $CONTRIBUTORS! 🎉 27 | no-changes-template: 'Changes are coming soon 😎' 28 | sort-direction: 'ascending' 29 | replacers: 30 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' 31 | replace: '' 32 | - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g' 33 | replace: '' 34 | - search: '/(?:and )?@bors(?:\[bot\])?,?/g' 35 | replace: '' 36 | - search: '/(?:and )?@meili-bors(?:\[bot\])?,?/g' 37 | replace: '' 38 | - search: '/(?:and )?@meili-bot,?/g' 39 | replace: '' 40 | - search: '/(?:and )?@meili-bot(?:\[bot\])?,?/g' 41 | replace: '' 42 | -------------------------------------------------------------------------------- /.github/scripts/check-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Checking if current tag matches the package version 4 | current_tag=$(echo $GITHUB_REF | cut -d '/' -f 3 | sed -r 's/^v//') 5 | file_tag=$(grep '__version__ =' meilisearch/version.py | cut -d '=' -f 2- | tr -d ' ' | tr -d '"' | tr -d ',') 6 | if [ "$current_tag" != "$file_tag" ]; then 7 | echo "Error: the current tag does not match the version in package file(s)." 8 | echo "$current_tag vs $file_tag" 9 | exit 1 10 | fi 11 | 12 | echo 'OK' 13 | exit 0 14 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | # Allows to run the workflow manually 8 | workflow_dispatch: 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | contents: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | env: 23 | CUSTOM_DOMAIN: python-sdk.meilisearch.com 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | lfs: true 33 | - name: Setup Github Pages 34 | uses: actions/configure-pages@v5 35 | - name: Install Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: "3.10" 39 | - name: Install pipenv 40 | run: | 41 | pip install pipx 42 | pipx install pipenv 43 | - name: Install dependencies 44 | run: | 45 | pipenv install --dev 46 | pipenv install sphinx 47 | pipenv install sphinx_rtd_theme 48 | - name: Build docs 49 | run: | 50 | pipenv run sphinx-apidoc -f -o docs meilisearch/ 51 | pipenv run sphinx-build docs ./docs/_build/html/ 52 | # CNAME file is required for GitHub pages custom domain 53 | - name: Create CNAME file 54 | run: | 55 | echo "$CUSTOM_DOMAIN" > ./docs/_build/html/CNAME 56 | echo "Created CNAME in ./docs/_build/html/: $CUSTOM_DOMAIN" 57 | - name: Upload artifacts 58 | uses: actions/upload-pages-artifact@v3 59 | with: 60 | path: "./docs/_build/html" 61 | deploy: 62 | needs: build 63 | environment: 64 | name: github-pages 65 | url: ${{ steps.deployment.outputs.page_url }} 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Deploy to GitHub Pages 69 | id: deployment 70 | uses: actions/deploy-pages@v4 71 | -------------------------------------------------------------------------------- /.github/workflows/pre-release-tests.yml: -------------------------------------------------------------------------------- 1 | # Testing the code base against the Meilisearch pre-releases 2 | name: Pre-Release Tests 3 | 4 | # Will only run for PRs and pushes to bump-meilisearch-v* 5 | on: 6 | push: 7 | branches: bump-meilisearch-v* 8 | pull_request: 9 | branches: bump-meilisearch-v* 10 | 11 | jobs: 12 | integration_tests: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 17 | name: integration-tests-against-rc 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | cache: "pipenv" 26 | - name: Install pipenv 27 | run: pipx install pipenv 28 | - name: Install dependencies 29 | run: pipenv install --dev --python=${{ matrix.python-version }} 30 | - name: Get the latest Meilisearch RC 31 | run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV 32 | - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker 33 | run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics 34 | - name: Test with pytest 35 | run: pipenv run pytest 36 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: "3.9" 17 | cache: "pipenv" 18 | - name: Check release validity 19 | run: sh .github/scripts/check-release.sh 20 | - name: Install pipenv 21 | run: pipx install pipenv 22 | - name: Install dependencies 23 | run: | 24 | pipenv install 25 | pipenv run pip3 install build setuptools wheel twine 26 | - name: Build and publish 27 | env: 28 | TWINE_USERNAME: __token__ 29 | TWINE_PASSWORD: "pypi-${{ secrets.PYPI_API_TOKEN }}" 30 | run: | 31 | pipenv run python3 -m build 32 | pipenv run twine upload dist/* 33 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | with: 14 | config-name: release-draft-template.yml 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # trying and staging branches are for BORS config 7 | branches: 8 | - trying 9 | - staging 10 | - main 11 | 12 | jobs: 13 | integration_tests: 14 | # Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR) 15 | # Will still run for each push to bump-meilisearch-v* 16 | if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v') 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 21 | name: integration-tests 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | cache: "pipenv" 30 | - name: Install pipenv 31 | run: pipx install pipenv 32 | - name: Install dependencies 33 | run: pipenv install --dev --python=${{ matrix.python-version }} 34 | - name: Meilisearch (latest version) setup with Docker 35 | run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --no-analytics --master-key=masterKey 36 | - name: Test with pytest 37 | run: pipenv run pytest --cov-report=xml 38 | 39 | pylint: 40 | name: pylint 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Set up Python 3.9 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: "3.9" 48 | cache: "pipenv" 49 | - name: Install pipenv 50 | run: pipx install pipenv 51 | - name: Install dependencies 52 | run: pipenv install --dev --python=${{ matrix.python-version }} 53 | - name: Linter with pylint 54 | run: pipenv run pylint meilisearch tests 55 | 56 | mypy: 57 | name: mypy 58 | runs-on: ubuntu-latest 59 | steps: 60 | - uses: actions/checkout@v4 61 | - name: Set up Python 3.9 62 | uses: actions/setup-python@v5 63 | with: 64 | python-version: 3.9 65 | cache: "pipenv" 66 | - name: Install pipenv 67 | run: pipx install pipenv 68 | - name: Install dependencies 69 | run: pipenv install --dev --python=${{ matrix.python-version }} 70 | - name: mypy type check 71 | run: pipenv run mypy meilisearch 72 | 73 | black: 74 | name: black 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | - name: Set up Python 3.9 79 | uses: actions/setup-python@v5 80 | with: 81 | python-version: 3.9 82 | cache: "pipenv" 83 | - name: Install pipenv 84 | run: pipx install pipenv 85 | - name: Install dependencies 86 | run: pipenv install --dev --python=${{ matrix.python-version }} 87 | - name: black 88 | run: pipenv run black meilisearch tests --check 89 | 90 | isort: 91 | name: isort 92 | runs-on: ubuntu-latest 93 | steps: 94 | - uses: actions/checkout@v4 95 | - name: Set up Python 3.9 96 | uses: actions/setup-python@v5 97 | with: 98 | python-version: 3.9 99 | cache: "pipenv" 100 | - name: Install pipenv 101 | run: pipx install pipenv 102 | - name: Install dependencies 103 | run: pipenv install --dev --python=${{ matrix.python-version }} 104 | - name: isort 105 | run: pipenv run isort meilisearch tests --check-only 106 | 107 | yaml-lint: 108 | name: Yaml linting check 109 | runs-on: ubuntu-latest 110 | steps: 111 | - uses: actions/checkout@v4 112 | - name: Yaml lint check 113 | uses: ibiqlik/action-yamllint@v3 114 | with: 115 | config_file: .yamllint.yml 116 | -------------------------------------------------------------------------------- /.github/workflows/update-version.yml: -------------------------------------------------------------------------------- 1 | name: Update library version 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | new_version: 7 | description: 'The new version (vX.Y.Z)' 8 | required: true 9 | 10 | env: 11 | NEW_VERSION: ${{ github.event.inputs.new_version }} 12 | NEW_BRANCH: update-version-${{ github.event.inputs.new_version }} 13 | GH_TOKEN: ${{ secrets.MEILI_BOT_GH_PAT }} 14 | 15 | jobs: 16 | update-version: 17 | name: Update version in version.py 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Update version.py file 22 | run: | 23 | raw_new_version=$(echo $NEW_VERSION | cut -d 'v' -f 2) 24 | new_string="__version__ = \"$raw_new_version\"" 25 | sed -i "s/^__version__ = .*/$new_string/" meilisearch/version.py 26 | - name: Commit and push the changes to the ${{ env.NEW_BRANCH }} branch 27 | uses: EndBug/add-and-commit@v9 28 | with: 29 | message: "Update version for the next release (${{ env.NEW_VERSION }}) in version.py" 30 | new_branch: ${{ env.NEW_BRANCH }} 31 | - name: Create the PR pointing to ${{ github.ref_name }} 32 | run: | 33 | gh pr create \ 34 | --title "Update version for the next release ($NEW_VERSION)" \ 35 | --body '⚠️ This PR is automatically generated. Check the new version is the expected one.' \ 36 | --label 'skip-changelog' \ 37 | --base $GITHUB_REF_NAME 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### VirtualEnv template 3 | # Virtualenv 4 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 5 | .Python 6 | [Bb]in 7 | [Ii]nclude 8 | [Ll]ib 9 | [Ll]ib64 10 | [Ll]ocal 11 | pyvenv.cfg 12 | .venv 13 | pip-selfcheck.json 14 | 15 | ### Python template 16 | # Byte-compiled / optimized / DLL files 17 | __pycache__/ 18 | *.py[cod] 19 | *$py.class 20 | 21 | # C extensions 22 | *.so 23 | 24 | # Distribution / packaging 25 | .Python 26 | build/ 27 | develop-eggs/ 28 | dist/ 29 | downloads/ 30 | eggs/ 31 | .eggs/ 32 | lib/ 33 | lib64/ 34 | parts/ 35 | sdist/ 36 | var/ 37 | wheels/ 38 | pip-wheel-metadata/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | 69 | # Translations 70 | *.mo 71 | *.pot 72 | 73 | # Django stuff: 74 | *.log 75 | local_settings.py 76 | db.sqlite3 77 | db.sqlite3-journal 78 | 79 | # Flask stuff: 80 | instance/ 81 | .webassets-cache 82 | 83 | # Scrapy stuff: 84 | .scrapy 85 | 86 | # Sphinx documentation 87 | docs/_build/ 88 | 89 | # PyBuilder 90 | target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | 135 | # mkdocs documentation 136 | /site 137 | 138 | # mypy 139 | .mypy_cache/ 140 | .dmypy.json 141 | dmypy.json 142 | 143 | # Pyre type checker 144 | .pyre/ 145 | 146 | .idea 147 | html/ 148 | 149 | .vscode 150 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | rules: 3 | line-length: disable 4 | document-start: disable 5 | truthy: disable 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you for contributing to Meilisearch! The goal of this document is to provide everything you need to know in order to contribute to Meilisearch and its different integrations. 4 | 5 | - [Assumptions](#assumptions) 6 | - [How to Contribute](#how-to-contribute) 7 | - [Development Workflow](#development-workflow) 8 | - [Git Guidelines](#git-guidelines) 9 | - [Release Process (for the internal team only)](#release-process-for-the-internal-team-only) 10 | 11 | ## Assumptions 12 | 13 | 1. **You're familiar with [GitHub](https://github.com) and the [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)(PR) workflow.** 14 | 2. **You've read the Meilisearch [documentation](https://www.meilisearch.com/docs) and the [README](/README.md).** 15 | 3. **You know about the [Meilisearch community](https://discord.com/invite/meilisearch). Please use this for help.** 16 | 17 | ## How to Contribute 18 | 19 | 1. Make sure that the contribution you want to make is explained or detailed in a GitHub issue! Find an [existing issue](https://github.com/meilisearch/meilisearch-python/issues/) or [open a new one](https://github.com/meilisearch/meilisearch-python/issues/new). 20 | 2. Once done, [fork the meilisearch-python repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) in your own GitHub account. Ask a maintainer if you want your issue to be checked before making a PR. 21 | 3. [Create a new Git branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository). 22 | 4. Review the [Development Workflow](#development-workflow) section that describes the steps to maintain the repository. 23 | 5. Make the changes on your branch. 24 | 6. [Submit the branch as a PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) pointing to the `main` branch of the main meilisearch-python repository. A maintainer should comment and/or review your Pull Request within a few days. Although depending on the circumstances, it may take longer.
25 | We do not enforce a naming convention for the PRs, but **please use something descriptive of your changes**, having in mind that the title of your PR will be automatically added to the next [release changelog](https://github.com/meilisearch/meilisearch-python/releases/). 26 | 27 | ## Development Workflow 28 | 29 | ### Setup 30 | 31 | You can set up your local environment natively or using `docker`, check out the [`docker-compose.yml`](/docker-compose.yml). 32 | 33 | Example of running all the checks with docker: 34 | ```bash 35 | docker-compose run --rm package bash -c "pipenv install --dev && pipenv run mypy meilisearch && pipenv run pylint meilisearch tests && pipenv run pytest tests" 36 | ``` 37 | 38 | To install dependencies: 39 | 40 | ```bash 41 | pipenv install --dev 42 | ``` 43 | 44 | ### Tests and Linter 45 | 46 | Each PR should pass the tests, mypy type checking, and the linter to be accepted. 47 | Your PR also needs to be formatted using black and isort. 48 | 49 | ```bash 50 | # Tests 51 | curl -L https://install.meilisearch.com | sh # download Meilisearch 52 | ./meilisearch --master-key=masterKey --no-analytics # run Meilisearch 53 | pipenv run pytest tests 54 | # MyPy 55 | pipenv run mypy meilisearch 56 | # Linter 57 | pipenv run pylint meilisearch tests 58 | # Black 59 | pipenv run black meilisearch tests 60 | # Isort 61 | pipenv run isort meilisearch tests 62 | ``` 63 | 64 | Optionally tox can be used to run test on all supported version of Python, mypy, and linting. 65 | 66 | ```bash 67 | docker pull getmeili/meilisearch:latest # Fetch the latest version of Meilisearch image from Docker Hub 68 | docker run -p 7700:7700 getmeili/meilisearch:latest meilisearch --master-key=masterKey --no-analytics 69 | pipenv run tox 70 | ``` 71 | 72 | To check if your `yaml` files are correctly formatted, you need to [install yamllint](https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint) and then run `yamllint .` 73 | 74 | ### Want to debug? 75 | 76 | Import `pdb` in your file and use it: 77 | 78 | ```python 79 | import pdb 80 | 81 | ... 82 | pdb.set_trace() # create a break point 83 | ... 84 | ``` 85 | 86 | More information [about pdb](https://docs.python.org/3/library/pdb.html). 87 | 88 | ## Git Guidelines 89 | 90 | ### Git Branches 91 | 92 | All changes must be made in a branch and submitted as PR. 93 | We do not enforce any branch naming style, but please use something descriptive of your changes. 94 | 95 | ### Git Commits 96 | 97 | As minimal requirements, your commit message should: 98 | - be capitalized 99 | - not finish by a dot or any other punctuation character (!,?) 100 | - start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message. 101 | e.g.: "Fix the home page button" or "Add more tests for create_index method" 102 | 103 | We don't follow any other convention, but if you want to use one, we recommend [this one](https://chris.beams.io/posts/git-commit/). 104 | 105 | ### GitHub Pull Requests 106 | 107 | Some notes on GitHub PRs: 108 | 109 | - [Convert your PR as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) if your changes are a work in progress: no one will review it until you pass your PR as ready for review.
110 | The draft PR can be very useful if you want to show that you are working on something and make your work visible. 111 | - The branch related to the PR must be **up-to-date with `main`** before merging. Fortunately, this project [integrates a bot](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md) to automatically enforce this requirement without the PR author having to do it manually. 112 | - All PRs must be reviewed and approved by at least one maintainer. 113 | - The PR title should be accurate and descriptive of the changes. The title of the PR will be indeed automatically added to the next [release changelogs](https://github.com/meilisearch/meilisearch-python/releases/). 114 | 115 | ## Release Process (for the internal team only) 116 | 117 | Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/). 118 | 119 | ### Automation to Rebase and Merge the PRs 120 | 121 | This project integrates a bot that helps us manage pull requests merging.
122 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md)._ 123 | 124 | ### Automated Changelogs 125 | 126 | This project integrates a tool to create automated changelogs.
127 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/release-drafter.md)._ 128 | 129 | ### How to Publish the Release 130 | 131 | ⚠️ Before doing anything, make sure you got through the guide about [Releasing an Integration](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md). 132 | 133 | Use [our automation](https://github.com/meilisearch/meilisearch-python/actions/workflows/update-version.yml) to update the version: click on `Run workflow`, and fill the appropriate version before validating. A PR updating the version in the [`meilisearch/version.py`](/meilisearch/version.py) file will be created. 134 | 135 | Or do it manually: 136 | 137 | Make a PR modifying the file [`meilisearch/version.py`](/meilisearch/version.py) with the right version. 138 | 139 | ```python 140 | __version__ = "X.X.X" 141 | ``` 142 | 143 | Once the changes are merged on `main`, you can publish the current draft release via the [GitHub interface](https://github.com/meilisearch/meilisearch-python/releases): on this page, click on `Edit` (related to the draft release) > update the description (be sure you apply [these recommendations](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md#writting-the-release-description)) > when you are ready, click on `Publish release`. 144 | 145 | GitHub Actions will be triggered and push the package to [PyPI](https://pypi.org/project/meilisearch). 146 | 147 |
148 | 149 | Thank you again for reading this through. We can not wait to begin to work with you if you make your way through this contributing guide ❤️ 150 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-buster 2 | 3 | COPY Pipfile . 4 | COPY Pipfile.lock . 5 | 6 | RUN apt-get update -y 7 | 8 | # Install pipenv and compilation dependencies 9 | RUN pip3 install pipenv 10 | RUN pipenv install --dev 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2025 Meili SAS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | mypy = "*" 8 | pylint = "*" 9 | pytest = "*" 10 | pdoc3 = "*" 11 | pytest-ordering = "*" 12 | tox = "*" 13 | tox-pipenv = "*" 14 | types-requests = "*" 15 | black = "*" 16 | isort = "*" 17 | exceptiongroup = {version = "*", markers="python_version < '3.11'"} 18 | tomli = {version = "*", markers="python_version < '3.11'"} 19 | wrapt = {version = "*", markers="python_version < '3.11'"} 20 | dill = {version = "*"} 21 | pytest-cov = "*" 22 | 23 | [packages] 24 | requests = "*" 25 | camel-converter = {version = "*", extras = ["pydantic"]} 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Meilisearch-Python 3 |

4 | 5 |

Meilisearch Python

6 | 7 |

8 | Meilisearch | 9 | Meilisearch Cloud | 10 | Documentation | 11 | Discord | 12 | Roadmap | 13 | Website | 14 | FAQ 15 |

16 | 17 |

18 | PyPI version 19 | Test Status 20 | License 21 | Bors enabled 22 |

23 | 24 |

⚡ The Meilisearch API client written for Python 🐍

25 | 26 | **Meilisearch Python** is the Meilisearch API client for Python developers. 27 | 28 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) 29 | 30 | ## Table of Contents 31 | 32 | - [📖 Documentation](#-documentation) 33 | - [🔧 Installation](#-installation) 34 | - [🚀 Getting started](#-getting-started) 35 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch) 36 | - [💡 Learn more](#-learn-more) 37 | - [⚙️ Contributing](#️-contributing) 38 | 39 | ## 📖 Documentation 40 | 41 | To learn more about Meilisearch Python, refer to the in-depth [Meilisearch Python documentation](https://meilisearch.github.io/meilisearch-python/). To learn more about Meilisearch in general, refer to our [documentation](https://www.meilisearch.com/docs/learn/getting_started/quick_start) or our [API reference](https://www.meilisearch.com/docs/reference/api/overview). 42 | 43 | ## 🔧 Installation 44 | 45 | **Note**: Python 3.8+ is required. 46 | 47 | With `pip3` in command line: 48 | 49 | ```bash 50 | pip3 install meilisearch 51 | ``` 52 | 53 | ### Run Meilisearch 54 | 55 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-python). 56 | 57 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-python) our fast, open-source search engine on your own infrastructure. 58 | 59 | ## 🚀 Getting started 60 | 61 | #### Add Documents 62 | 63 | ```python 64 | import meilisearch 65 | 66 | client = meilisearch.Client('http://127.0.0.1:7700', 'masterKey') 67 | 68 | # An index is where the documents are stored. 69 | index = client.index('movies') 70 | 71 | documents = [ 72 | { 'id': 1, 'title': 'Carol', 'genres': ['Romance', 'Drama'] }, 73 | { 'id': 2, 'title': 'Wonder Woman', 'genres': ['Action', 'Adventure'] }, 74 | { 'id': 3, 'title': 'Life of Pi', 'genres': ['Adventure', 'Drama'] }, 75 | { 'id': 4, 'title': 'Mad Max: Fury Road', 'genres': ['Adventure', 'Science Fiction'] }, 76 | { 'id': 5, 'title': 'Moana', 'genres': ['Fantasy', 'Action']}, 77 | { 'id': 6, 'title': 'Philadelphia', 'genres': ['Drama'] }, 78 | ] 79 | 80 | # If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. 81 | index.add_documents(documents) # => { "uid": 0 } 82 | ``` 83 | 84 | With the task `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-tasks). 85 | 86 | #### Basic Search 87 | 88 | ```python 89 | # Meilisearch is typo-tolerant: 90 | index.search('caorl') 91 | ``` 92 | 93 | Output: 94 | 95 | ```json 96 | { 97 | "hits": [ 98 | { 99 | "id": 1, 100 | "title": "Carol", 101 | "genre": ["Romance", "Drama"] 102 | } 103 | ], 104 | "offset": 0, 105 | "limit": 20, 106 | "processingTimeMs": 1, 107 | "query": "caorl" 108 | } 109 | ``` 110 | 111 | #### Custom Search 112 | 113 | All the supported options are described in the [search parameters](https://www.meilisearch.com/docs/reference/api/search#search-parameters) 114 | 115 | ```python 116 | index.search( 117 | 'phil', 118 | { 119 | 'attributesToHighlight': ['*'], 120 | } 121 | ) 122 | ``` 123 | 124 | JSON output: 125 | 126 | ```json 127 | { 128 | "hits": [ 129 | { 130 | "id": 6, 131 | "title": "Philadelphia", 132 | "_formatted": { 133 | "id": 6, 134 | "title": "Philadelphia", 135 | "genre": ["Drama"] 136 | } 137 | } 138 | ], 139 | "offset": 0, 140 | "limit": 20, 141 | "processingTimeMs": 0, 142 | "query": "phil" 143 | } 144 | ``` 145 | 146 | #### Hybrid Search 147 | 148 | Hybrid search combines traditional keyword search with semantic search for more relevant results. You need to have an embedder configured in your index settings to use this feature. 149 | 150 | ```python 151 | # Using hybrid search with the search method 152 | index.search( 153 | 'action movie', 154 | { 155 | "hybrid": {"semanticRatio": 0.5, "embedder": "default"} 156 | } 157 | ) 158 | ``` 159 | 160 | The `semanticRatio` parameter (between 0 and 1) controls the balance between keyword search and semantic search: 161 | - 0: Only keyword search 162 | - 1: Only semantic search 163 | - Values in between: A mix of both approaches 164 | 165 | The `embedder` parameter specifies which configured embedder to use for the semantic search component. 166 | 167 | #### Custom Search With Filters 168 | 169 | If you want to enable filtering, you must add your attributes to the `filterableAttributes` index setting. 170 | 171 | ```py 172 | index.update_filterable_attributes([ 173 | 'id', 174 | 'genres' 175 | ]) 176 | ``` 177 | 178 | #### Custom Serializer for documents 179 | 180 | If your documents contain fields that the Python JSON serializer does not know how to handle you 181 | can use your own custom serializer. 182 | 183 | ```py 184 | from datetime import datetime 185 | from json import JSONEncoder 186 | from uuid import uuid4 187 | 188 | 189 | class CustomEncoder(JSONEncoder): 190 | def default(self, o): 191 | if isinstance(o, (UUID, datetime)): 192 | return str(o) 193 | 194 | # Let the base class default method raise the TypeError 195 | return super().default(o) 196 | 197 | 198 | documents = [ 199 | {"id": uuid4(), "title": "test 1", "when": datetime.now()}, 200 | {"id": uuid4(), "title": "Test 2", "when": datetime.now()}, 201 | ] 202 | index.add_documents(documents, serializer=CustomEncoder) 203 | ``` 204 | 205 | You only need to perform this operation once. 206 | 207 | Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-tasks). 208 | 209 | Then, you can perform the search: 210 | 211 | ```py 212 | index.search( 213 | 'wonder', 214 | { 215 | 'filter': ['id > 1 AND genres = Action'] 216 | } 217 | ) 218 | ``` 219 | 220 | ```json 221 | { 222 | "hits": [ 223 | { 224 | "id": 2, 225 | "title": "Wonder Woman", 226 | "genres": ["Action", "Adventure"] 227 | } 228 | ], 229 | "offset": 0, 230 | "limit": 20, 231 | "estimatedTotalHits": 1, 232 | "processingTimeMs": 0, 233 | "query": "wonder" 234 | } 235 | ``` 236 | 237 | ## 🤖 Compatibility with Meilisearch 238 | 239 | This package guarantees compatibility with [version v1.2 and above of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-python/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. 240 | 241 | ## 💡 Learn more 242 | 243 | The following sections in our main documentation website may interest you: 244 | 245 | - **Manipulate documents**: see the [API references](https://www.meilisearch.com/docs/reference/api/documents) or read more about [documents](https://www.meilisearch.com/docs/learn/core_concepts/documents). 246 | - **Search**: see the [API references](https://www.meilisearch.com/docs/reference/api/search). 247 | - **Manage the indexes**: see the [API references](https://www.meilisearch.com/docs/reference/api/indexes) or read more about [indexes](https://www.meilisearch.com/docs/learn/core_concepts/indexes). 248 | - **Configure the index settings**: see the [API references](https://www.meilisearch.com/docs/reference/api/settings) or follow our guide on [settings parameters](https://www.meilisearch.com/docs/learn/configuration/settings). 249 | 250 | ## ⚙️ Contributing 251 | 252 | Any new contribution is more than welcome in this project! 253 | 254 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions! 255 | 256 |
257 | 258 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository. 259 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | 'pylint', 3 | 'mypy', 4 | 'integration-tests (3.9)', 5 | 'integration-tests (3.10)', 6 | 'integration-tests (3.11)', 7 | 'integration-tests (3.12)', 8 | 'integration-tests (3.13)', 9 | ] 10 | # 1 hour timeout 11 | timeout-sec = 3600 12 | -------------------------------------------------------------------------------- /datasets/nested_movies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "title": "Pride and Prejudice", 5 | "info": { 6 | "comment": "A great book", 7 | "reviewNb": 50 8 | } 9 | }, 10 | { 11 | "id": 2, 12 | "title": "Le Petit Prince", 13 | "info": { 14 | "comment": "A french book", 15 | "reviewNb": 600 16 | } 17 | }, 18 | { 19 | "id": 3, 20 | "title": "Le Rouge et le Noir", 21 | "info": { 22 | "comment": "Another french book", 23 | "reviewNb": 700 24 | } 25 | }, 26 | { 27 | "id": 4, 28 | "title": "Alice In Wonderland", 29 | "comment": "A weird book", 30 | "info": { 31 | "comment": "A weird book", 32 | "reviewNb": 800 33 | } 34 | }, 35 | { 36 | "id": 5, 37 | "title": "The Hobbit", 38 | "info": { 39 | "comment": "An awesome book", 40 | "reviewNb": 900 41 | } 42 | }, 43 | { 44 | "id": 6, 45 | "title": "Harry Potter and the Half-Blood Prince", 46 | "info": { 47 | "comment": "The best book", 48 | "reviewNb": 1000 49 | } 50 | }, 51 | { "id": 7, 52 | "title": "The Hitchhiker's Guide to the Galaxy" 53 | } 54 | ] 55 | -------------------------------------------------------------------------------- /datasets/songs_custom_delimiter.csv: -------------------------------------------------------------------------------- 1 | id;title;album;artist;genre;country;released;duration;released-timestamp;duration-float 2 | 702481615;Armatage Shanks;Dookie: The Ultimate Critical Review;Green Day;Rock;Europe;2005;;1104537600; 3 | 888221515;Old Folks;Six Classic Albums Plus Bonus Tracks;Harold Land;Jazz;Europe;2013;6:36;1356998400;6.36 4 | 1382413601;คำขอร้อง;สำเนียงคนจันทร์ / เอาเถอะถ้าเห็นเขาดีกว่า;อิทธิพล บำรุงกุล;"Folk; World; & Country";Thailand;;;; 5 | 190889300;Track 1;Summer Breeze;Dreas;Funk / Soul;US;2008;18:56;1199145600;18.56 6 | 813645611;Slave (Alternative Version);Honky Château;Elton John;Rock;Europe;;2:53;;2.5300000000000002 7 | 394018506;Sex & Geld;Trackz Für Den Index;Mafia Clikk;Hip Hop;Germany;2006;5:02;1136073600;5.02 8 | 1522481803;Pisciaunella;Don Pepp U Pacce;Giovanni Russo (2);"Folk; World; & Country";Italy;1980;;315532800; 9 | 862296713;不知;Kiss 2001 Hong Kong Live Concert;Various;Electronic;Hong Kong;2002-04-13;;1018656000; 10 | 467946423;Rot;Be Quick Or Be Dead Vol. 3;Various;Electronic;Serbia;2013-06-20;1:00;1371686400;1 11 | 1323854803;"Simulation Project 1; ツキハナ「Moonflower」";Unlimited Dream Company;Amun Dragoon;Electronic;US;2018-04-10;2:44;1523318400;2.44 12 | 235115704;Doctor Vine;The Big F;The Big F;Rock;US;1989;5:29;599616000;5.29 13 | 249025232;"Ringel; Ringel; Reihe";Kinderlieder ABC - Der Bielefelder Kinderchor Singt 42 Lieder Von A-Z;Der Bielefelder Kinderchor;Children's;Germany;1971;;31536000; 14 | 710094000;Happy Safari = Safari Feliz;Safari Swings Again = El Safari Sigue En Su Swing;Bert Kaempfert & His Orchestra;Jazz;Argentina;1977;2:45;220924800;2.45 15 | 538632700;Take Me Up;Spring;Various;Electronic;US;2000;3:06;946684800;3.06 16 | 1556505508;Doin To Me ( Radio Version );Say My Name;Netta Dogg;Hip Hop;US;2005;;1104537600; 17 | 1067031900;Concerto For Balloon & Orchestra / Concerto For Synthesizer & Orchestra;Concerto For Balloon & Orchestra And Three Overtures;Stanyan String & Wind Ensemble;Classical;US;1977;;220924800; 18 | 137251914;"I Love The Nightlife (Disco 'Round) (Real Rapino 7"" Mix)";The Adventures Of Priscilla: Queen Of The Desert - Original Motion Picture Soundtrack;Various;Stage & Screen;US;1994;3:31;757382400;3.31 19 | 554983904;Walking On The Moon;Certifiable (Live In Buenos Aires);The Police;Rock;Malaysia;2008-11-00;;1225497600; 20 | 557616002;Two Soldiers;Jerry Garcia / David Grisman;David Grisman;"Folk; World; & Country";US;2014-04-00;4:24;1396310400;4.24 21 | 878936809;When You Gonna Learn;Live At Firenze 93;Jamiroquai;Funk / Soul;France;2004;13:01;1072915200;13.01 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | package: 5 | build: . 6 | tty: true 7 | stdin_open: true 8 | working_dir: /home/package 9 | environment: 10 | - MEILISEARCH_URL=http://meilisearch:7700 11 | depends_on: 12 | - meilisearch 13 | links: 14 | - meilisearch 15 | volumes: 16 | - ./:/home/package 17 | 18 | meilisearch: 19 | image: getmeili/meilisearch:latest 20 | ports: 21 | - "7700" 22 | environment: 23 | - MEILI_MASTER_KEY=masterKey 24 | - MEILI_NO_ANALYTICS=true 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.abspath("..")) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "meilisearch-python" 21 | copyright = "2019, Meili SAS" 22 | author = "Charlotte Vermandel" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.napoleon", 33 | "sphinx.ext.viewcode", 34 | ] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ["_templates"] 38 | 39 | # List of patterns, relative to source directory, that match files and 40 | # directories to ignore when looking for source files. 41 | # This pattern also affects html_static_path and html_extra_path. 42 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 43 | 44 | 45 | # -- Options for HTML output ------------------------------------------------- 46 | 47 | # The theme to use for HTML and HTML Help pages. See the documentation for 48 | # a list of builtin themes. 49 | # 50 | html_theme = "sphinx_rtd_theme" 51 | # html_theme = 'alabaster' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = [] 57 | 58 | # This value contains a list of modules to be mocked up. 59 | autodoc_mock_imports = ["camel_converter"] 60 | 61 | html_title = 'Meilisearch Python | Documentation' 62 | 63 | # Add Fathom analytics script 64 | html_js_files = [ 65 | ("https://cdn.usefathom.com/script.js", { "data-site": "QNBPJXIV", "defer": "defer" }) 66 | ] 67 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. meilisearch-python documentation master file, created by 2 | sphinx-quickstart on Sun Oct 2 13:24:48 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to meilisearch-python's documentation! 7 | ============================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | .. include:: modules.rst 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/meilisearch.models.rst: -------------------------------------------------------------------------------- 1 | meilisearch.models package 2 | ========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | meilisearch.models.client module 8 | -------------------------------- 9 | 10 | .. automodule:: meilisearch.models.client 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | meilisearch.models.document module 16 | ---------------------------------- 17 | 18 | .. automodule:: meilisearch.models.document 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | meilisearch.models.index module 24 | ------------------------------- 25 | 26 | .. automodule:: meilisearch.models.index 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | meilisearch.models.ressource module 32 | ----------------------------------- 33 | 34 | .. automodule:: meilisearch.models.ressource 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | meilisearch.models.task module 40 | ------------------------------ 41 | 42 | .. automodule:: meilisearch.models.task 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: meilisearch.models 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /docs/meilisearch.rst: -------------------------------------------------------------------------------- 1 | meilisearch package 2 | =================== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | meilisearch.models 11 | 12 | Submodules 13 | ---------- 14 | 15 | meilisearch.client module 16 | ------------------------- 17 | 18 | .. automodule:: meilisearch.client 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | meilisearch.config module 24 | ------------------------- 25 | 26 | .. automodule:: meilisearch.config 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | meilisearch.errors module 32 | ------------------------- 33 | 34 | .. automodule:: meilisearch.errors 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | meilisearch.index module 40 | ------------------------ 41 | 42 | .. automodule:: meilisearch.index 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | meilisearch.task module 48 | ----------------------- 49 | 50 | .. automodule:: meilisearch.task 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | meilisearch.version module 56 | -------------------------- 57 | 58 | .. automodule:: meilisearch.version 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | Module contents 64 | --------------- 65 | 66 | .. automodule:: meilisearch 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | meilisearch 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | meilisearch 8 | -------------------------------------------------------------------------------- /meilisearch/__init__.py: -------------------------------------------------------------------------------- 1 | from meilisearch.client import Client as Client # pylint: disable=useless-import-alias 2 | -------------------------------------------------------------------------------- /meilisearch/_httprequests.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from functools import lru_cache 5 | from typing import Any, Callable, List, Mapping, Optional, Sequence, Tuple, Type, Union 6 | 7 | import requests 8 | 9 | from meilisearch.config import Config 10 | from meilisearch.errors import ( 11 | MeilisearchApiError, 12 | MeilisearchCommunicationError, 13 | MeilisearchTimeoutError, 14 | ) 15 | from meilisearch.models.index import ProximityPrecision 16 | from meilisearch.version import qualified_version 17 | 18 | 19 | class HttpRequests: 20 | def __init__(self, config: Config, custom_headers: Optional[Mapping[str, str]] = None) -> None: 21 | self.config = config 22 | self.headers = { 23 | "Authorization": f"Bearer {self.config.api_key}", 24 | "User-Agent": _build_user_agent(config.client_agents), 25 | } 26 | 27 | if custom_headers is not None: 28 | self.headers.update(custom_headers) 29 | 30 | def send_request( 31 | self, 32 | http_method: Callable, 33 | path: str, 34 | body: Optional[ 35 | Union[ 36 | Mapping[str, Any], 37 | Sequence[Mapping[str, Any]], 38 | List[str], 39 | bytes, 40 | str, 41 | int, 42 | ProximityPrecision, 43 | ] 44 | ] = None, 45 | content_type: Optional[str] = None, 46 | *, 47 | serializer: Optional[Type[json.JSONEncoder]] = None, 48 | ) -> Any: 49 | if content_type: 50 | self.headers["Content-Type"] = content_type 51 | try: 52 | request_path = self.config.url + "/" + path 53 | if http_method.__name__ == "get": 54 | request = http_method( 55 | request_path, 56 | timeout=self.config.timeout, 57 | headers=self.headers, 58 | ) 59 | elif isinstance(body, bytes): 60 | request = http_method( 61 | request_path, 62 | timeout=self.config.timeout, 63 | headers=self.headers, 64 | data=body, 65 | ) 66 | else: 67 | serialize_body = isinstance(body, dict) or body 68 | data = ( 69 | json.dumps(body, cls=serializer) 70 | if serialize_body 71 | else "" if body == "" else "null" 72 | ) 73 | 74 | request = http_method( 75 | request_path, timeout=self.config.timeout, headers=self.headers, data=data 76 | ) 77 | return self.__validate(request) 78 | 79 | except requests.exceptions.Timeout as err: 80 | raise MeilisearchTimeoutError(str(err)) from err 81 | except requests.exceptions.ConnectionError as err: 82 | raise MeilisearchCommunicationError(str(err)) from err 83 | 84 | def get(self, path: str) -> Any: 85 | return self.send_request(requests.get, path) 86 | 87 | def post( 88 | self, 89 | path: str, 90 | body: Optional[ 91 | Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], bytes, str] 92 | ] = None, 93 | content_type: Optional[str] = "application/json", 94 | *, 95 | serializer: Optional[Type[json.JSONEncoder]] = None, 96 | ) -> Any: 97 | return self.send_request(requests.post, path, body, content_type, serializer=serializer) 98 | 99 | def patch( 100 | self, 101 | path: str, 102 | body: Optional[ 103 | Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], bytes, str] 104 | ] = None, 105 | content_type: Optional[str] = "application/json", 106 | ) -> Any: 107 | return self.send_request(requests.patch, path, body, content_type) 108 | 109 | def put( 110 | self, 111 | path: str, 112 | body: Optional[ 113 | Union[ 114 | Mapping[str, Any], 115 | Sequence[Mapping[str, Any]], 116 | List[str], 117 | bytes, 118 | str, 119 | int, 120 | ProximityPrecision, 121 | ] 122 | ] = None, 123 | content_type: Optional[str] = "application/json", 124 | *, 125 | serializer: Optional[Type[json.JSONEncoder]] = None, 126 | ) -> Any: 127 | return self.send_request(requests.put, path, body, content_type, serializer=serializer) 128 | 129 | def delete( 130 | self, 131 | path: str, 132 | body: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str]]] = None, 133 | ) -> Any: 134 | return self.send_request(requests.delete, path, body) 135 | 136 | @staticmethod 137 | def __to_json(request: requests.Response) -> Any: 138 | if request.content == b"": 139 | return request 140 | return request.json() 141 | 142 | @staticmethod 143 | def __validate(request: requests.Response) -> Any: 144 | try: 145 | request.raise_for_status() 146 | return HttpRequests.__to_json(request) 147 | except requests.exceptions.HTTPError as err: 148 | raise MeilisearchApiError(str(err), request) from err 149 | 150 | 151 | @lru_cache(maxsize=1) 152 | def _build_user_agent(client_agents: Optional[Tuple[str, ...]] = None) -> str: 153 | user_agent = qualified_version() 154 | if not client_agents: 155 | return user_agent 156 | 157 | return f"{user_agent};{';'.join(client_agents)}" 158 | -------------------------------------------------------------------------------- /meilisearch/_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from functools import lru_cache 3 | from typing import Union 4 | 5 | import pydantic 6 | 7 | 8 | @lru_cache(maxsize=1) 9 | def is_pydantic_2() -> bool: 10 | try: 11 | # __version__ was added with Pydantic 2 so we know if this errors the version is < 2. 12 | # Still check the version as a fail safe incase __version__ gets added to verion 1. 13 | if int(pydantic.__version__[:1]) >= 2: # type: ignore[attr-defined] 14 | return True 15 | 16 | # Raise an AttributeError to match the AttributeError on __version__ because in either 17 | # case we need to get to the same place. 18 | raise AttributeError # pragma: no cover 19 | except AttributeError: # pragma: no cover 20 | return False 21 | 22 | 23 | def iso_to_date_time(iso_date: Union[datetime, str, None]) -> Union[datetime, None]: 24 | """Handle conversion of iso string to datetime. 25 | 26 | The microseconds from Meilisearch are sometimes too long for python to convert so this 27 | strips off the last digits to shorten it when that happens. 28 | """ 29 | if not iso_date: 30 | return None 31 | 32 | if isinstance(iso_date, datetime): 33 | return iso_date 34 | 35 | try: 36 | return datetime.strptime(iso_date, "%Y-%m-%dT%H:%M:%S.%fZ") 37 | except ValueError: 38 | split = iso_date.split(".") 39 | if len(split) < 2: 40 | raise 41 | reduce = len(split[1]) - 6 42 | reduced = f"{split[0]}.{split[1][:-reduce]}Z" 43 | return datetime.strptime(reduced, "%Y-%m-%dT%H:%M:%S.%fZ") 44 | -------------------------------------------------------------------------------- /meilisearch/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional, Tuple 4 | 5 | 6 | class Config: 7 | """ 8 | Client's credentials and configuration parameters 9 | """ 10 | 11 | class Paths: 12 | health = "health" 13 | keys = "keys" 14 | version = "version" 15 | index = "indexes" 16 | task = "tasks" 17 | batch = "batches" 18 | stat = "stats" 19 | search = "search" 20 | facet_search = "facet-search" 21 | multi_search = "multi-search" 22 | document = "documents" 23 | similar = "similar" 24 | setting = "settings" 25 | ranking_rules = "ranking-rules" 26 | distinct_attribute = "distinct-attribute" 27 | searchable_attributes = "searchable-attributes" 28 | displayed_attributes = "displayed-attributes" 29 | stop_words = "stop-words" 30 | synonyms = "synonyms" 31 | accept_new_fields = "accept-new-fields" 32 | filterable_attributes = "filterable-attributes" 33 | sortable_attributes = "sortable-attributes" 34 | typo_tolerance = "typo-tolerance" 35 | dumps = "dumps" 36 | snapshots = "snapshots" 37 | pagination = "pagination" 38 | faceting = "faceting" 39 | dictionary = "dictionary" 40 | separator_tokens = "separator-tokens" 41 | non_separator_tokens = "non-separator-tokens" 42 | swap = "swap-indexes" 43 | embedders = "embedders" 44 | search_cutoff_ms = "search-cutoff-ms" 45 | proximity_precision = "proximity-precision" 46 | localized_attributes = "localized-attributes" 47 | edit = "edit" 48 | 49 | def __init__( 50 | self, 51 | url: str, 52 | api_key: Optional[str] = None, 53 | timeout: Optional[int] = None, 54 | client_agents: Optional[Tuple[str, ...]] = None, 55 | ) -> None: 56 | """ 57 | Parameters 58 | ---------- 59 | url: 60 | The url to the Meilisearch API (ex: http://localhost:7700) 61 | api_key: 62 | The optional API key to access Meilisearch 63 | """ 64 | 65 | self.url = url 66 | self.api_key = api_key 67 | self.timeout = timeout 68 | self.client_agents = client_agents 69 | self.paths = self.Paths() 70 | -------------------------------------------------------------------------------- /meilisearch/errors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from functools import wraps 5 | from typing import TYPE_CHECKING, Any, Callable, TypeVar 6 | 7 | from requests import Response 8 | 9 | if TYPE_CHECKING: # pragma: no cover 10 | from meilisearch.client import Client 11 | from meilisearch.index import Index 12 | from meilisearch.task import TaskHandler 13 | 14 | T = TypeVar("T") 15 | 16 | 17 | class MeilisearchError(Exception): # pragma: no cover 18 | """Generic class for Meilisearch error handling""" 19 | 20 | def __init__(self, message: str) -> None: 21 | self.message = message 22 | super().__init__(self.message) 23 | 24 | def __str__(self) -> str: 25 | return f"MeilisearchError. Error message: {self.message}" 26 | 27 | 28 | class MeilisearchApiError(MeilisearchError): 29 | """Error sent by Meilisearch API""" 30 | 31 | def __init__(self, error: str, request: Response) -> None: 32 | self.status_code = request.status_code 33 | self.code = None 34 | self.link = None 35 | self.type = None 36 | 37 | if request.text: 38 | json_data = json.loads(request.text) 39 | self.message = json_data.get("message") 40 | self.code = json_data.get("code") 41 | self.link = json_data.get("link") 42 | self.type = json_data.get("type") 43 | else: 44 | self.message = error 45 | super().__init__(self.message) 46 | 47 | def __str__(self) -> str: 48 | if self.code and self.link: # pragma: no cover 49 | return f"MeilisearchApiError. Error code: {self.code}. Error message: {self.message} Error documentation: {self.link} Error type: {self.type}" 50 | 51 | return f"MeilisearchApiError. {self.message}" 52 | 53 | 54 | class MeilisearchCommunicationError(MeilisearchError): 55 | """Error when connecting to Meilisearch""" 56 | 57 | def __str__(self) -> str: # pragma: no cover 58 | return f"MeilisearchCommunicationError, {self.message}" 59 | 60 | 61 | class MeilisearchTimeoutError(MeilisearchError): 62 | """Error when Meilisearch operation takes longer than expected""" 63 | 64 | def __str__(self) -> str: # pragma: no cover 65 | return f"MeilisearchTimeoutError, {self.message}" 66 | 67 | 68 | def version_error_hint_message(func: Callable[..., T]) -> Callable[..., T]: 69 | @wraps(func) 70 | def wrapper(*args: Any, **kwargs: Any) -> Any: 71 | try: 72 | return func(*args, **kwargs) 73 | except MeilisearchApiError as exc: 74 | exc.message = f"{exc.message}. Hint: It might not be working because you're not up to date with the Meilisearch version that {func.__name__} call requires." 75 | raise exc 76 | 77 | return wrapper 78 | -------------------------------------------------------------------------------- /meilisearch/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/meilisearch/models/__init__.py -------------------------------------------------------------------------------- /meilisearch/models/document.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Iterator, List 2 | 3 | 4 | class Document: 5 | __doc: Dict 6 | 7 | def __init__(self, doc: Dict[str, Any]) -> None: 8 | self.__doc = doc 9 | for key in doc: 10 | setattr(self, key, doc[key]) 11 | 12 | def __getattr__(self, attr: str) -> str: 13 | if attr in self.__doc.keys(): 14 | return attr 15 | raise AttributeError(f"{self.__class__.__name__} object has no attribute {attr}") 16 | 17 | def __iter__(self) -> Iterator: 18 | return iter(self.__dict__.items()) 19 | 20 | 21 | class DocumentsResults: 22 | def __init__(self, resp: Dict[str, Any]) -> None: 23 | self.results: List[Document] = [Document(doc) for doc in resp["results"]] 24 | self.offset: int = resp["offset"] 25 | self.limit: int = resp["limit"] 26 | self.total: int = resp["total"] 27 | -------------------------------------------------------------------------------- /meilisearch/models/embedders.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import Any, Dict, Optional, Union 5 | 6 | from camel_converter.pydantic_base import CamelBase 7 | 8 | 9 | class Distribution(CamelBase): 10 | """Distribution settings for embedders. 11 | 12 | Parameters 13 | ---------- 14 | mean: float 15 | Mean value between 0 and 1 16 | sigma: float 17 | Sigma value between 0 and 1 18 | """ 19 | 20 | mean: float 21 | sigma: float 22 | 23 | 24 | class PoolingType(str, Enum): 25 | """Pooling strategies for HuggingFaceEmbedder. 26 | 27 | Attributes 28 | ---------- 29 | USE_MODEL : str 30 | Use the model's default pooling strategy. 31 | FORCE_MEAN : str 32 | Force mean pooling over the token embeddings. 33 | FORCE_CLS : str 34 | Use the [CLS] token embedding as the sentence representation. 35 | """ 36 | 37 | USE_MODEL = "useModel" 38 | FORCE_MEAN = "forceMean" 39 | FORCE_CLS = "forceCls" 40 | 41 | 42 | class OpenAiEmbedder(CamelBase): 43 | """OpenAI embedder configuration. 44 | 45 | Parameters 46 | ---------- 47 | source: str 48 | The embedder source, must be "openAi" 49 | url: Optional[str] 50 | The URL Meilisearch contacts when querying the embedder 51 | api_key: Optional[str] 52 | Authentication token Meilisearch should send with each request to the embedder 53 | model: Optional[str] 54 | The model your embedder uses when generating vectors (defaults to text-embedding-3-small) 55 | dimensions: Optional[int] 56 | Number of dimensions in the chosen model 57 | document_template: Optional[str] 58 | Template defining the data Meilisearch sends to the embedder 59 | document_template_max_bytes: Optional[int] 60 | Maximum allowed size of rendered document template (defaults to 400) 61 | distribution: Optional[Distribution] 62 | Describes the natural distribution of search results 63 | binary_quantized: Optional[bool] 64 | Once set to true, irreversibly converts all vector dimensions to 1-bit values 65 | """ 66 | 67 | source: str = "openAi" 68 | url: Optional[str] = None 69 | api_key: Optional[str] = None 70 | model: Optional[str] = None # Defaults to text-embedding-3-small 71 | dimensions: Optional[int] = None # Uses the model default 72 | document_template: Optional[str] = None 73 | document_template_max_bytes: Optional[int] = None # Default to 400 74 | distribution: Optional[Distribution] = None 75 | binary_quantized: Optional[bool] = None 76 | 77 | 78 | class HuggingFaceEmbedder(CamelBase): 79 | """HuggingFace embedder configuration. 80 | 81 | Parameters 82 | ---------- 83 | source: str 84 | The embedder source, must be "huggingFace" 85 | url: Optional[str] 86 | The URL Meilisearch contacts when querying the embedder 87 | model: Optional[str] 88 | The model your embedder uses when generating vectors (defaults to BAAI/bge-base-en-v1.5) 89 | dimensions: Optional[int] 90 | Number of dimensions in the chosen model 91 | revision: Optional[str] 92 | Model revision hash 93 | document_template: Optional[str] 94 | Template defining the data Meilisearch sends to the embedder 95 | document_template_max_bytes: Optional[int] 96 | Maximum allowed size of rendered document template (defaults to 400) 97 | distribution: Optional[Distribution] 98 | Describes the natural distribution of search results 99 | binary_quantized: Optional[bool] 100 | Once set to true, irreversibly converts all vector dimensions to 1-bit values 101 | pooling: Optional[PoolingType] 102 | Configures how individual tokens are merged into a single embedding 103 | """ 104 | 105 | source: str = "huggingFace" 106 | url: Optional[str] = None 107 | model: Optional[str] = None # Defaults to BAAI/bge-base-en-v1.5 108 | dimensions: Optional[int] = None 109 | revision: Optional[str] = None 110 | document_template: Optional[str] = None 111 | document_template_max_bytes: Optional[int] = None # Default to 400 112 | distribution: Optional[Distribution] = None 113 | binary_quantized: Optional[bool] = None 114 | pooling: Optional[PoolingType] = PoolingType.USE_MODEL 115 | 116 | 117 | class OllamaEmbedder(CamelBase): 118 | """Ollama embedder configuration. 119 | 120 | Parameters 121 | ---------- 122 | source: str 123 | The embedder source, must be "ollama" 124 | url: Optional[str] 125 | The URL Meilisearch contacts when querying the embedder (defaults to http://localhost:11434/api/embeddings) 126 | api_key: Optional[str] 127 | Authentication token Meilisearch should send with each request to the embedder 128 | model: Optional[str] 129 | The model your embedder uses when generating vectors 130 | dimensions: Optional[int] 131 | Number of dimensions in the chosen model 132 | document_template: Optional[str] 133 | Template defining the data Meilisearch sends to the embedder 134 | document_template_max_bytes: Optional[int] 135 | Maximum allowed size of rendered document template (defaults to 400) 136 | distribution: Optional[Distribution] 137 | Describes the natural distribution of search results 138 | binary_quantized: Optional[bool] 139 | Once set to true, irreversibly converts all vector dimensions to 1-bit values 140 | """ 141 | 142 | source: str = "ollama" 143 | url: Optional[str] = None 144 | api_key: Optional[str] = None 145 | model: Optional[str] = None 146 | dimensions: Optional[int] = None 147 | document_template: Optional[str] = None 148 | document_template_max_bytes: Optional[int] = None 149 | distribution: Optional[Distribution] = None 150 | binary_quantized: Optional[bool] = None 151 | 152 | 153 | class RestEmbedder(CamelBase): 154 | """REST API embedder configuration. 155 | 156 | Parameters 157 | ---------- 158 | source: str 159 | The embedder source, must be "rest" 160 | url: Optional[str] 161 | The URL Meilisearch contacts when querying the embedder 162 | api_key: Optional[str] 163 | Authentication token Meilisearch should send with each request to the embedder 164 | dimensions: Optional[int] 165 | Number of dimensions in the embeddings 166 | document_template: Optional[str] 167 | Template defining the data Meilisearch sends to the embedder 168 | document_template_max_bytes: Optional[int] 169 | Maximum allowed size of rendered document template (defaults to 400) 170 | request: Dict[str, Any] 171 | A JSON value representing the request Meilisearch makes to the remote embedder 172 | response: Dict[str, Any] 173 | A JSON value representing the request Meilisearch expects from the remote embedder 174 | headers: Optional[Dict[str, str]] 175 | Custom headers to send with the request 176 | distribution: Optional[Distribution] 177 | Describes the natural distribution of search results 178 | binary_quantized: Optional[bool] 179 | Once set to true, irreversibly converts all vector dimensions to 1-bit values 180 | """ 181 | 182 | source: str = "rest" 183 | url: Optional[str] = None 184 | api_key: Optional[str] = None 185 | dimensions: Optional[int] = None 186 | document_template: Optional[str] = None 187 | document_template_max_bytes: Optional[int] = None 188 | request: Dict[str, Any] 189 | response: Dict[str, Any] 190 | headers: Optional[Dict[str, str]] = None 191 | distribution: Optional[Distribution] = None 192 | binary_quantized: Optional[bool] = None 193 | 194 | 195 | class UserProvidedEmbedder(CamelBase): 196 | """User-provided embedder configuration. 197 | 198 | Parameters 199 | ---------- 200 | source: str 201 | The embedder source, must be "userProvided" 202 | dimensions: int 203 | Number of dimensions in the embeddings 204 | distribution: Optional[Distribution] 205 | Describes the natural distribution of search results 206 | binary_quantized: Optional[bool] 207 | Once set to true, irreversibly converts all vector dimensions to 1-bit values 208 | """ 209 | 210 | source: str = "userProvided" 211 | dimensions: int 212 | distribution: Optional[Distribution] = None 213 | binary_quantized: Optional[bool] = None 214 | 215 | 216 | class CompositeEmbedder(CamelBase): 217 | """Composite embedder configuration. 218 | 219 | Parameters 220 | ---------- 221 | source: str 222 | The embedder source, must be "composite" 223 | indexing_embedder: Union[ 224 | OpenAiEmbedder, 225 | HuggingFaceEmbedder, 226 | OllamaEmbedder, 227 | RestEmbedder, 228 | UserProvidedEmbedder, 229 | ] 230 | search_embedder: Union[ 231 | OpenAiEmbedder, 232 | HuggingFaceEmbedder, 233 | OllamaEmbedder, 234 | RestEmbedder, 235 | UserProvidedEmbedder, 236 | ]""" 237 | 238 | source: str = "composite" 239 | search_embedder: Union[ 240 | OpenAiEmbedder, 241 | HuggingFaceEmbedder, 242 | OllamaEmbedder, 243 | RestEmbedder, 244 | UserProvidedEmbedder, 245 | ] 246 | indexing_embedder: Union[ 247 | OpenAiEmbedder, 248 | HuggingFaceEmbedder, 249 | OllamaEmbedder, 250 | RestEmbedder, 251 | UserProvidedEmbedder, 252 | ] 253 | 254 | 255 | # Type alias for the embedder union type 256 | EmbedderType = Union[ 257 | OpenAiEmbedder, 258 | HuggingFaceEmbedder, 259 | OllamaEmbedder, 260 | RestEmbedder, 261 | UserProvidedEmbedder, 262 | CompositeEmbedder, 263 | ] 264 | 265 | 266 | class Embedders(CamelBase): 267 | """Container for embedder configurations. 268 | 269 | Parameters 270 | ---------- 271 | embedders: Dict[str, Union[OpenAiEmbedder, HuggingFaceEmbedder, OllamaEmbedder, RestEmbedder, UserProvidedEmbedder]] 272 | Dictionary of embedder configurations, where keys are embedder names 273 | """ 274 | 275 | embedders: Dict[str, EmbedderType] 276 | -------------------------------------------------------------------------------- /meilisearch/models/index.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import Any, Dict, Iterator, List, Optional 5 | 6 | from camel_converter import to_snake 7 | from camel_converter.pydantic_base import CamelBase 8 | 9 | 10 | class IndexStats: 11 | __dict: Dict 12 | 13 | def __init__(self, doc: Dict[str, Any]) -> None: 14 | self.__dict = doc 15 | for key, val in doc.items(): 16 | key = to_snake(key) 17 | if isinstance(val, dict): 18 | setattr(self, key, IndexStats(val)) 19 | else: 20 | setattr(self, key, val) 21 | 22 | def __getattr__(self, attr: str) -> Any: 23 | if attr in self.__dict.keys(): 24 | return attr 25 | raise AttributeError(f"{self.__class__.__name__} object has no attribute {attr}") 26 | 27 | def __iter__(self) -> Iterator: 28 | return iter(self.__dict__.items()) 29 | 30 | 31 | class Faceting(CamelBase): 32 | max_values_per_facet: int 33 | sort_facet_values_by: Optional[Dict[str, str]] = None 34 | 35 | 36 | class Pagination(CamelBase): 37 | max_total_hits: int 38 | 39 | 40 | class MinWordSizeForTypos(CamelBase): 41 | one_typo: Optional[int] = None 42 | two_typos: Optional[int] = None 43 | 44 | 45 | class TypoTolerance(CamelBase): 46 | enabled: bool = True 47 | disable_on_attributes: Optional[List[str]] = None 48 | disable_on_words: Optional[List[str]] = None 49 | min_word_size_for_typos: Optional[MinWordSizeForTypos] = None 50 | 51 | 52 | class ProximityPrecision(str, Enum): 53 | BY_WORD = "byWord" 54 | BY_ATTRIBUTE = "byAttribute" 55 | 56 | 57 | class EmbedderDistribution(CamelBase): 58 | mean: float 59 | sigma: float 60 | 61 | 62 | class LocalizedAttributes(CamelBase): 63 | attribute_patterns: List[str] 64 | locales: List[str] 65 | -------------------------------------------------------------------------------- /meilisearch/models/key.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List, Optional, Union 3 | 4 | import pydantic 5 | from camel_converter.pydantic_base import CamelBase 6 | 7 | from meilisearch._utils import is_pydantic_2, iso_to_date_time 8 | 9 | 10 | class _KeyBase(CamelBase): 11 | uid: str 12 | name: Optional[str] = None 13 | description: Optional[str] 14 | actions: List[str] 15 | indexes: List[str] 16 | expires_at: Optional[datetime] = None 17 | 18 | if is_pydantic_2(): 19 | model_config = pydantic.ConfigDict(ser_json_timedelta="iso8601") # type: ignore[typeddict-unknown-key] 20 | 21 | @pydantic.field_validator("expires_at", mode="before") # type: ignore[attr-defined] 22 | @classmethod 23 | def validate_expires_at( # pylint: disable=invalid-name 24 | cls, v: str 25 | ) -> Union[datetime, None]: 26 | return iso_to_date_time(v) 27 | 28 | else: # pragma: no cover 29 | 30 | @pydantic.validator("expires_at", pre=True) 31 | @classmethod 32 | def validate_expires_at( # pylint: disable=invalid-name 33 | cls, v: str 34 | ) -> Union[datetime, None]: 35 | return iso_to_date_time(v) 36 | 37 | class Config: 38 | json_encoders = { 39 | datetime: lambda v: ( 40 | None 41 | if not v 42 | else ( 43 | f"{str(v).split('+', maxsplit=1)[0].replace(' ', 'T')}Z" 44 | if "+" in str(v) 45 | else f"{str(v).replace(' ', 'T')}Z" 46 | ) 47 | ) 48 | } 49 | 50 | 51 | class Key(_KeyBase): 52 | key: str 53 | created_at: datetime 54 | updated_at: Optional[datetime] = None 55 | 56 | if is_pydantic_2(): 57 | 58 | @pydantic.field_validator("created_at", mode="before") # type: ignore[attr-defined] 59 | @classmethod 60 | def validate_created_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 61 | converted = iso_to_date_time(v) 62 | 63 | if not converted: 64 | raise ValueError("created_at is required") # pragma: no cover 65 | 66 | return converted 67 | 68 | @pydantic.field_validator("updated_at", mode="before") # type: ignore[attr-defined] 69 | @classmethod 70 | def validate_updated_at( # pylint: disable=invalid-name 71 | cls, v: str 72 | ) -> Union[datetime, None]: 73 | return iso_to_date_time(v) 74 | 75 | else: # pragma: no cover 76 | 77 | @pydantic.validator("created_at", pre=True) 78 | @classmethod 79 | def validate_created_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 80 | converted = iso_to_date_time(v) 81 | 82 | if not converted: 83 | raise ValueError("created_at is required") 84 | 85 | return converted 86 | 87 | @pydantic.validator("updated_at", pre=True) 88 | @classmethod 89 | def validate_updated_at( # pylint: disable=invalid-name 90 | cls, v: str 91 | ) -> Union[datetime, None]: 92 | return iso_to_date_time(v) 93 | 94 | 95 | class KeyUpdate(CamelBase): 96 | key: str 97 | name: Optional[str] = None 98 | description: Optional[str] = None 99 | actions: Optional[List[str]] = None 100 | indexes: Optional[List[str]] = None 101 | expires_at: Optional[datetime] = None 102 | 103 | if is_pydantic_2(): 104 | model_config = pydantic.ConfigDict(ser_json_timedelta="iso8601") # type: ignore[typeddict-unknown-key] 105 | 106 | else: # pragma: no cover 107 | 108 | class Config: 109 | json_encoders = { 110 | datetime: lambda v: ( 111 | None 112 | if not v 113 | else ( 114 | f"{str(v).split('+', maxsplit=1)[0].replace(' ', 'T')}Z" 115 | if "+" in str(v) 116 | else f"{str(v).replace(' ', 'T')}Z" 117 | ) 118 | ) 119 | } 120 | 121 | 122 | class KeysResults(CamelBase): 123 | results: List[Key] 124 | offset: int 125 | limit: int 126 | total: int 127 | -------------------------------------------------------------------------------- /meilisearch/models/task.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from typing import Any, Dict, List, Optional, Union 5 | 6 | import pydantic 7 | from camel_converter.pydantic_base import CamelBase 8 | 9 | from meilisearch._utils import is_pydantic_2, iso_to_date_time 10 | 11 | 12 | class Task(CamelBase): 13 | uid: int 14 | index_uid: Union[str, None] = None 15 | status: str 16 | type: str 17 | details: Union[Dict[str, Any], None] = None 18 | error: Union[Dict[str, Any], None] = None 19 | canceled_by: Union[int, None] = None 20 | duration: Optional[str] = None 21 | enqueued_at: datetime 22 | started_at: Optional[datetime] = None 23 | finished_at: Optional[datetime] = None 24 | 25 | if is_pydantic_2(): 26 | 27 | @pydantic.field_validator("enqueued_at", mode="before") # type: ignore[attr-defined] 28 | @classmethod 29 | def validate_enqueued_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 30 | converted = iso_to_date_time(v) 31 | 32 | if not converted: # pragma: no cover 33 | raise ValueError("enqueued_at is required") 34 | return converted 35 | 36 | @pydantic.field_validator("started_at", mode="before") # type: ignore[attr-defined] 37 | @classmethod 38 | def validate_started_at( # pylint: disable=invalid-name 39 | cls, v: str 40 | ) -> Union[datetime, None]: 41 | return iso_to_date_time(v) 42 | 43 | @pydantic.field_validator("finished_at", mode="before") # type: ignore[attr-defined] 44 | @classmethod 45 | def validate_finished_at( # pylint: disable=invalid-name 46 | cls, v: str 47 | ) -> Union[datetime, None]: 48 | return iso_to_date_time(v) 49 | 50 | else: # pragma: no cover 51 | 52 | @pydantic.validator("enqueued_at", pre=True) 53 | @classmethod 54 | def validate_enqueued_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 55 | converted = iso_to_date_time(v) 56 | 57 | if not converted: 58 | raise ValueError("enqueued_at is required") 59 | 60 | return converted 61 | 62 | @pydantic.validator("started_at", pre=True) 63 | @classmethod 64 | def validate_started_at( # pylint: disable=invalid-name 65 | cls, v: str 66 | ) -> Union[datetime, None]: 67 | return iso_to_date_time(v) 68 | 69 | @pydantic.validator("finished_at", pre=True) 70 | @classmethod 71 | def validate_finished_at( # pylint: disable=invalid-name 72 | cls, v: str 73 | ) -> Union[datetime, None]: 74 | return iso_to_date_time(v) 75 | 76 | 77 | class TaskInfo(CamelBase): 78 | task_uid: int 79 | index_uid: Union[str, None] 80 | status: str 81 | type: str 82 | enqueued_at: datetime 83 | 84 | if is_pydantic_2(): 85 | 86 | @pydantic.field_validator("enqueued_at", mode="before") # type: ignore[attr-defined] 87 | @classmethod 88 | def validate_enqueued_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 89 | converted = iso_to_date_time(v) 90 | 91 | if not converted: # pragma: no cover 92 | raise ValueError("enqueued_at is required") 93 | 94 | return converted 95 | 96 | else: # pragma: no cover 97 | 98 | @pydantic.validator("enqueued_at", pre=True) 99 | @classmethod 100 | def validate_enqueued_at(cls, v: str) -> datetime: # pylint: disable=invalid-name 101 | converted = iso_to_date_time(v) 102 | 103 | if not converted: 104 | raise ValueError("enqueued_at is required") 105 | 106 | return converted 107 | 108 | 109 | class TaskResults: 110 | def __init__(self, resp: Dict[str, Any]) -> None: 111 | self.results: List[Task] = [Task(**task) for task in resp["results"]] 112 | self.limit: int = resp["limit"] 113 | self.total: int = resp["total"] 114 | self.from_: int = resp["from"] 115 | self.next_: int = resp["next"] 116 | 117 | 118 | class Batch(CamelBase): 119 | uid: int 120 | details: Optional[Dict[str, Any]] = None 121 | stats: Optional[Dict[str, Union[int, Dict[str, Any]]]] = None 122 | duration: Optional[str] = None 123 | started_at: Optional[datetime] = None 124 | finished_at: Optional[datetime] = None 125 | progress: Optional[Dict[str, Union[float, List[Dict[str, Any]]]]] = None 126 | 127 | if is_pydantic_2(): 128 | 129 | @pydantic.field_validator("started_at", mode="before") # type: ignore[attr-defined] 130 | @classmethod 131 | def validate_started_at(cls, v: str) -> Optional[datetime]: # pylint: disable=invalid-name 132 | return iso_to_date_time(v) 133 | 134 | @pydantic.field_validator("finished_at", mode="before") # type: ignore[attr-defined] 135 | @classmethod 136 | def validate_finished_at(cls, v: str) -> Optional[datetime]: # pylint: disable=invalid-name 137 | return iso_to_date_time(v) 138 | 139 | else: # pragma: no cover 140 | 141 | @pydantic.validator("started_at", pre=True) 142 | @classmethod 143 | def validate_started_at(cls, v: str) -> Optional[datetime]: # pylint: disable=invalid-name 144 | return iso_to_date_time(v) 145 | 146 | @pydantic.validator("finished_at", pre=True) 147 | @classmethod 148 | def validate_finished_at(cls, v: str) -> Optional[datetime]: # pylint: disable=invalid-name 149 | return iso_to_date_time(v) 150 | 151 | 152 | class BatchResults(CamelBase): 153 | results: List[Batch] 154 | total: int 155 | limit: int 156 | from_: int 157 | # None means last page 158 | next_: Optional[int] 159 | -------------------------------------------------------------------------------- /meilisearch/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/meilisearch/py.typed -------------------------------------------------------------------------------- /meilisearch/task.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from time import sleep 5 | from typing import Any, MutableMapping, Optional 6 | from urllib import parse 7 | 8 | from meilisearch._httprequests import HttpRequests 9 | from meilisearch.config import Config 10 | from meilisearch.errors import MeilisearchTimeoutError 11 | from meilisearch.models.task import Batch, BatchResults, Task, TaskInfo, TaskResults 12 | 13 | 14 | class TaskHandler: 15 | """ 16 | A class covering the Meilisearch Task API 17 | 18 | The task class gives access to all task routes and gives information about the progress of asynchronous operations. 19 | https://www.meilisearch.com/docs/reference/api/tasks 20 | """ 21 | 22 | def __init__(self, config: Config): 23 | """Parameters 24 | ---------- 25 | config: Config object containing permission and location of Meilisearch. 26 | """ 27 | self.config = config 28 | self.http = HttpRequests(config) 29 | 30 | def get_batches(self, parameters: Optional[MutableMapping[str, Any]] = None) -> BatchResults: 31 | """Get all task batches. 32 | 33 | Parameters 34 | ---------- 35 | parameters (optional): 36 | parameters accepted by the get batches route: https://www.meilisearch.com/docs/reference/api/batches#get-batches. 37 | 38 | Returns 39 | ------- 40 | batch: 41 | BatchResults instance contining limit, from, next and results containing a list of all batches. 42 | 43 | Raises 44 | ------ 45 | MeilisearchApiError 46 | 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 47 | """ 48 | if parameters is None: 49 | parameters = {} 50 | for param in parameters: 51 | if isinstance(parameters[param], (list, tuple)): 52 | parameters[param] = ",".join(parameters[param]) 53 | batches = self.http.get(f"{self.config.paths.batch}?{parse.urlencode(parameters)}") 54 | return BatchResults(**batches) 55 | 56 | def get_batch(self, uid: int) -> Batch: 57 | """Get one tasks batch. 58 | 59 | Parameters 60 | ---------- 61 | uid: 62 | Identifier of the batch. 63 | 64 | Returns 65 | ------- 66 | task: 67 | Batch instance containing information about the progress of the asynchronous batch. 68 | 69 | Raises 70 | ------ 71 | MeilisearchApiError 72 | 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 73 | """ 74 | batch = self.http.get(f"{self.config.paths.batch}/{uid}") 75 | return Batch(**batch) 76 | 77 | def get_tasks(self, parameters: Optional[MutableMapping[str, Any]] = None) -> TaskResults: 78 | """Get all tasks. 79 | 80 | Parameters 81 | ---------- 82 | parameters (optional): 83 | parameters accepted by the get tasks route: https://www.meilisearch.com/docs/reference/api/tasks#get-tasks. 84 | 85 | Returns 86 | ------- 87 | task: 88 | TaskResults instance contining limit, from, next and results containing a list of all 89 | enqueued, processing, succeeded or failed tasks. 90 | 91 | Raises 92 | ------ 93 | MeilisearchApiError 94 | 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 95 | """ 96 | if parameters is None: 97 | parameters = {} 98 | for param in parameters: 99 | if isinstance(parameters[param], list): 100 | parameters[param] = ",".join(parameters[param]) 101 | tasks = self.http.get(f"{self.config.paths.task}?{parse.urlencode(parameters)}") 102 | return TaskResults(tasks) 103 | 104 | def get_task(self, uid: int) -> Task: 105 | """Get one task. 106 | 107 | Parameters 108 | ---------- 109 | uid: 110 | Identifier of the task. 111 | 112 | Returns 113 | ------- 114 | task: 115 | Task instance containing information about the processed asynchronous task. 116 | 117 | Raises 118 | ------ 119 | MeilisearchApiError 120 | 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 121 | """ 122 | task = self.http.get(f"{self.config.paths.task}/{uid}") 123 | return Task(**task) 124 | 125 | def cancel_tasks(self, parameters: MutableMapping[str, Any]) -> TaskInfo: 126 | """Cancel a list of enqueued or processing tasks. 127 | 128 | Parameters 129 | ---------- 130 | parameters: 131 | parameters accepted by the cancel tasks https://www.meilisearch.com/docs/reference/api/tasks#cancel-task. 132 | 133 | Returns 134 | ------- 135 | task_info: 136 | TaskInfo instance containing information about a task to track the progress of an asynchronous process. 137 | https://www.meilisearch.com/docs/reference/api/tasks#get-one-task 138 | 139 | Raises 140 | ------ 141 | MeilisearchApiError 142 | 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 143 | """ 144 | for param in parameters: 145 | if isinstance(parameters[param], list): 146 | parameters[param] = ",".join(parameters[param]) 147 | response = self.http.post(f"{self.config.paths.task}/cancel?{parse.urlencode(parameters)}") 148 | return TaskInfo(**response) 149 | 150 | def delete_tasks(self, parameters: MutableMapping[str, Any]) -> TaskInfo: 151 | """Delete a list of enqueued or processing tasks. 152 | Parameters 153 | ---------- 154 | config: 155 | Config object containing permission and location of Meilisearch. 156 | parameters: 157 | parameters accepted by the delete tasks route:https://www.meilisearch.com/docs/reference/api/tasks#delete-task. 158 | Returns 159 | ------- 160 | task_info: 161 | TaskInfo instance containing information about a task to track the progress of an asynchronous process. 162 | https://www.meilisearch.com/docs/reference/api/tasks#get-one-task 163 | Raises 164 | ------ 165 | MeilisearchApiError 166 | 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 167 | """ 168 | for param in parameters: 169 | if isinstance(parameters[param], list): 170 | parameters[param] = ",".join(parameters[param]) 171 | response = self.http.delete(f"{self.config.paths.task}?{parse.urlencode(parameters)}") 172 | return TaskInfo(**response) 173 | 174 | def wait_for_task( 175 | self, 176 | uid: int, 177 | timeout_in_ms: int = 5000, 178 | interval_in_ms: int = 50, 179 | ) -> Task: 180 | """Wait until the task fails or succeeds in Meilisearch. 181 | 182 | Parameters 183 | ---------- 184 | uid: 185 | Identifier of the task to wait for being processed. 186 | timeout_in_ms (optional): 187 | Time the method should wait before raising a MeilisearchTimeoutError. 188 | interval_in_ms (optional): 189 | Time interval the method should wait (sleep) between requests. 190 | 191 | Returns 192 | ------- 193 | task: 194 | Task instance containing information about the processed asynchronous task. 195 | 196 | Raises 197 | ------ 198 | MeilisearchTimeoutError 199 | 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 200 | """ 201 | start_time = datetime.now() 202 | elapsed_time = 0.0 203 | while elapsed_time < timeout_in_ms: 204 | task = self.get_task(uid) 205 | if task.status not in ("enqueued", "processing"): 206 | return task 207 | sleep(interval_in_ms / 1000) 208 | time_delta = datetime.now() - start_time 209 | elapsed_time = time_delta.seconds * 1000 + time_delta.microseconds / 1000 210 | raise MeilisearchTimeoutError( 211 | f"timeout of ${timeout_in_ms}ms has exceeded on process ${uid} when waiting for task to be resolve." 212 | ) 213 | -------------------------------------------------------------------------------- /meilisearch/version.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __version__ = "0.34.1" 4 | 5 | 6 | def qualified_version() -> str: 7 | """Get the qualified version of this module.""" 8 | 9 | return f"Meilisearch Python (v{__version__})" 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "meilisearch" 7 | description = "The python client for Meilisearch API." 8 | license = { text = "MIT" } 9 | requires-python = ">=3.9" 10 | authors = [ 11 | { name = "Clémentine", email = "clementine@meilisearch.com" }, 12 | ] 13 | keywords = [ 14 | "search", 15 | "python", 16 | "meilisearch", 17 | ] 18 | classifiers = [ 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | "Programming Language :: Python :: 3.12", 24 | "Programming Language :: Python :: 3.13", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ] 28 | dependencies = [ 29 | "requests", 30 | "camel-converter[pydantic]", 31 | ] 32 | dynamic = ['version'] 33 | 34 | [tool.setuptools.dynamic] 35 | version = {attr = "meilisearch.version.__version__"} 36 | 37 | [tool.setuptools.packages.find] 38 | include = ["meilisearch*"] 39 | exclude = ["docs*", "tests*"] 40 | 41 | [tool.setuptools.package-data] 42 | meilisearch = ["py.typed"] 43 | 44 | [project.urls] 45 | Meilisearch_Documentation = "https://www.meilisearch.com/docs" 46 | Documentation = "https://meilisearch.github.io/meilisearch-python/" 47 | Homepage = "https://github.com/meilisearch/meilisearch-python" 48 | 49 | [tool.black] 50 | line-length = 100 51 | target-version = ['py38'] 52 | include = '\.pyi?$' 53 | extend-exclude = ''' 54 | /( 55 | \.egg 56 | | \.git 57 | | \.hg 58 | | \.mypy_cache 59 | | \.nox 60 | | \.tox 61 | | \.venv 62 | | \venv 63 | | _build 64 | | buck-out 65 | | build 66 | | dist 67 | | setup.py 68 | )/ 69 | ''' 70 | 71 | [tool.isort] 72 | profile = "black" 73 | line_length = 100 74 | src_paths = ["meilisearch", "tests"] 75 | 76 | [tool.mypy] 77 | disallow_untyped_defs = true 78 | 79 | [[tool.mypy.overrides]] 80 | module = ["tests.*"] 81 | disallow_untyped_defs = false 82 | 83 | [tool.pylint] 84 | [tool.pylint.MASTER] 85 | load-plugins = [ 86 | 'pylint.extensions.bad_builtin', 87 | ] 88 | 89 | [tool.pylint.'DEPRECATED_BUILTINS'] 90 | bad-functions=[ 91 | "map", 92 | "filter", 93 | "input" 94 | ] 95 | 96 | [tool.pylint.'BASIC'] 97 | const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))$" 98 | attr-rgx = "[a-z_][a-z0-9_]{2,}$" 99 | module-rgx = "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" 100 | method-rgx = "[a-z_][a-z0-9_]{2,}$" 101 | 102 | [tool.pylint.'DESIGN'] 103 | max-args = 10 104 | max-public-methods = 25 105 | 106 | [tool.pylint.'FORMAT'] 107 | max-module-lines=2000 108 | 109 | [tool.pylint.'MESSAGES CONTROL'] 110 | disable=[ 111 | "attribute-defined-outside-init", 112 | "duplicate-code", 113 | "missing-docstring", 114 | "too-few-public-methods", 115 | "line-too-long", 116 | "too-many-positional-arguments", 117 | ] 118 | enable=[ 119 | "use-symbolic-message-instead", 120 | "fixme", 121 | ] 122 | 123 | [tool.pylint.'REPORTS'] 124 | reports=false 125 | 126 | [tool.pytest.ini_options] 127 | minversion = "6.0" 128 | addopts = "--cov=meilisearch --cov-report term-missing" 129 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .common import BASE_URL, MASTER_KEY 2 | -------------------------------------------------------------------------------- /tests/client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/tests/client/__init__.py -------------------------------------------------------------------------------- /tests/client/test_client.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import pytest 4 | 5 | import meilisearch 6 | from tests import BASE_URL, MASTER_KEY 7 | 8 | 9 | def test_get_client(): 10 | """Tests getting a client instance.""" 11 | client = meilisearch.Client(BASE_URL, MASTER_KEY) 12 | assert client.config 13 | response = client.health() 14 | assert response["status"] == "available" 15 | 16 | 17 | def test_client_timeout_set(): 18 | timeout = 5 19 | client = meilisearch.Client(BASE_URL, MASTER_KEY, timeout=timeout) 20 | response = client.health() 21 | assert client.config.timeout == timeout 22 | assert response["status"] == "available" 23 | 24 | 25 | def test_client_timeout_not_set(): 26 | default_timeout = None 27 | client = meilisearch.Client(BASE_URL, MASTER_KEY) 28 | response = client.health() 29 | assert client.config.timeout == default_timeout 30 | assert response["status"] == "available" 31 | 32 | 33 | @pytest.mark.parametrize( 34 | "api_key, custom_headers, expected", 35 | ( 36 | ("testKey", None, {"Authorization": "Bearer testKey"}), 37 | ( 38 | "testKey", 39 | {"header_key_1": "header_value_1", "header_key_2": "header_value_2"}, 40 | { 41 | "Authorization": "Bearer testKey", 42 | "header_key_1": "header_value_1", 43 | "header_key_2": "header_value_2", 44 | }, 45 | ), 46 | ( 47 | None, 48 | {"header_key_1": "header_value_1", "header_key_2": "header_value_2"}, 49 | { 50 | "header_key_1": "header_value_1", 51 | "header_key_2": "header_value_2", 52 | }, 53 | ), 54 | (None, None, {}), 55 | ), 56 | ) 57 | def test_headers(api_key, custom_headers, expected): 58 | client = meilisearch.Client("127.0.0.1:7700", api_key=api_key, custom_headers=custom_headers) 59 | 60 | assert client.http.headers.items() >= expected.items() 61 | -------------------------------------------------------------------------------- /tests/client/test_client_dumps.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | 4 | def test_dump_creation(client, index_with_documents): 5 | """Tests the creation of a Meilisearch dump.""" 6 | index_with_documents("indexUID-dump-creation") 7 | dump = client.create_dump() 8 | client.wait_for_task(dump.task_uid) 9 | dump_status = client.get_task(dump.task_uid) 10 | assert dump_status.status == "succeeded" 11 | assert dump_status.type == "dumpCreation" 12 | -------------------------------------------------------------------------------- /tests/client/test_client_health_meilisearch.py: -------------------------------------------------------------------------------- 1 | import meilisearch 2 | 3 | 4 | def test_health(client): 5 | """Tests checking the health of the Meilisearch instance.""" 6 | response = client.health() 7 | assert response["status"] == "available" 8 | 9 | 10 | def test_is_healthy(client): 11 | """Tests checking if is_healthy return true when Meilisearch instance is available.""" 12 | response = client.is_healthy() 13 | assert response is True 14 | 15 | 16 | def test_is_healthy_bad_route(): 17 | """Tests checking if is_healthy returns false when trying to reach a bad URL.""" 18 | client = meilisearch.Client("http://wrongurl:1234", timeout=1) 19 | response = client.is_healthy() 20 | assert response is False 21 | -------------------------------------------------------------------------------- /tests/client/test_client_key_meilisearch.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | 5 | from meilisearch.errors import MeilisearchApiError 6 | from tests import common 7 | 8 | 9 | def test_get_keys_default(client): 10 | """Tests if search and admin keys have been generated and can be retrieved.""" 11 | keys = client.get_keys() 12 | assert len(keys.results) == 2 13 | assert keys.results[0].key is not None 14 | assert keys.results[1].key is not None 15 | 16 | 17 | def test_get_keys_with_parameters(client): 18 | """Tests if search and admin keys have been generated and can be retrieved.""" 19 | keys = client.get_keys({"limit": 1}) 20 | assert len(keys.results) == 1 21 | 22 | 23 | def test_get_key(client, test_key): 24 | """Tests if a key can be retrieved.""" 25 | key = client.get_key(test_key.key) 26 | assert key.created_at is not None 27 | 28 | 29 | def test_get_key_inexistent(client): 30 | """Tests getting a key that does not exists.""" 31 | with pytest.raises(Exception): 32 | client.get_key("No existing key") 33 | 34 | 35 | def test_create_keys_default(client, test_key_info): 36 | """Tests the creation of a key with no optional argument.""" 37 | key = client.create_key(test_key_info) 38 | print(key) 39 | assert key.key is not None 40 | assert key.name is not None 41 | assert key.expires_at is None 42 | assert key.created_at is not None 43 | assert key.updated_at is not None 44 | assert key.key is not None 45 | assert key.actions == test_key_info["actions"] 46 | assert key.indexes == test_key_info["indexes"] 47 | 48 | 49 | def test_create_keys_without_desc(client, test_nondescript_key_info): 50 | """Tests the creation of a key with no optional argument.""" 51 | key = client.create_key(test_nondescript_key_info) 52 | print(key) 53 | 54 | assert key.name == "keyWithoutDescription" 55 | assert key.description is None 56 | 57 | 58 | def test_create_keys_with_options(client, test_key_info): 59 | """Tests the creation of a key with arguments.""" 60 | key = client.create_key( 61 | options={ 62 | "description": test_key_info["description"], 63 | "actions": test_key_info["actions"], 64 | "indexes": test_key_info["indexes"], 65 | "uid": "82acc342-d2df-4291-84bd-8400d3f05f06", 66 | "expiresAt": datetime(2030, 6, 4, 21, 8, 12, 32).isoformat() + "Z", 67 | } 68 | ) 69 | assert key.key is not None 70 | assert key.name is None 71 | assert key.description == test_key_info["description"] 72 | assert key.expires_at is not None 73 | assert key.created_at is not None 74 | assert key.updated_at is not None 75 | assert key.actions == test_key_info["actions"] 76 | assert key.indexes == test_key_info["indexes"] 77 | 78 | 79 | def test_create_keys_with_wildcarded_actions(client, test_key_info): 80 | """Tests the creation of a key with an action which contains a wildcard.""" 81 | key = client.create_key( 82 | options={ 83 | "description": test_key_info["description"], 84 | "actions": ["documents.*"], 85 | "indexes": test_key_info["indexes"], 86 | "expiresAt": None, 87 | } 88 | ) 89 | 90 | assert key.actions == ["documents.*"] 91 | 92 | 93 | def test_create_keys_without_actions(client): 94 | """Tests the creation of a key with missing arguments.""" 95 | with pytest.raises(MeilisearchApiError): 96 | client.create_key(options={"indexes": [common.INDEX_UID]}) 97 | 98 | 99 | def test_update_keys(client, test_key_info): 100 | """Tests updating a key.""" 101 | key = client.create_key(test_key_info) 102 | assert key.name == test_key_info["name"] 103 | update_key = client.update_key(key_or_uid=key.key, options={"name": "keyTest"}) 104 | assert update_key.key is not None 105 | assert update_key.expires_at is None 106 | assert update_key.name == "keyTest" 107 | 108 | 109 | def test_delete_key(client, test_key): 110 | """Tests deleting a key.""" 111 | resp = client.delete_key(test_key.key) 112 | assert resp == 204 113 | with pytest.raises(MeilisearchApiError): 114 | client.get_key(test_key.key) 115 | 116 | 117 | def test_delete_key_inexisting(client): 118 | """Tests deleting a key that does not exists.""" 119 | with pytest.raises(MeilisearchApiError): 120 | client.delete_key("No existing key") 121 | -------------------------------------------------------------------------------- /tests/client/test_client_multi_search_meilisearch.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from meilisearch.errors import MeilisearchApiError 4 | from tests.common import INDEX_UID 5 | 6 | 7 | def test_basic_multi_search(client, empty_index): 8 | """Tests multi-search on two indexes.""" 9 | empty_index("indexA") 10 | empty_index("indexB") 11 | response = client.multi_search( 12 | [{"indexUid": "indexA", "q": ""}, {"indexUid": "indexB", "q": ""}] 13 | ) 14 | 15 | assert isinstance(response, dict) 16 | assert response["results"][0]["indexUid"] == "indexA" 17 | assert response["results"][1]["indexUid"] == "indexB" 18 | assert response["results"][0]["limit"] == 20 19 | assert response["results"][1]["limit"] == 20 20 | 21 | 22 | def test_multi_search_one_index(client, empty_index): 23 | """Tests multi-search on a simple query.""" 24 | empty_index("indexA") 25 | response = client.multi_search([{"indexUid": "indexA", "q": ""}]) 26 | 27 | assert isinstance(response, dict) 28 | assert response["results"][0]["indexUid"] == "indexA" 29 | assert response["results"][0]["limit"] == 20 30 | 31 | 32 | def test_multi_search_on_no_index(client): 33 | """Tests multi-search on a non existing index.""" 34 | with pytest.raises(MeilisearchApiError): 35 | client.multi_search([{"indexUid": "indexDoesNotExist", "q": ""}]) 36 | 37 | 38 | def test_multi_search_with_no_value_in_federation(client, empty_index, index_with_documents): 39 | """Tests multi-search with federation, but no value""" 40 | index_with_documents() 41 | empty_index("indexB") 42 | response = client.multi_search( 43 | [{"indexUid": INDEX_UID, "q": ""}, {"indexUid": "indexB", "q": ""}], {} 44 | ) 45 | assert "results" not in response 46 | assert len(response["hits"]) > 0 47 | assert "_federation" in response["hits"][0] 48 | assert response["limit"] == 20 49 | assert response["offset"] == 0 50 | 51 | 52 | def test_multi_search_with_offset_and_limit_in_federation(client, index_with_documents): 53 | """Tests multi-search with federation, with offset and limit value""" 54 | index_with_documents() 55 | response = client.multi_search([{"indexUid": INDEX_UID, "q": ""}], {"offset": 2, "limit": 2}) 56 | 57 | assert "results" not in response 58 | assert len(response["hits"]) == 2 59 | assert "_federation" in response["hits"][0] 60 | assert response["limit"] == 2 61 | assert response["offset"] == 2 62 | 63 | 64 | def test_multi_search_with_federation_options(client, index_with_documents): 65 | """Tests multi-search with federation, with federation options""" 66 | index_with_documents() 67 | response = client.multi_search( 68 | [{"indexUid": INDEX_UID, "q": "", "federationOptions": {"weight": 0.99}}], {"limit": 2} 69 | ) 70 | 71 | assert "results" not in response 72 | assert isinstance(response["hits"], list) 73 | assert len(response["hits"]) == 2 74 | assert response["hits"][0]["_federation"]["indexUid"] == INDEX_UID 75 | assert response["hits"][0]["_federation"]["weightedRankingScore"] >= 0.99 76 | assert response["limit"] == 2 77 | assert response["offset"] == 0 78 | -------------------------------------------------------------------------------- /tests/client/test_client_stats_meilisearch.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.usefixtures("indexes_sample") 5 | def test_get_all_stats(client): 6 | """Tests getting all stats.""" 7 | response = client.get_all_stats() 8 | assert isinstance(response, dict) 9 | assert "databaseSize" in response 10 | assert isinstance(response["databaseSize"], int) 11 | assert "lastUpdate" in response 12 | assert "indexes" in response 13 | assert "indexUID" in response["indexes"] 14 | assert "indexUID2" in response["indexes"] 15 | -------------------------------------------------------------------------------- /tests/client/test_client_swap_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import pytest 4 | 5 | from meilisearch.errors import MeilisearchApiError 6 | 7 | 8 | def test_swap_indexes(client, empty_index): 9 | """Tests swap two indexes.""" 10 | indexA = empty_index("index_A") 11 | indexB = empty_index("index_B") 12 | taskA = indexA.add_documents([{"id": 1, "title": "index_A"}]) 13 | taskB = indexB.add_documents([{"id": 1, "title": "index_B"}]) 14 | client.wait_for_task(taskA.task_uid) 15 | client.wait_for_task(taskB.task_uid) 16 | swapTask = client.swap_indexes( 17 | [ 18 | { 19 | "indexes": [indexA.uid, indexB.uid], 20 | }, 21 | ] 22 | ) 23 | task = client.wait_for_task(swapTask.task_uid) 24 | docA = client.index(indexA.uid).get_document(1) 25 | docB = client.index(indexB.uid).get_document(1) 26 | 27 | assert docA.title == indexB.uid 28 | assert docB.title == indexA.uid 29 | assert task.type == "indexSwap" 30 | assert "swaps" in task.details 31 | 32 | 33 | def test_swap_indexes_with_one_that_does_not_exist(client, empty_index): 34 | """Tests swap indexes with one that does not exist.""" 35 | index = empty_index("index_A") 36 | swapTask = client.swap_indexes( 37 | [ 38 | { 39 | "indexes": [index.uid, "does_not_exist"], 40 | }, 41 | ] 42 | ) 43 | task = client.wait_for_task(swapTask.task_uid) 44 | 45 | assert swapTask.type == "indexSwap" 46 | assert task.error["code"] == "index_not_found" 47 | 48 | 49 | def test_swap_indexes_with_itself(client, empty_index): 50 | """Tests swap indexes with itself.""" 51 | index = empty_index() 52 | with pytest.raises(MeilisearchApiError): 53 | client.swap_indexes( 54 | [ 55 | { 56 | "indexes": [index.uid, index.uid], 57 | }, 58 | ] 59 | ) 60 | -------------------------------------------------------------------------------- /tests/client/test_client_task_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import pytest 4 | 5 | from meilisearch.models.task import TaskInfo 6 | from tests import common 7 | 8 | 9 | def test_get_tasks_default(client): 10 | """Tests getting the global tasks list.""" 11 | tasks = client.get_tasks() 12 | assert len(tasks.results) >= 1 13 | 14 | 15 | def test_get_tasks(client, empty_index): 16 | """Tests getting the global tasks list after populating an index.""" 17 | current_tasks = client.get_tasks() 18 | pre_count = current_tasks.from_ 19 | empty_index() 20 | tasks = client.get_tasks() 21 | assert tasks.from_ == pre_count + 1 22 | 23 | 24 | def test_get_tasks_empty_parameters(client): 25 | """Tests getting the global tasks list after populating an index.""" 26 | tasks = client.get_tasks({}) 27 | assert isinstance(tasks.results, list) 28 | 29 | 30 | def test_get_tasks_with_parameters(client, empty_index): 31 | """Tests getting the global tasks list after populating an index.""" 32 | empty_index() 33 | tasks = client.get_tasks({"limit": 1}) 34 | assert len(tasks.results) == 1 35 | 36 | 37 | def test_get_tasks_with_all_plural_parameters(client, empty_index): 38 | """Tests getting the global tasks list after populating an index.""" 39 | empty_index() 40 | tasks = client.get_tasks( 41 | {"indexUids": [common.INDEX_UID], "statuses": ["succeeded"], "types": ["indexCreation"]} 42 | ) 43 | assert len(tasks.results) >= 1 44 | 45 | 46 | def test_get_tasks_with_date_parameters(client, empty_index): 47 | """Tests getting the global tasks list after populating an index.""" 48 | empty_index() 49 | tasks = client.get_tasks( 50 | { 51 | "beforeEnqueuedAt": "2042-04-02T00:42:42Z", 52 | "beforeStartedAt": "2042-04-02T00:42:42Z", 53 | "beforeFinishedAt": "2042-04-02T00:42:42Z", 54 | } 55 | ) 56 | assert len(tasks.results) > 1 57 | 58 | 59 | def test_get_tasks_with_index_uid(client, empty_index): 60 | """Tests getting the global tasks list after populating an index.""" 61 | empty_index() 62 | tasks = client.get_tasks({"limit": 1, "indexUids": [common.INDEX_UID]}) 63 | assert len(tasks.results) == 1 64 | 65 | 66 | def test_get_task(client): 67 | """Tests getting the tasks list of an empty index.""" 68 | response = client.create_index(uid=common.INDEX_UID) 69 | client.wait_for_task(response.task_uid) 70 | task = client.get_task(response.task_uid) 71 | task_dict = task.__dict__ 72 | assert "uid" in task_dict 73 | assert "index_uid" in task_dict 74 | assert "status" in task_dict 75 | assert "type" in task_dict 76 | assert "duration" in task_dict 77 | assert "enqueued_at" in task_dict 78 | assert "finished_at" in task_dict 79 | assert "details" in task_dict 80 | assert "started_at" in task_dict 81 | 82 | 83 | def test_get_task_inexistent(client): 84 | """Tests getting a task that does not exists.""" 85 | with pytest.raises(Exception): 86 | client.get_task("abc") 87 | 88 | 89 | @pytest.fixture 90 | def create_tasks(empty_index, small_movies): 91 | """Ensures there are some tasks present for testing.""" 92 | index = empty_index() 93 | index.update_ranking_rules(["typo", "exactness"]) 94 | index.reset_ranking_rules() 95 | index.add_documents(small_movies) 96 | index.add_documents(small_movies) 97 | 98 | 99 | @pytest.mark.usefixtures("create_tasks") 100 | def test_cancel_tasks(client): 101 | """Tests cancel a task with uid 1.""" 102 | task = client.cancel_tasks({"uids": ["1", "2"]}) 103 | client.wait_for_task(task.task_uid) 104 | tasks = client.get_tasks({"types": "taskCancelation"}) 105 | 106 | assert isinstance(task, TaskInfo) 107 | assert task.task_uid is not None 108 | assert task.index_uid is None 109 | assert task.type == "taskCancelation" 110 | assert "uids" in tasks.results[0].details["originalFilter"] 111 | assert "uids=1%2C2" in tasks.results[0].details["originalFilter"] 112 | 113 | 114 | @pytest.mark.usefixtures("create_tasks") 115 | def test_cancel_every_task(client): 116 | """Tests cancel every task.""" 117 | task = client.cancel_tasks({"statuses": ["enqueued", "processing"]}) 118 | client.wait_for_task(task.task_uid) 119 | tasks = client.get_tasks({"types": "taskCancelation"}) 120 | 121 | assert isinstance(task, TaskInfo) 122 | assert task.task_uid is not None 123 | assert task.index_uid is None 124 | assert task.type == "taskCancelation" 125 | assert "statuses=enqueued%2Cprocessing" in tasks.results[0].details["originalFilter"] 126 | 127 | 128 | def test_delete_tasks_by_uid(client, empty_index, small_movies): 129 | """Tests getting a task of an inexistent operation.""" 130 | index = empty_index() 131 | task_addition = index.add_documents(small_movies) 132 | task_deleted = client.delete_tasks({"uids": task_addition.task_uid}) 133 | client.wait_for_task(task_deleted.task_uid) 134 | with pytest.raises(Exception): 135 | client.get_task(task_addition.task_uid) 136 | task = client.get_task(task_deleted.task_uid) 137 | 138 | assert isinstance(task_deleted, TaskInfo) 139 | assert task_deleted.task_uid is not None 140 | assert task_deleted.index_uid is None 141 | assert task_deleted.type == "taskDeletion" 142 | assert "uids" in task.details["originalFilter"] 143 | assert f"uids={task_addition.task_uid}" in task.details["originalFilter"] 144 | 145 | 146 | def test_delete_tasks_by_filter(client): 147 | task = client.delete_tasks({"statuses": ["succeeded", "failed", "canceled"]}) 148 | client.wait_for_task(task.task_uid) 149 | tasks_after = client.get_tasks() 150 | 151 | assert isinstance(task, TaskInfo) 152 | assert task.task_uid is not None 153 | assert task.index_uid is None 154 | assert task.type == "taskDeletion" 155 | assert len(tasks_after.results) >= 1 156 | assert ( 157 | "statuses=succeeded%2Cfailed%2Ccanceled" in tasks_after.results[0].details["originalFilter"] 158 | ) 159 | 160 | 161 | @pytest.mark.usefixtures("create_tasks") 162 | def test_get_tasks_in_reverse(client): 163 | """Tests getting the global tasks list in reverse.""" 164 | tasks = client.get_tasks({}) 165 | reverse_tasks = client.get_tasks({"reverse": "true"}) 166 | 167 | assert reverse_tasks.results[0] == tasks.results[-1] 168 | 169 | 170 | def test_get_batches_default(client): 171 | """Tests getting the batches.""" 172 | batches = client.get_batches() 173 | assert len(batches.results) >= 1 174 | 175 | 176 | @pytest.mark.usefixtures("create_tasks") 177 | def test_get_batches_with_parameters(client): 178 | """Tests getting batches with a parameter (empty or otherwise).""" 179 | rev_batches = client.get_batches({"reverse": "true"}) 180 | batches = client.get_batches({}) 181 | 182 | assert len(batches.results) > 1 183 | assert rev_batches.results[0].uid == batches.results[-1].uid 184 | 185 | 186 | def test_get_batch(client): 187 | """Tests getting the details of a batch.""" 188 | batches = client.get_batches({"limit": 1}) 189 | uid = batches.results[0].uid 190 | batch = client.get_batch(uid) 191 | assert batch.uid == uid 192 | -------------------------------------------------------------------------------- /tests/client/test_client_tenant_token.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import datetime 4 | from copy import deepcopy 5 | 6 | import pytest 7 | 8 | import meilisearch 9 | from meilisearch.errors import MeilisearchApiError 10 | from tests import BASE_URL 11 | 12 | 13 | def test_generate_tenant_token_with_search_rules(get_private_key, index_with_documents): 14 | """Tests create a tenant token with only search rules.""" 15 | index_with_documents() 16 | client = meilisearch.Client(BASE_URL, get_private_key.key) 17 | 18 | token = client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules=["*"]) 19 | 20 | token_client = meilisearch.Client(BASE_URL, token) 21 | response = token_client.index("indexUID").search("", {"limit": 5}) 22 | assert isinstance(response, dict) 23 | assert len(response["hits"]) == 5 24 | assert response["query"] == "" 25 | 26 | 27 | def test_generate_tenant_token_with_search_rules_on_one_index(get_private_key, empty_index): 28 | """Tests create a tenant token with search rules set for one index.""" 29 | empty_index() 30 | empty_index("tenant_token") 31 | client = meilisearch.Client(BASE_URL, get_private_key.key) 32 | 33 | token = client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules=["indexUID"]) 34 | 35 | token_client = meilisearch.Client(BASE_URL, token) 36 | response = token_client.index("indexUID").search("") 37 | assert isinstance(response, dict) 38 | assert response["query"] == "" 39 | with pytest.raises(MeilisearchApiError): 40 | response = token_client.index("tenant_token").search("") 41 | 42 | 43 | def test_generate_tenant_token_with_api_key(client, get_private_key, empty_index): 44 | """Tests create a tenant token with search rules and an api key.""" 45 | empty_index() 46 | token = client.generate_tenant_token( 47 | api_key_uid=get_private_key.uid, 48 | search_rules=["*"], 49 | api_key=get_private_key.key, 50 | ) 51 | 52 | token_client = meilisearch.Client(BASE_URL, token) 53 | response = token_client.index("indexUID").search("") 54 | assert isinstance(response, dict) 55 | assert response["query"] == "" 56 | 57 | 58 | def test_generate_tenant_token_no_api_key(client, get_private_key): 59 | # Make a copy of the client so we don't change the value in the session scoped fixture as this 60 | # would affect other tests. 61 | client_copy = deepcopy(client) 62 | client_copy.config.api_key = None 63 | with pytest.raises(ValueError) as exc: 64 | client_copy.generate_tenant_token( 65 | api_key_uid=get_private_key.uid, 66 | search_rules=["*"], 67 | ) 68 | 69 | assert "An api key is required" in str(exc.value) 70 | 71 | 72 | def test_generate_tenant_token_with_expires_at(client, get_private_key, empty_index): 73 | """Tests create a tenant token with search rules and expiration date.""" 74 | empty_index() 75 | client = meilisearch.Client(BASE_URL, get_private_key.key) 76 | tomorrow = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=1) 77 | 78 | token = client.generate_tenant_token( 79 | api_key_uid=get_private_key.uid, search_rules=["*"], expires_at=tomorrow 80 | ) 81 | 82 | token_client = meilisearch.Client(BASE_URL, token) 83 | response = token_client.index("indexUID").search("") 84 | assert isinstance(response, dict) 85 | assert response["query"] == "" 86 | 87 | 88 | def test_generate_tenant_token_with_empty_search_rules_in_list(get_private_key): 89 | """Tests create a tenant token without search rules.""" 90 | client = meilisearch.Client(BASE_URL, get_private_key.key) 91 | 92 | with pytest.raises(Exception): 93 | client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules=[""]) 94 | 95 | 96 | def test_generate_tenant_token_without_search_rules_in_list(get_private_key): 97 | """Tests create a tenant token without search rules.""" 98 | client = meilisearch.Client(BASE_URL, get_private_key.key) 99 | 100 | with pytest.raises(Exception): 101 | client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules=[]) 102 | 103 | 104 | def test_generate_tenant_token_without_search_rules_in_dict(get_private_key): 105 | """Tests create a tenant token without search rules.""" 106 | client = meilisearch.Client(BASE_URL, get_private_key.key) 107 | 108 | with pytest.raises(Exception): 109 | client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules={}) 110 | 111 | 112 | def test_generate_tenant_token_with_empty_search_rules_in_dict(get_private_key): 113 | """Tests create a tenant token without search rules.""" 114 | client = meilisearch.Client(BASE_URL, get_private_key.key) 115 | 116 | with pytest.raises(Exception): 117 | client.generate_tenant_token(api_key_uid=get_private_key.uid, search_rules={""}) 118 | 119 | 120 | def test_generate_tenant_token_with_bad_expires_at(client, get_private_key): 121 | """Tests create a tenant token with a bad expires at.""" 122 | client = meilisearch.Client(BASE_URL, get_private_key.key) 123 | 124 | yesterday = datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=-1) 125 | 126 | with pytest.raises(Exception): 127 | client.generate_tenant_token( 128 | api_key_uid=get_private_key.uid, search_rules=["*"], expires_at=yesterday 129 | ) 130 | 131 | 132 | def test_generate_tenant_token_with_no_api_key(client): 133 | """Tests create a tenant token with no api key.""" 134 | client = meilisearch.Client(BASE_URL) 135 | 136 | with pytest.raises(Exception): 137 | client.generate_tenant_token(search_rules=["*"]) # pylint: disable=no-value-for-parameter 138 | 139 | 140 | def test_generate_tenant_token_with_no_uid(client, get_private_key): 141 | """Tests create a tenant token with no uid.""" 142 | client = meilisearch.Client(BASE_URL, get_private_key.key) 143 | 144 | with pytest.raises(Exception): 145 | client.generate_tenant_token(api_key_uid=None, search_rules=["*"]) 146 | -------------------------------------------------------------------------------- /tests/client/test_client_update_document_by_functions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.common import INDEX_UID 4 | 5 | 6 | @pytest.mark.usefixtures("enable_edit_documents_by_function") 7 | def test_update_document_by_function(client, index_with_documents): 8 | """Delete the document with id and update document title""" 9 | index_with_documents() 10 | response = client.update_documents_by_function( 11 | INDEX_UID, 12 | {"function": 'if doc.id == "522681" {doc = () } else {doc.title = `* ${doc.title} *`}'}, 13 | ) 14 | 15 | assert isinstance(response, dict) 16 | assert isinstance(response["taskUid"], int) 17 | assert response["indexUid"] == INDEX_UID 18 | -------------------------------------------------------------------------------- /tests/client/test_client_version_meilisearch.py: -------------------------------------------------------------------------------- 1 | def test_get_version(client): 2 | """Tests getting the version of the Meilisearch instance.""" 3 | response = client.get_version() 4 | assert "pkgVersion" in response 5 | assert "commitSha" in response 6 | assert "commitDate" in response 7 | -------------------------------------------------------------------------------- /tests/client/test_clinet_snapshots.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | 4 | def test_snapshot_creation(client, index_with_documents): 5 | """Tests the creation of a Meilisearch snapshot.""" 6 | index_with_documents("indexUID-snapshot-creation") 7 | snapshot = client.create_snapshot() 8 | client.wait_for_task(snapshot.task_uid) 9 | snapshot_status = client.get_task(snapshot.task_uid) 10 | assert snapshot_status.status == "succeeded" 11 | assert snapshot_status.type == "snapshotCreation" 12 | -------------------------------------------------------------------------------- /tests/client/test_http_requests.py: -------------------------------------------------------------------------------- 1 | from meilisearch._httprequests import HttpRequests 2 | from meilisearch.config import Config 3 | from meilisearch.version import qualified_version 4 | from tests import BASE_URL, MASTER_KEY 5 | 6 | 7 | def test_get_headers_from_http_requests_instance(): 8 | """Tests getting defined headers from instance in HttpRequests.""" 9 | config = Config(BASE_URL, MASTER_KEY, timeout=None) 10 | http = HttpRequests(config=config) 11 | 12 | assert http.headers["Authorization"] == f"Bearer {MASTER_KEY}" 13 | assert http.headers["User-Agent"] == qualified_version() 14 | 15 | 16 | def test_get_headers_with_multiple_user_agent(): 17 | """Tests getting defined headers from instance in HttpRequests.""" 18 | config = Config( 19 | BASE_URL, 20 | MASTER_KEY, 21 | timeout=None, 22 | client_agents=("Meilisearch Package1 (v1.1.1)", "Meilisearch Package2 (v2.2.2)"), 23 | ) 24 | http = HttpRequests(config=config) 25 | 26 | assert http.headers["Authorization"] == f"Bearer {MASTER_KEY}" 27 | assert ( 28 | http.headers["User-Agent"] 29 | == qualified_version() + ";Meilisearch Package1 (v1.1.1);Meilisearch Package2 (v2.2.2)" 30 | ) 31 | -------------------------------------------------------------------------------- /tests/client/test_version.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import re 4 | 5 | from meilisearch.version import __version__, qualified_version 6 | 7 | 8 | def test_get_version(): 9 | assert re.match(r"^(\d+\.)?(\d+\.)?(\*|\d+)$", __version__) 10 | 11 | 12 | def test_get_qualified_version(): 13 | assert qualified_version() == f"Meilisearch Python (v{__version__})" 14 | -------------------------------------------------------------------------------- /tests/common.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | MASTER_KEY = "masterKey" 4 | BASE_URL = os.getenv("MEILISEARCH_URL", "http://127.0.0.1:7700") 5 | 6 | INDEX_UID = "indexUID" 7 | INDEX_UID2 = "indexUID2" 8 | INDEX_UID3 = "indexUID3" 9 | INDEX_UID4 = "indexUID4" 10 | 11 | INDEX_FIXTURE = [ 12 | {"uid": INDEX_UID}, 13 | {"uid": INDEX_UID2, "options": {"primaryKey": "book_id"}}, 14 | {"uid": INDEX_UID3, "options": {"uid": "wrong", "primaryKey": "book_id"}}, 15 | ] 16 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | import json 3 | from typing import Optional 4 | 5 | import requests 6 | from pytest import fixture 7 | 8 | import meilisearch 9 | from meilisearch.errors import MeilisearchApiError 10 | from meilisearch.models.embedders import OpenAiEmbedder, UserProvidedEmbedder 11 | from tests import common 12 | 13 | 14 | @fixture(scope="session") 15 | def client(): 16 | return meilisearch.Client(common.BASE_URL, common.MASTER_KEY) 17 | 18 | 19 | @fixture(autouse=True) 20 | def clear_indexes(client): 21 | """ 22 | Auto-clears the indexes after each test function run. 23 | Makes all the test functions independent. 24 | """ 25 | # Yields back to the test function. 26 | yield 27 | # Deletes all the indexes in the Meilisearch instance. 28 | indexes = client.get_indexes() 29 | for index in indexes["results"]: 30 | task = client.index(index.uid).delete() 31 | client.wait_for_task(task.task_uid) 32 | 33 | 34 | @fixture(autouse=True) 35 | def clear_all_tasks(client): 36 | """ 37 | Auto-clears the tasks after each test function run. 38 | Makes all the test functions independent. 39 | """ 40 | client.delete_tasks({"statuses": ["succeeded", "failed", "canceled"]}) 41 | 42 | 43 | @fixture(scope="function") 44 | def indexes_sample(client): 45 | indexes = [] 46 | for index_args in common.INDEX_FIXTURE: 47 | task = client.create_index(**index_args) 48 | client.wait_for_task(task.task_uid) 49 | indexes.append(client.get_index(index_args["uid"])) 50 | # Yields the indexes to the test to make them accessible. 51 | yield indexes 52 | 53 | 54 | @fixture(scope="session") 55 | def small_movies(): 56 | """ 57 | Runs once per session. Provides the content of small_movies.json. 58 | """ 59 | with open("./datasets/small_movies.json", encoding="utf-8") as movie_file: 60 | yield json.loads(movie_file.read()) 61 | 62 | 63 | @fixture(scope="session") 64 | def small_movies_json_file(): 65 | """ 66 | Runs once per session. Provides the content of small_movies.json from read. 67 | """ 68 | with open("./datasets/small_movies.json", encoding="utf-8") as movie_json_file: 69 | return movie_json_file.read().encode("utf-8") 70 | 71 | 72 | @fixture(scope="session") 73 | def songs_csv(): 74 | """ 75 | Runs once per session. Provides the content of songs.csv from read. 76 | """ 77 | with open("./datasets/songs.csv", encoding="utf-8") as song_csv_file: 78 | return song_csv_file.read().encode("utf-8") 79 | 80 | 81 | @fixture(scope="session") 82 | def songs_csv_custom_separator(): 83 | """ 84 | Runs once per session. Provides the content of songs_custom_delimiter.csv from read. 85 | """ 86 | with open("./datasets/songs_custom_delimiter.csv", encoding="utf-8") as song_csv_file: 87 | return song_csv_file.read().encode("utf-8") 88 | 89 | 90 | @fixture(scope="session") 91 | def songs_ndjson(): 92 | """ 93 | Runs once per session. Provides the content of songs.ndjson from read. 94 | """ 95 | with open("./datasets/songs.ndjson", encoding="utf-8") as song_ndjson_file: 96 | return song_ndjson_file.read().encode("utf-8") 97 | 98 | 99 | @fixture(scope="session") 100 | def nested_movies(): 101 | """ 102 | Runs once per session. Provides the content of nested_movies.json. 103 | """ 104 | with open("./datasets/nested_movies.json", encoding="utf-8") as nested_movie_file: 105 | yield json.loads(nested_movie_file.read()) 106 | 107 | 108 | @fixture(scope="function") 109 | def empty_index(client, index_uid: Optional[str] = None): 110 | index_uid = index_uid if index_uid else common.INDEX_UID 111 | 112 | def index_maker(index_uid=index_uid): 113 | task = client.create_index(uid=index_uid) 114 | client.wait_for_task(task.task_uid) 115 | return client.get_index(uid=index_uid) 116 | 117 | return index_maker 118 | 119 | 120 | @fixture(scope="function") 121 | def index_with_documents(empty_index, small_movies): 122 | def index_maker(index_uid=common.INDEX_UID, documents=small_movies): 123 | index = empty_index(index_uid) 124 | task = index.add_documents(documents) 125 | index.wait_for_task(task.task_uid) 126 | return index 127 | 128 | return index_maker 129 | 130 | 131 | @fixture(scope="function") 132 | def index_with_documents_and_vectors(empty_index, small_movies): 133 | small_movies[0]["_vectors"] = {"default": [0.1, 0.2]} 134 | for movie in small_movies[1:]: 135 | movie["_vectors"] = {"default": [0.9, 0.9]} 136 | 137 | def index_maker(index_uid=common.INDEX_UID, documents=small_movies): 138 | index = empty_index(index_uid) 139 | settings_update_task = index.update_embedders( 140 | { 141 | "default": { 142 | "source": "userProvided", 143 | "dimensions": 2, 144 | } 145 | } 146 | ) 147 | index.wait_for_task(settings_update_task.task_uid) 148 | document_addition_task = index.add_documents(documents) 149 | index.wait_for_task(document_addition_task.task_uid) 150 | return index 151 | 152 | return index_maker 153 | 154 | 155 | @fixture(scope="function") 156 | def index_with_documents_and_facets(empty_index, small_movies): 157 | def index_maker(index_uid=common.INDEX_UID, documents=small_movies): 158 | index = empty_index(index_uid) 159 | task_1 = index.update_filterable_attributes(["genre"]) 160 | index.wait_for_task(task_1.task_uid) 161 | task_2 = index.add_documents(documents) 162 | index.wait_for_task(task_2.task_uid) 163 | return index 164 | 165 | return index_maker 166 | 167 | 168 | @fixture(scope="function") 169 | def test_key(client): 170 | key_info = { 171 | "description": "test", 172 | "actions": ["search"], 173 | "indexes": ["movies"], 174 | "expiresAt": None, 175 | } 176 | 177 | key = client.create_key(key_info) 178 | 179 | yield key 180 | 181 | try: 182 | client.delete_key(key.key) 183 | except MeilisearchApiError: 184 | pass 185 | 186 | 187 | @fixture(scope="function") 188 | def test_key_info(client): 189 | key_info = { 190 | "name": "testKeyName", 191 | "description": "test", 192 | "actions": ["search"], 193 | "indexes": [common.INDEX_UID], 194 | "expiresAt": None, 195 | } 196 | 197 | yield key_info 198 | 199 | try: 200 | keys = client.get_keys().results 201 | key = next(x for x in keys if x.description == key_info["description"]) 202 | client.delete_key(key.key) 203 | except MeilisearchApiError: 204 | pass 205 | except StopIteration: 206 | pass 207 | 208 | 209 | @fixture(scope="function") 210 | def test_nondescript_key_info(client): 211 | key_info = { 212 | "name": "keyWithoutDescription", 213 | "actions": ["search"], 214 | "indexes": [common.INDEX_UID], 215 | "expiresAt": None, 216 | } 217 | 218 | yield key_info 219 | 220 | try: 221 | keys = client.get_keys().results 222 | key = next(x for x in keys if x.name == key_info["name"]) 223 | client.delete_key(key.key) 224 | except MeilisearchApiError: 225 | pass 226 | except StopIteration: 227 | pass 228 | 229 | 230 | @fixture(scope="function") 231 | def get_private_key(client): 232 | keys = client.get_keys().results 233 | key = next(x for x in keys if "Default Search API" in x.name) 234 | return key 235 | 236 | 237 | @fixture 238 | def enable_vector_search(): 239 | requests.patch( 240 | f"{common.BASE_URL}/experimental-features", 241 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 242 | json={"vectorStore": True}, 243 | timeout=10, 244 | ) 245 | yield 246 | requests.patch( 247 | f"{common.BASE_URL}/experimental-features", 248 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 249 | json={"vectorStore": False}, 250 | timeout=10, 251 | ) 252 | 253 | 254 | @fixture 255 | def enable_edit_documents_by_function(): 256 | requests.patch( 257 | f"{common.BASE_URL}/experimental-features", 258 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 259 | json={"editDocumentsByFunction": True}, 260 | timeout=10, 261 | ) 262 | yield 263 | requests.patch( 264 | f"{common.BASE_URL}/experimental-features", 265 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 266 | json={"editDocumentsByFunction": False}, 267 | timeout=10, 268 | ) 269 | 270 | 271 | @fixture 272 | def new_embedders(): 273 | return { 274 | "default": UserProvidedEmbedder(dimensions=1).model_dump(by_alias=True), 275 | "open_ai": OpenAiEmbedder().model_dump(by_alias=True), 276 | } 277 | 278 | 279 | @fixture 280 | def enable_composite_embedders(): 281 | requests.patch( 282 | f"{common.BASE_URL}/experimental-features", 283 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 284 | json={"compositeEmbedders": True}, 285 | timeout=10, 286 | ) 287 | yield 288 | requests.patch( 289 | f"{common.BASE_URL}/experimental-features", 290 | headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, 291 | json={"compositeEmbedders": False}, 292 | timeout=10, 293 | ) 294 | -------------------------------------------------------------------------------- /tests/errors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/tests/errors/__init__.py -------------------------------------------------------------------------------- /tests/errors/test_api_error_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | import requests 7 | 8 | import meilisearch 9 | from meilisearch.errors import MeilisearchApiError, version_error_hint_message 10 | from tests import BASE_URL, MASTER_KEY 11 | 12 | 13 | def test_meilisearch_api_error_no_master_key(): 14 | client = meilisearch.Client(BASE_URL) 15 | with pytest.raises(MeilisearchApiError): 16 | client.create_index("some_index") 17 | 18 | 19 | def test_meilisearch_api_error_wrong_master_key(): 20 | client = meilisearch.Client(BASE_URL, MASTER_KEY + "123") 21 | with pytest.raises(MeilisearchApiError): 22 | client.create_index("some_index") 23 | 24 | 25 | @patch("requests.post") 26 | def test_meilisearch_api_error_no_code(mock_post): 27 | """Here to test for regressions related to https://github.com/meilisearch/meilisearch-python/issues/305.""" 28 | mock_post.configure_mock(__name__="post") 29 | mock_response = requests.models.Response() 30 | mock_response.status_code = 408 31 | mock_post.return_value = mock_response 32 | 33 | with pytest.raises(MeilisearchApiError): 34 | client = meilisearch.Client(BASE_URL, MASTER_KEY + "123") 35 | client.create_index("some_index") 36 | 37 | 38 | def test_version_error_hint_message(): 39 | mock_response = requests.models.Response() 40 | mock_response.status_code = 408 41 | 42 | class FakeClass: 43 | @version_error_hint_message 44 | def test_method(self): 45 | raise MeilisearchApiError("This is a test", mock_response) 46 | 47 | with pytest.raises(MeilisearchApiError) as e: 48 | fake = FakeClass() 49 | fake.test_method() 50 | 51 | assert ( 52 | "MeilisearchApiError. This is a test. Hint: It might not be working because you're not up to date with the Meilisearch version that test_method call requires." 53 | == str(e.value) 54 | ) 55 | -------------------------------------------------------------------------------- /tests/errors/test_communication_error_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | from unittest.mock import patch 4 | 5 | import pytest 6 | import requests 7 | 8 | import meilisearch 9 | from meilisearch.errors import MeilisearchCommunicationError 10 | from tests import MASTER_KEY 11 | 12 | 13 | @patch("requests.post") 14 | def test_meilisearch_communication_error_host(mock_post): 15 | mock_post.configure_mock(__name__="post") 16 | mock_post.side_effect = requests.exceptions.ConnectionError() 17 | client = meilisearch.Client("http://wrongurl:1234", MASTER_KEY) 18 | with pytest.raises(MeilisearchCommunicationError): 19 | client.create_index("some_index") 20 | -------------------------------------------------------------------------------- /tests/errors/test_timeout_error_meilisearch.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | import pytest 4 | import requests 5 | 6 | import meilisearch 7 | from meilisearch.errors import MeilisearchTimeoutError 8 | from tests import BASE_URL, MASTER_KEY 9 | 10 | 11 | @patch("requests.get") 12 | def test_client_timeout_error(mock_get): 13 | mock_get.configure_mock(__name__="get") 14 | mock_get.side_effect = requests.exceptions.Timeout() 15 | client = meilisearch.Client(BASE_URL, MASTER_KEY, timeout=1) 16 | 17 | with pytest.raises(MeilisearchTimeoutError): 18 | client.version() 19 | 20 | 21 | def test_client_timeout_set(): 22 | timeout = 1 23 | client = meilisearch.Client("http://wrongurl:1234", MASTER_KEY, timeout=timeout) 24 | 25 | with pytest.raises(Exception): 26 | client.health() 27 | 28 | assert client.config.timeout == timeout 29 | -------------------------------------------------------------------------------- /tests/index/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/tests/index/__init__.py -------------------------------------------------------------------------------- /tests/index/test_index.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | from datetime import datetime 4 | 5 | import pytest 6 | 7 | from meilisearch.client import Client 8 | from meilisearch.errors import MeilisearchApiError 9 | from meilisearch.index import Index 10 | from tests import BASE_URL, MASTER_KEY, common 11 | 12 | 13 | def test_create_index(empty_index): 14 | """Tests creating an index.""" 15 | index = empty_index() 16 | assert isinstance(index, Index) 17 | assert index.uid == common.INDEX_UID 18 | assert index.primary_key is None 19 | assert index.get_primary_key() is None 20 | 21 | 22 | def test_create_index_with_primary_key(client): 23 | """Tests creating an index with a primary key.""" 24 | response = client.create_index(uid=common.INDEX_UID2, options={"primaryKey": "book_id"}) 25 | client.wait_for_task(response.task_uid) 26 | index = client.get_index(uid=common.INDEX_UID2) 27 | assert isinstance(index, Index) 28 | assert index.uid == common.INDEX_UID2 29 | assert index.primary_key == "book_id" 30 | assert index.get_primary_key() == "book_id" 31 | 32 | 33 | def test_create_index_with_uid_in_options(client): 34 | """Tests creating an index with a primary key.""" 35 | response = client.create_index( 36 | uid=common.INDEX_UID3, options={"uid": "wrong", "primaryKey": "book_id"} 37 | ) 38 | client.wait_for_task(response.task_uid) 39 | index = client.get_index(uid=common.INDEX_UID3) 40 | assert isinstance(index, Index) 41 | assert index.uid == common.INDEX_UID3 42 | assert index.primary_key == "book_id" 43 | assert index.get_primary_key() == "book_id" 44 | 45 | 46 | @pytest.mark.usefixtures("indexes_sample") 47 | def test_get_indexes(client): 48 | """Tests getting all indexes.""" 49 | response = client.get_indexes() 50 | uids = [index.uid for index in response["results"]] 51 | assert isinstance(response["results"], list) 52 | assert common.INDEX_UID in uids 53 | assert common.INDEX_UID2 in uids 54 | assert common.INDEX_UID3 in uids 55 | assert len(response["results"]) == 3 56 | 57 | 58 | @pytest.mark.usefixtures("indexes_sample") 59 | def test_get_indexes_with_parameters(client): 60 | """Tests getting all indexes.""" 61 | response = client.get_indexes(parameters={"limit": 1, "offset": 1}) 62 | assert len(response["results"]) == 1 63 | 64 | 65 | @pytest.mark.usefixtures("indexes_sample") 66 | def test_get_raw_indexes(client): 67 | response = client.get_raw_indexes() 68 | uids = [index["uid"] for index in response["results"]] 69 | assert isinstance(response["results"], list) 70 | assert common.INDEX_UID in uids 71 | assert common.INDEX_UID2 in uids 72 | assert common.INDEX_UID3 in uids 73 | assert len(response["results"]) == 3 74 | 75 | 76 | @pytest.mark.usefixtures("indexes_sample") 77 | def test_get_raw_indexeswith_parameters(client): 78 | response = client.get_raw_indexes(parameters={"limit": 1, "offset": 1}) 79 | assert isinstance(response["results"], list) 80 | assert len(response["results"]) == 1 81 | 82 | 83 | def test_index_with_any_uid(client): 84 | index = client.index("anyUID") 85 | assert isinstance(index, Index) 86 | assert index.uid == "anyUID" 87 | assert index.primary_key is None 88 | assert index.created_at is None 89 | assert index.updated_at is None 90 | assert index.config is not None 91 | assert index.http is not None 92 | 93 | 94 | def test_index_with_none_uid(client): 95 | with pytest.raises(Exception): 96 | client.index(None) 97 | 98 | 99 | @pytest.mark.usefixtures("indexes_sample") 100 | def test_get_index_with_valid_uid(client): 101 | """Tests getting one index with uid.""" 102 | response = client.get_index(uid=common.INDEX_UID) 103 | assert isinstance(response, Index) 104 | assert response.uid == common.INDEX_UID 105 | assert isinstance(response.created_at, datetime) 106 | assert isinstance(response.updated_at, datetime) 107 | 108 | 109 | def test_get_index_with_none_uid(client): 110 | """Test raising an exception if the index UID is None.""" 111 | with pytest.raises(Exception): 112 | client.get_index(uid=None) 113 | 114 | 115 | def test_get_index_with_wrong_uid(client): 116 | """Tests get_index with an non-existing index.""" 117 | with pytest.raises(Exception): 118 | client.get_index(uid="wrongUID") 119 | 120 | 121 | @pytest.mark.usefixtures("indexes_sample") 122 | def test_get_raw_index_with_valid_uid(client): 123 | response = client.get_raw_index(uid=common.INDEX_UID) 124 | assert isinstance(response, dict) 125 | assert response["uid"] == common.INDEX_UID 126 | 127 | 128 | def test_get_raw_index_with_none_uid(client): 129 | with pytest.raises(Exception): 130 | client.get_raw_index(uid=None) 131 | 132 | 133 | def test_get_raw_index_with_wrong_uid(client): 134 | with pytest.raises(Exception): 135 | client.get_raw_index(uid="wrongUID") 136 | 137 | 138 | @pytest.mark.usefixtures("indexes_sample") 139 | def test_index_fetch_info(client): 140 | """Tests fetching the index info.""" 141 | index = client.index(uid=common.INDEX_UID) 142 | response = index.fetch_info() 143 | assert isinstance(response, Index) 144 | assert response.uid == common.INDEX_UID 145 | assert response.primary_key is None 146 | assert response.primary_key == index.primary_key 147 | assert response.primary_key == index.get_primary_key() 148 | 149 | 150 | @pytest.mark.usefixtures("indexes_sample") 151 | def test_index_fetch_info_containing_primary_key(client): 152 | """Tests fetching the index info when a primary key has been set.""" 153 | index = client.index(uid=common.INDEX_UID3) 154 | response = index.fetch_info() 155 | assert isinstance(response, Index) 156 | assert response.uid == common.INDEX_UID3 157 | assert response.primary_key == "book_id" 158 | assert response.primary_key == index.primary_key 159 | assert response.primary_key == index.get_primary_key() 160 | 161 | 162 | @pytest.mark.usefixtures("indexes_sample") 163 | def test_get_primary_key(client): 164 | """Tests getting the primary key of an index.""" 165 | index = client.index(uid=common.INDEX_UID3) 166 | assert index.primary_key is None 167 | response = index.get_primary_key() 168 | assert response == "book_id" 169 | assert index.primary_key == "book_id" 170 | assert index.get_primary_key() == "book_id" 171 | 172 | 173 | def test_update_index(empty_index): 174 | """Tests updating an index.""" 175 | index = empty_index() 176 | response = index.update(primary_key="objectID") 177 | index.wait_for_task(response.task_uid) 178 | response = index.fetch_info() 179 | assert isinstance(response, Index) 180 | assert index.get_primary_key() == "objectID" 181 | assert isinstance(index.created_at, datetime) 182 | assert isinstance(index.updated_at, datetime) 183 | 184 | 185 | @pytest.mark.usefixtures("indexes_sample") 186 | def test_delete_index_by_client(client): 187 | """Tests deleting an index.""" 188 | response = client.index(uid=common.INDEX_UID).delete() 189 | assert response.status == "enqueued" 190 | client.wait_for_task(response.task_uid) 191 | with pytest.raises(Exception): 192 | client.get_index(uid=common.INDEX_UID) 193 | response = client.index(uid=common.INDEX_UID2).delete() 194 | assert response.status == "enqueued" 195 | client.wait_for_task(response.task_uid) 196 | with pytest.raises(Exception): 197 | client.get_index(uid=common.INDEX_UID2) 198 | response = client.index(uid=common.INDEX_UID3).delete() 199 | assert response.status == "enqueued" 200 | client.wait_for_task(response.task_uid) 201 | with pytest.raises(Exception): 202 | client.get_index(uid=common.INDEX_UID3) 203 | assert len(client.get_indexes()["results"]) == 0 204 | 205 | 206 | @pytest.mark.usefixtures("indexes_sample") 207 | def test_delete(client): 208 | assert client.get_index(uid=common.INDEX_UID) 209 | deleted = Client(BASE_URL, MASTER_KEY).index(common.INDEX_UID).delete() 210 | client.wait_for_task(deleted.task_uid) 211 | with pytest.raises(MeilisearchApiError): 212 | client.get_index(uid=common.INDEX_UID) 213 | 214 | 215 | @pytest.mark.usefixtures("indexes_sample") 216 | def test_delete_index(client): 217 | assert client.get_index(uid=common.INDEX_UID) 218 | deleted = Client(BASE_URL, MASTER_KEY).delete_index(uid=common.INDEX_UID) 219 | client.wait_for_task(deleted.task_uid) 220 | with pytest.raises(MeilisearchApiError): 221 | client.get_index(uid=common.INDEX_UID) 222 | -------------------------------------------------------------------------------- /tests/index/test_index_facet_search_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | 4 | def test_basic_facet_search(index_with_documents_and_facets): 5 | """Tests facet search with a simple query.""" 6 | response = index_with_documents_and_facets().facet_search("genre", "cartoon") 7 | assert isinstance(response, dict) 8 | assert response["facetHits"][0]["count"] == 3 9 | assert response["facetQuery"] == "cartoon" 10 | 11 | 12 | def test_facet_search_with_empty_query(index_with_documents_and_facets): 13 | """Tests facet search with a empty query.""" 14 | response = index_with_documents_and_facets().facet_search("genre") 15 | assert isinstance(response, dict) 16 | assert len(response["facetHits"]) == 4 17 | assert response["facetHits"][0]["value"] == "action" 18 | assert response["facetHits"][1]["count"] == 3 19 | assert response["facetQuery"] is None 20 | 21 | 22 | def test_facet_search_with_q(index_with_documents_and_facets): 23 | """Tests facet search with a keyword query q.""" 24 | response = index_with_documents_and_facets().facet_search("genre", "cartoon", {"q": "dragon"}) 25 | assert isinstance(response, dict) 26 | assert response["facetHits"][0]["count"] == 1 27 | assert response["facetQuery"] == "cartoon" 28 | 29 | 30 | def test_facet_search_with_filter(index_with_documents_and_facets): 31 | """Tests facet search with a filter.""" 32 | index = index_with_documents_and_facets() 33 | task = index.update_filterable_attributes(["genre", "release_date"]) 34 | index.wait_for_task(task.task_uid) 35 | response = index.facet_search("genre", "cartoon", {"filter": "release_date > 1149728400"}) 36 | assert isinstance(response, dict) 37 | assert response["facetHits"][0]["count"] == 2 38 | assert response["facetQuery"] == "cartoon" 39 | 40 | 41 | def test_facet_search_with_attributes_to_search_on(index_with_documents_and_facets): 42 | """Tests facet search with optional parameter attributesToSearchOn.""" 43 | response = index_with_documents_and_facets().facet_search( 44 | "genre", "action", {"q": "aquaman", "attributesToSearchOn": ["overview"]} 45 | ) 46 | assert isinstance(response, dict) 47 | assert len(response["facetHits"]) == 0 48 | assert response["facetQuery"] == "action" 49 | -------------------------------------------------------------------------------- /tests/index/test_index_stats_meilisearch.py: -------------------------------------------------------------------------------- 1 | from meilisearch.models.index import IndexStats 2 | 3 | 4 | def test_get_stats(empty_index): 5 | """Tests getting stats of an index.""" 6 | response = empty_index().get_stats() 7 | assert isinstance(response, IndexStats) 8 | assert response.number_of_documents == 0 9 | 10 | 11 | def test_get_stats_default(index_with_documents): 12 | """Tests getting stats of a non-empty index.""" 13 | response = index_with_documents().get_stats() 14 | assert isinstance(response, IndexStats) 15 | assert response.number_of_documents == 31 16 | assert hasattr(response.field_distribution, "genre") 17 | assert response.field_distribution.genre == 11 18 | -------------------------------------------------------------------------------- /tests/index/test_index_task_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | import pytest 4 | 5 | from meilisearch.models.task import Task, TaskResults 6 | from tests import common 7 | 8 | 9 | def test_get_tasks_default(index_with_documents): 10 | """Tests getting the tasks list of an empty index.""" 11 | tasks = index_with_documents().get_tasks() 12 | assert isinstance(tasks, TaskResults) 13 | assert hasattr(tasks, "results") 14 | assert len(tasks.results) != 0 15 | 16 | 17 | def test_get_tasks(empty_index, small_movies): 18 | """Tests getting the tasks list of a populated index.""" 19 | index = empty_index("test_task") 20 | current_tasks = index.get_tasks() 21 | pre_count = len(current_tasks.results) 22 | response = index.add_documents(small_movies) 23 | assert response.task_uid is not None 24 | index.wait_for_task(response.task_uid) 25 | tasks = index.get_tasks() 26 | assert len(tasks.results) == pre_count + 1 27 | 28 | 29 | def test_get_tasks_with_parameters(empty_index): 30 | """Tests getting the tasks list of a populated index.""" 31 | index = empty_index() 32 | tasks = index.get_tasks({"limit": 1}) 33 | assert isinstance(tasks, TaskResults) 34 | assert len(tasks.results) == 1 35 | 36 | 37 | def test_get_tasks_with_index_uid(empty_index): 38 | """Tests getting the tasks list of a populated index.""" 39 | index = empty_index() 40 | tasks = index.get_tasks({"limit": 1, "indexUids": [common.INDEX_UID]}) 41 | assert isinstance(tasks, TaskResults) 42 | assert len(tasks.results) == 1 43 | 44 | 45 | def test_get_tasks_empty_parameters(empty_index): 46 | """Tests getting the global tasks list after populating an index.""" 47 | index = empty_index() 48 | tasks = index.get_tasks({}) 49 | assert isinstance(tasks, TaskResults) 50 | assert isinstance(tasks.results, list) 51 | 52 | 53 | def test_get_task(client): 54 | """Tests getting a task of a operation.""" 55 | task = client.create_index(uid=common.INDEX_UID) 56 | client.wait_for_task(task.task_uid) 57 | index = client.get_index(uid=common.INDEX_UID) 58 | task = index.get_task(task.task_uid) 59 | assert isinstance(task, Task) 60 | assert task.uid is not None 61 | assert task.index_uid is not None 62 | assert task.status is not None 63 | assert task.type is not None 64 | assert task.duration is not None 65 | assert task.enqueued_at is not None 66 | assert task.finished_at is not None 67 | assert task.details is not None 68 | assert task.started_at is not None 69 | 70 | 71 | def test_get_task_inexistent(empty_index): 72 | """Tests getting a task of an inexistent operation.""" 73 | with pytest.raises(Exception): 74 | empty_index().get_task("abc") 75 | -------------------------------------------------------------------------------- /tests/index/test_index_wait_for_task.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | from datetime import datetime 4 | 5 | import pytest 6 | 7 | from meilisearch.errors import MeilisearchTimeoutError 8 | from meilisearch.models.task import Task 9 | 10 | 11 | def test_wait_for_task_default(index_with_documents): 12 | """Tests waiting for an update with default parameters.""" 13 | index = index_with_documents() 14 | response = index.add_documents([{"id": 1, "title": "Le Petit Prince"}]) 15 | assert response.task_uid is not None 16 | update = index.wait_for_task(response.task_uid) 17 | assert isinstance(update, Task) 18 | assert update.status is not None 19 | assert update.status not in ("enqueued", "processing") 20 | 21 | 22 | def test_wait_for_task_timeout(index_with_documents): 23 | """Tests timeout risen by waiting for an update.""" 24 | with pytest.raises(MeilisearchTimeoutError): 25 | index_with_documents().wait_for_task(2, timeout_in_ms=0) 26 | 27 | 28 | def test_wait_for_task_interval_custom(index_with_documents, small_movies): 29 | """Tests call to wait for an update with custom interval.""" 30 | index = index_with_documents() 31 | response = index.add_documents(small_movies) 32 | assert response.task_uid is not None 33 | start_time = datetime.now() 34 | wait_update = index.wait_for_task(response.task_uid, interval_in_ms=1000, timeout_in_ms=6000) 35 | time_delta = datetime.now() - start_time 36 | assert isinstance(wait_update, Task) 37 | assert wait_update.status is not None 38 | assert wait_update.status != "enqueued" 39 | assert wait_update.status != "processing" 40 | assert time_delta.seconds >= 1 41 | 42 | 43 | def test_wait_for_task_interval_zero(index_with_documents, small_movies): 44 | """Tests call to wait for an update with custom interval.""" 45 | index = index_with_documents() 46 | response = index.add_documents(small_movies) 47 | assert response.task_uid is not None 48 | wait_update = index.wait_for_task(response.task_uid, interval_in_ms=0, timeout_in_ms=6000) 49 | assert isinstance(wait_update, Task) 50 | assert wait_update.status is not None 51 | assert wait_update.status != "enqueued" 52 | assert wait_update.status != "processing" 53 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_document.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unnecessary-dunder-call 2 | 3 | 4 | import pytest 5 | 6 | from meilisearch.models.document import Document 7 | 8 | 9 | def test_getattr(): 10 | document = Document({"field1": "test 1", "fiels2": "test 2"}) 11 | assert document.__getattr__("field1") == "field1" 12 | 13 | 14 | def test_getattr_not_found(): 15 | document = Document({"field1": "test 1", "fiels2": "test 2"}) 16 | with pytest.raises(AttributeError): 17 | document.__getattr__("bad") 18 | 19 | 20 | def test_iter(): 21 | # I wrote a test what what this does, but I have a feeling this isn't actually what it was 22 | # expected to do when written as it doesn't really act like I would expect an iterator to act. 23 | document = Document({"field1": "test 1", "fiels2": "test 2"}) 24 | 25 | assert next(document.__iter__()) == ( 26 | "_Document__doc", 27 | {"field1": "test 1", "fiels2": "test 2"}, 28 | ) 29 | -------------------------------------------------------------------------------- /tests/models/test_index.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=unnecessary-dunder-call 2 | 3 | import pytest 4 | 5 | from meilisearch.models.index import IndexStats 6 | 7 | 8 | def test_getattr(): 9 | document = IndexStats({"field1": "test 1", "fiels2": "test 2"}) 10 | assert document.__getattr__("field1") == "field1" 11 | 12 | 13 | def test_getattr_not_found(): 14 | document = IndexStats({"field1": "test 1", "fiels2": "test 2"}) 15 | with pytest.raises(AttributeError): 16 | document.__getattr__("bad") 17 | 18 | 19 | def test_iter(): 20 | # I wrote a test what what this does, but I have a feeling this isn't actually what it was 21 | # expected to do when written as it doesn't really act like I would expect an iterator to act. 22 | document = IndexStats({"field1": "test 1", "fiels2": "test 2"}) 23 | 24 | assert next(document.__iter__()) == ( 25 | "_IndexStats__dict", 26 | {"field1": "test 1", "fiels2": "test 2"}, 27 | ) 28 | -------------------------------------------------------------------------------- /tests/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-python/b3cc7c76b0cf9aa44a73c7fc0d0fef0b8e697af3/tests/settings/__init__.py -------------------------------------------------------------------------------- /tests/settings/test_setting_faceting.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from meilisearch.models.index import Faceting 4 | 5 | DEFAULT_MAX_VALUE_PER_FACET = 100 6 | DEFAULT_SORT_FACET_VALUES_BY = {"*": "alpha"} 7 | NEW_MAX_VALUE_PER_FACET = {"maxValuesPerFacet": 200} 8 | 9 | 10 | def test_get_faceting_settings(empty_index): 11 | response = empty_index().get_faceting_settings() 12 | 13 | assert DEFAULT_MAX_VALUE_PER_FACET == response.max_values_per_facet 14 | assert DEFAULT_SORT_FACET_VALUES_BY == response.sort_facet_values_by 15 | 16 | 17 | def test_update_faceting_settings(empty_index): 18 | index = empty_index() 19 | response = index.update_faceting_settings(NEW_MAX_VALUE_PER_FACET) 20 | index.wait_for_task(response.task_uid) 21 | response = index.get_faceting_settings() 22 | assert NEW_MAX_VALUE_PER_FACET["maxValuesPerFacet"] == response.max_values_per_facet 23 | 24 | 25 | def test_delete_faceting_settings(empty_index): 26 | index = empty_index() 27 | response = index.update_faceting_settings(NEW_MAX_VALUE_PER_FACET) 28 | index.wait_for_task(response.task_uid) 29 | 30 | response = index.reset_faceting_settings() 31 | index.wait_for_task(response.task_uid) 32 | response = index.get_faceting_settings() 33 | assert DEFAULT_MAX_VALUE_PER_FACET == response.max_values_per_facet 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "index_name, facet_order, max_values_per_facet, expected", 38 | [ 39 | ("*", "alpha", 17, {"max_values_per_facet": 17, "sort_facet_values_by": {"*": "alpha"}}), 40 | ("*", "count", 41, {"max_values_per_facet": 41, "sort_facet_values_by": {"*": "count"}}), 41 | ( 42 | "movies", 43 | "alpha", 44 | 42, 45 | {"max_values_per_facet": 42, "sort_facet_values_by": {"*": "alpha", "movies": "alpha"}}, 46 | ), 47 | ( 48 | "movies", 49 | "alpha", 50 | 73, 51 | {"max_values_per_facet": 73, "sort_facet_values_by": {"*": "alpha", "movies": "alpha"}}, 52 | ), 53 | ], 54 | ) 55 | def test_update_faceting_sort_facet_values( 56 | index_name, facet_order, max_values_per_facet, expected, empty_index 57 | ): 58 | faceting = Faceting( 59 | max_values_per_facet=max_values_per_facet, 60 | sort_facet_values_by={index_name: facet_order}, 61 | ) 62 | index = empty_index() 63 | response = index.update_faceting_settings(faceting.model_dump(by_alias=True)) 64 | index.wait_for_task(response.task_uid) 65 | response = index.get_faceting_settings() 66 | assert response.model_dump() == expected 67 | 68 | 69 | def test_reset_faceting(empty_index): 70 | index = empty_index() 71 | response = index.update_faceting_settings( 72 | {"maxValuesPerFacet": 17, "sortFacetValuesBy": {"*": "count"}} 73 | ) 74 | index.wait_for_task(response.task_uid) 75 | response = index.reset_faceting_settings() 76 | index.wait_for_task(response.task_uid) 77 | response = index.get_faceting_settings() 78 | assert response == Faceting( 79 | max_values_per_facet=DEFAULT_MAX_VALUE_PER_FACET, 80 | sort_facet_values_by=DEFAULT_SORT_FACET_VALUES_BY, 81 | ) 82 | -------------------------------------------------------------------------------- /tests/settings/test_settings.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | import pytest 3 | 4 | from meilisearch.models.embedders import OpenAiEmbedder, UserProvidedEmbedder 5 | 6 | 7 | @pytest.fixture 8 | def new_settings(new_embedders): 9 | return { 10 | "rankingRules": ["typo", "words"], 11 | "searchableAttributes": ["title", "overview"], 12 | "embedders": new_embedders, 13 | } 14 | 15 | 16 | DEFAULT_RANKING_RULES = ["words", "typo", "proximity", "attribute", "sort", "exactness"] 17 | 18 | DEFAULT_TYPO_TOLERANCE = { 19 | "enabled": True, 20 | "minWordSizeForTypos": { 21 | "oneTypo": 5, 22 | "twoTypos": 9, 23 | }, 24 | "disableOnWords": [], 25 | "disableOnAttributes": [], 26 | } 27 | 28 | 29 | def test_get_settings_default(empty_index): 30 | """Tests getting all settings by default.""" 31 | response = empty_index().get_settings() 32 | for rule in DEFAULT_RANKING_RULES: 33 | assert rule in response["rankingRules"] 34 | for typo in DEFAULT_TYPO_TOLERANCE: # pylint: disable=consider-using-dict-items 35 | assert typo in response["typoTolerance"] 36 | assert DEFAULT_TYPO_TOLERANCE[typo] == response["typoTolerance"][typo] 37 | assert response["distinctAttribute"] is None 38 | assert response["searchableAttributes"] == ["*"] 39 | assert response["displayedAttributes"] == ["*"] 40 | assert response["stopWords"] == [] 41 | assert response["synonyms"] == {} 42 | 43 | 44 | def test_update_settings(new_settings, empty_index): 45 | """Tests updating some settings.""" 46 | index = empty_index() 47 | response = index.update_settings(new_settings) 48 | update = index.wait_for_task(response.task_uid) 49 | assert update.status == "succeeded" 50 | response = index.get_settings() 51 | for rule in new_settings["rankingRules"]: 52 | assert rule in response["rankingRules"] 53 | assert response["distinctAttribute"] is None 54 | for attribute in new_settings["searchableAttributes"]: 55 | assert attribute in response["searchableAttributes"] 56 | assert response["displayedAttributes"] == ["*"] 57 | assert response["stopWords"] == [] 58 | assert response["synonyms"] == {} 59 | assert isinstance(response["embedders"]["default"], UserProvidedEmbedder) 60 | assert isinstance(response["embedders"]["open_ai"], OpenAiEmbedder) 61 | 62 | 63 | def test_reset_settings(new_settings, empty_index): 64 | """Tests resetting all the settings to their default value.""" 65 | index = empty_index() 66 | # Update settings first 67 | response = index.update_settings(new_settings) 68 | update = index.wait_for_task(response.task_uid) 69 | assert update.status == "succeeded" 70 | # Check the settings have been correctly updated 71 | response = index.get_settings() 72 | for rule in new_settings["rankingRules"]: 73 | assert rule in response["rankingRules"] 74 | assert response["distinctAttribute"] is None 75 | for attribute in new_settings["searchableAttributes"]: 76 | assert attribute in response["searchableAttributes"] 77 | assert response["displayedAttributes"] == ["*"] 78 | assert response["stopWords"] == [] 79 | assert response["synonyms"] == {} 80 | # Check the reset of the settings 81 | response = index.reset_settings() 82 | update = index.wait_for_task(response.task_uid) 83 | assert update.status == "succeeded" 84 | response = index.get_settings() 85 | for rule in DEFAULT_RANKING_RULES: 86 | assert rule in response["rankingRules"] 87 | for typo in DEFAULT_TYPO_TOLERANCE: # pylint: disable=consider-using-dict-items 88 | assert typo in response["typoTolerance"] 89 | assert DEFAULT_TYPO_TOLERANCE[typo] == response["typoTolerance"][typo] 90 | assert response["distinctAttribute"] is None 91 | assert response["displayedAttributes"] == ["*"] 92 | assert response["searchableAttributes"] == ["*"] 93 | assert response["stopWords"] == [] 94 | assert response["synonyms"] == {} 95 | assert response["embedders"] == {} 96 | -------------------------------------------------------------------------------- /tests/settings/test_settings_dictionary_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_DICTIONARY = ["J. R. R. Tolkien", "W. E. B. Du Bois"] 2 | 3 | 4 | def test_get_dictionary_default(empty_index): 5 | """Tests getting the default value of user dictionary.""" 6 | dictionary = empty_index().get_dictionary() 7 | assert dictionary == [] 8 | 9 | 10 | def test_update_dictionary(empty_index): 11 | """Tests updating the user dictionary.""" 12 | index = empty_index() 13 | task = index.update_dictionary(NEW_DICTIONARY) 14 | task = index.wait_for_task(task.task_uid) 15 | assert task.status == "succeeded" 16 | 17 | dictionary = index.get_dictionary() 18 | for word in NEW_DICTIONARY: 19 | assert word in dictionary 20 | 21 | 22 | def test_reset_dictionary(empty_index): 23 | """Tests resetting the user dictionary to its default empty list.""" 24 | index = empty_index() 25 | task = index.update_dictionary(NEW_DICTIONARY) 26 | task = index.wait_for_task(task.task_uid) 27 | assert task.status == "succeeded" 28 | 29 | dictionary = index.get_dictionary() 30 | for word in NEW_DICTIONARY: 31 | assert word in dictionary 32 | 33 | reset_task = index.reset_dictionary() 34 | reset_task = index.wait_for_task(reset_task.task_uid) 35 | assert reset_task.status == "succeeded" 36 | 37 | dictionary = index.get_dictionary() 38 | assert dictionary == [] 39 | -------------------------------------------------------------------------------- /tests/settings/test_settings_displayed_attributes_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | DISPLAYED_ATTRIBUTES = ["id", "release_date", "title", "poster", "overview", "genre"] 4 | 5 | 6 | def test_get_displayed_attributes(empty_index, small_movies): 7 | """Tests getting the displayed attributes before and after indexing a dataset.""" 8 | index = empty_index() 9 | response = index.get_displayed_attributes() 10 | assert response == ["*"] 11 | response = index.add_documents(small_movies) 12 | index.wait_for_task(response.task_uid) 13 | get_attributes = index.get_displayed_attributes() 14 | assert get_attributes == ["*"] 15 | 16 | 17 | def test_update_displayed_attributes(empty_index): 18 | """Tests updating the displayed attributes.""" 19 | index = empty_index() 20 | response = index.update_displayed_attributes(DISPLAYED_ATTRIBUTES) 21 | index.wait_for_task(response.task_uid) 22 | get_attributes_new = index.get_displayed_attributes() 23 | assert len(get_attributes_new) == len(DISPLAYED_ATTRIBUTES) 24 | for attribute in DISPLAYED_ATTRIBUTES: 25 | assert attribute in get_attributes_new 26 | 27 | 28 | def test_update_displayed_attributes_to_none(empty_index): 29 | """Tests updating the displayed attributes at null.""" 30 | index = empty_index() 31 | # Update the settings first 32 | response = index.update_displayed_attributes(DISPLAYED_ATTRIBUTES) 33 | update = index.wait_for_task(response.task_uid) 34 | assert update.status == "succeeded" 35 | # Check the settings have been correctly updated 36 | get_attributes = index.get_displayed_attributes() 37 | for attribute in DISPLAYED_ATTRIBUTES: 38 | assert attribute in get_attributes 39 | # Launch test to update at null the setting 40 | response = index.update_displayed_attributes(None) 41 | index.wait_for_task(response.task_uid) 42 | response = index.get_displayed_attributes() 43 | assert response == ["*"] 44 | 45 | 46 | def test_reset_displayed_attributes(empty_index): 47 | """Tests resetting the displayed attributes setting to its default value.""" 48 | index = empty_index() 49 | # Update the settings first 50 | response = index.update_displayed_attributes(DISPLAYED_ATTRIBUTES) 51 | update = index.wait_for_task(response.task_uid) 52 | assert update.status == "succeeded" 53 | # Check the settings have been correctly updated 54 | get_attributes_new = index.get_displayed_attributes() 55 | assert len(get_attributes_new) == len(DISPLAYED_ATTRIBUTES) 56 | for attribute in DISPLAYED_ATTRIBUTES: 57 | assert attribute in get_attributes_new 58 | # Check the reset of the settings 59 | response = index.reset_displayed_attributes() 60 | index.wait_for_task(response.task_uid) 61 | get_attributes = index.get_displayed_attributes() 62 | assert get_attributes == ["*"] 63 | -------------------------------------------------------------------------------- /tests/settings/test_settings_distinct_attribute_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_DISTINCT_ATTRIBUTE = "title" 2 | DEFAULT_DISTINCT_ATTRIBUTE = None 3 | 4 | 5 | def test_get_distinct_attribute(empty_index): 6 | """Tests geting the distinct attribute.""" 7 | response = empty_index().get_distinct_attribute() 8 | assert response == DEFAULT_DISTINCT_ATTRIBUTE 9 | 10 | 11 | def test_update_distinct_attribute(empty_index): 12 | """Tests updating a custom distinct attribute.""" 13 | index = empty_index() 14 | response = index.update_distinct_attribute(NEW_DISTINCT_ATTRIBUTE) 15 | index.wait_for_task(response.task_uid) 16 | response = index.get_distinct_attribute() 17 | assert response == NEW_DISTINCT_ATTRIBUTE 18 | 19 | 20 | def test_update_distinct_at_to_none(empty_index): 21 | """Tests updating distinct attribute at null.""" 22 | index = empty_index() 23 | # Update the settings first 24 | response = index.update_distinct_attribute(NEW_DISTINCT_ATTRIBUTE) 25 | update = index.wait_for_task(response.task_uid) 26 | assert update.status == "succeeded" 27 | # Check the settings have been correctly updated 28 | response = index.get_distinct_attribute() 29 | assert response == NEW_DISTINCT_ATTRIBUTE 30 | # Launch test to update at null the setting 31 | response = index.update_distinct_attribute(None) 32 | index.wait_for_task(response.task_uid) 33 | response = index.get_distinct_attribute() 34 | assert response == DEFAULT_DISTINCT_ATTRIBUTE 35 | 36 | 37 | def test_reset_distinct_attribute(empty_index): 38 | """Tests resetting the distinct attribute setting to its default value.""" 39 | index = empty_index() 40 | # Update the settings first 41 | response = index.update_distinct_attribute(NEW_DISTINCT_ATTRIBUTE) 42 | update = index.wait_for_task(response.task_uid) 43 | assert update.status == "succeeded" 44 | # Check the settings have been correctly updated 45 | response = index.get_distinct_attribute() 46 | assert response == NEW_DISTINCT_ATTRIBUTE 47 | # Check the reset of the settings 48 | response = index.reset_distinct_attribute() 49 | index.wait_for_task(response.task_uid) 50 | response = index.get_distinct_attribute() 51 | assert response == DEFAULT_DISTINCT_ATTRIBUTE 52 | -------------------------------------------------------------------------------- /tests/settings/test_settings_embedders.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=redefined-outer-name 2 | 3 | import pytest 4 | 5 | from meilisearch.models.embedders import ( 6 | CompositeEmbedder, 7 | HuggingFaceEmbedder, 8 | OpenAiEmbedder, 9 | PoolingType, 10 | UserProvidedEmbedder, 11 | ) 12 | 13 | 14 | def test_get_default_embedders(empty_index): 15 | """Tests getting default embedders.""" 16 | response = empty_index().get_embedders() 17 | 18 | assert response is None 19 | 20 | 21 | def test_update_embedders_with_user_provided_source(new_embedders, empty_index): 22 | """Tests updating embedders.""" 23 | index = empty_index() 24 | response_update = index.update_embedders(new_embedders) 25 | update = index.wait_for_task(response_update.task_uid) 26 | response_get = index.get_embedders() 27 | assert update.status == "succeeded" 28 | assert isinstance(response_get.embedders["default"], UserProvidedEmbedder) 29 | assert isinstance(response_get.embedders["open_ai"], OpenAiEmbedder) 30 | 31 | 32 | def test_reset_embedders(new_embedders, empty_index): 33 | """Tests resetting the typo_tolerance setting to its default value.""" 34 | index = empty_index() 35 | 36 | # Update the settings 37 | response_update = index.update_embedders(new_embedders) 38 | update1 = index.wait_for_task(response_update.task_uid) 39 | assert update1.status == "succeeded" 40 | # Get the setting after update 41 | response_get = index.get_embedders() 42 | assert isinstance(response_get.embedders["default"], UserProvidedEmbedder) 43 | assert isinstance(response_get.embedders["open_ai"], OpenAiEmbedder) 44 | # Reset the setting 45 | response_reset = index.reset_embedders() 46 | update2 = index.wait_for_task(response_reset.task_uid) 47 | # Get the setting after reset 48 | assert update2.status == "succeeded" 49 | assert isinstance(response_get.embedders["default"], UserProvidedEmbedder) 50 | assert isinstance(response_get.embedders["open_ai"], OpenAiEmbedder) 51 | response_last = index.get_embedders() 52 | assert response_last is None 53 | 54 | 55 | def test_openai_embedder_format(empty_index): 56 | """Tests that OpenAi embedder has the required fields and proper format.""" 57 | index = empty_index() 58 | 59 | openai_embedder = { 60 | "openai": { 61 | "source": "openAi", 62 | "apiKey": "test-key", 63 | "model": "text-embedding-3-small", 64 | "dimensions": 1536, 65 | "documentTemplateMaxBytes": 400, 66 | "distribution": {"mean": 0.5, "sigma": 0.1}, 67 | "binaryQuantized": False, 68 | } 69 | } 70 | response = index.update_embedders(openai_embedder) 71 | index.wait_for_task(response.task_uid) 72 | embedders = index.get_embedders() 73 | assert embedders.embedders["openai"].source == "openAi" 74 | assert embedders.embedders["openai"].model == "text-embedding-3-small" 75 | assert embedders.embedders["openai"].dimensions == 1536 76 | assert hasattr(embedders.embedders["openai"], "document_template") 77 | assert embedders.embedders["openai"].document_template_max_bytes == 400 78 | assert embedders.embedders["openai"].distribution.mean == 0.5 79 | assert embedders.embedders["openai"].distribution.sigma == 0.1 80 | assert embedders.embedders["openai"].binary_quantized is False 81 | 82 | 83 | def test_huggingface_embedder_format(empty_index): 84 | """Tests that HuggingFace embedder has the required fields and proper format.""" 85 | index = empty_index() 86 | 87 | huggingface_embedder = { 88 | "huggingface": { 89 | "source": "huggingFace", 90 | "model": "BAAI/bge-base-en-v1.5", 91 | "revision": "main", 92 | "documentTemplateMaxBytes": 400, 93 | "distribution": {"mean": 0.5, "sigma": 0.1}, 94 | "binaryQuantized": False, 95 | } 96 | } 97 | response = index.update_embedders(huggingface_embedder) 98 | index.wait_for_task(response.task_uid) 99 | embedders = index.get_embedders() 100 | assert embedders.embedders["huggingface"].source == "huggingFace" 101 | assert embedders.embedders["huggingface"].model == "BAAI/bge-base-en-v1.5" 102 | assert embedders.embedders["huggingface"].revision == "main" 103 | assert hasattr(embedders.embedders["huggingface"], "document_template") 104 | assert embedders.embedders["huggingface"].document_template_max_bytes == 400 105 | assert embedders.embedders["huggingface"].distribution.mean == 0.5 106 | assert embedders.embedders["huggingface"].distribution.sigma == 0.1 107 | assert embedders.embedders["huggingface"].binary_quantized is False 108 | assert embedders.embedders["huggingface"].pooling is PoolingType.USE_MODEL 109 | 110 | 111 | def test_ollama_embedder_format(empty_index): 112 | """Tests that Ollama embedder has the required fields and proper format.""" 113 | index = empty_index() 114 | 115 | ollama_embedder = { 116 | "ollama": { 117 | "source": "ollama", 118 | "url": "http://localhost:11434/api/embeddings", 119 | "apiKey": "test-key", 120 | "model": "llama2", 121 | "dimensions": 4096, 122 | "documentTemplateMaxBytes": 400, 123 | "distribution": {"mean": 0.5, "sigma": 0.1}, 124 | "binaryQuantized": False, 125 | } 126 | } 127 | response = index.update_embedders(ollama_embedder) 128 | index.wait_for_task(response.task_uid) 129 | embedders = index.get_embedders() 130 | assert embedders.embedders["ollama"].source == "ollama" 131 | assert embedders.embedders["ollama"].url == "http://localhost:11434/api/embeddings" 132 | assert embedders.embedders["ollama"].model == "llama2" 133 | assert embedders.embedders["ollama"].dimensions == 4096 134 | assert hasattr(embedders.embedders["ollama"], "document_template") 135 | assert embedders.embedders["ollama"].document_template_max_bytes == 400 136 | assert embedders.embedders["ollama"].distribution.mean == 0.5 137 | assert embedders.embedders["ollama"].distribution.sigma == 0.1 138 | assert embedders.embedders["ollama"].binary_quantized is False 139 | 140 | 141 | def test_rest_embedder_format(empty_index): 142 | """Tests that Rest embedder has the required fields and proper format.""" 143 | index = empty_index() 144 | 145 | rest_embedder = { 146 | "rest": { 147 | "source": "rest", 148 | "url": "http://localhost:8000/embed", 149 | "apiKey": "test-key", 150 | "dimensions": 512, 151 | "documentTemplateMaxBytes": 400, 152 | "request": {"model": "MODEL_NAME", "input": "{{text}}"}, 153 | "response": {"result": {"data": ["{{embedding}}"]}}, 154 | "headers": {"Authorization": "Bearer test-key"}, 155 | "distribution": {"mean": 0.5, "sigma": 0.1}, 156 | "binaryQuantized": False, 157 | } 158 | } 159 | response = index.update_embedders(rest_embedder) 160 | index.wait_for_task(response.task_uid) 161 | embedders = index.get_embedders() 162 | assert embedders.embedders["rest"].source == "rest" 163 | assert embedders.embedders["rest"].url == "http://localhost:8000/embed" 164 | assert embedders.embedders["rest"].dimensions == 512 165 | assert hasattr(embedders.embedders["rest"], "document_template") 166 | assert embedders.embedders["rest"].document_template_max_bytes == 400 167 | assert embedders.embedders["rest"].request == {"model": "MODEL_NAME", "input": "{{text}}"} 168 | assert embedders.embedders["rest"].response == {"result": {"data": ["{{embedding}}"]}} 169 | assert embedders.embedders["rest"].headers == {"Authorization": "Bearer test-key"} 170 | assert embedders.embedders["rest"].distribution.mean == 0.5 171 | assert embedders.embedders["rest"].distribution.sigma == 0.1 172 | assert embedders.embedders["rest"].binary_quantized is False 173 | 174 | 175 | def test_user_provided_embedder_format(empty_index): 176 | """Tests that UserProvided embedder has the required fields and proper format.""" 177 | index = empty_index() 178 | 179 | user_provided_embedder = { 180 | "user_provided": { 181 | "source": "userProvided", 182 | "dimensions": 512, 183 | "distribution": {"mean": 0.5, "sigma": 0.1}, 184 | "binaryQuantized": False, 185 | } 186 | } 187 | response = index.update_embedders(user_provided_embedder) 188 | index.wait_for_task(response.task_uid) 189 | embedders = index.get_embedders() 190 | assert embedders.embedders["user_provided"].source == "userProvided" 191 | assert embedders.embedders["user_provided"].dimensions == 512 192 | assert embedders.embedders["user_provided"].distribution.mean == 0.5 193 | assert embedders.embedders["user_provided"].distribution.sigma == 0.1 194 | assert embedders.embedders["user_provided"].binary_quantized is False 195 | 196 | 197 | @pytest.mark.usefixtures("enable_composite_embedders") 198 | def test_composite_embedder_format(empty_index): 199 | """Tests that CompositeEmbedder embedder has the required fields and proper format.""" 200 | index = empty_index() 201 | 202 | embedder = HuggingFaceEmbedder().model_dump(by_alias=True, exclude_none=True) 203 | 204 | # create composite embedder 205 | composite_embedder = { 206 | "composite": { 207 | "source": "composite", 208 | "searchEmbedder": embedder, 209 | "indexingEmbedder": embedder, 210 | } 211 | } 212 | 213 | response = index.update_embedders(composite_embedder) 214 | update = index.wait_for_task(response.task_uid) 215 | embedders = index.get_embedders() 216 | assert update.status == "succeeded" 217 | 218 | assert embedders.embedders["composite"].source == "composite" 219 | 220 | # ensure serialization roundtrips nicely 221 | assert isinstance(embedders.embedders["composite"], CompositeEmbedder) 222 | assert isinstance(embedders.embedders["composite"].search_embedder, HuggingFaceEmbedder) 223 | assert isinstance(embedders.embedders["composite"].indexing_embedder, HuggingFaceEmbedder) 224 | 225 | # ensure search_embedder has no document_template 226 | assert getattr(embedders.embedders["composite"].search_embedder, "document_template") is None 227 | assert ( 228 | getattr( 229 | embedders.embedders["composite"].search_embedder, 230 | "document_template_max_bytes", 231 | ) 232 | is None 233 | ) 234 | assert getattr(embedders.embedders["composite"].indexing_embedder, "document_template") 235 | -------------------------------------------------------------------------------- /tests/settings/test_settings_filterable_attributes_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | FILTERABLE_ATTRIBUTES = ["title", "release_date"] 4 | 5 | 6 | def test_get_filterable_attributes(empty_index): 7 | """Tests getting the filterable attributes.""" 8 | response = empty_index().get_filterable_attributes() 9 | assert response == [] 10 | 11 | 12 | def test_update_filterable_attributes(empty_index): 13 | """Tests updating the filterable attributes.""" 14 | index = empty_index() 15 | response = index.update_filterable_attributes(FILTERABLE_ATTRIBUTES) 16 | index.wait_for_task(response.task_uid) 17 | get_attributes = index.get_filterable_attributes() 18 | assert len(get_attributes) == len(FILTERABLE_ATTRIBUTES) 19 | for attribute in FILTERABLE_ATTRIBUTES: 20 | assert attribute in get_attributes 21 | 22 | 23 | def test_update_filterable_attributes_to_none(empty_index): 24 | """Tests updating the filterable attributes at null.""" 25 | index = empty_index() 26 | # Update the settings first 27 | response = index.update_filterable_attributes(FILTERABLE_ATTRIBUTES) 28 | update = index.wait_for_task(response.task_uid) 29 | assert update.status == "succeeded" 30 | # Check the settings have been correctly updated 31 | get_attributes = index.get_filterable_attributes() 32 | for attribute in FILTERABLE_ATTRIBUTES: 33 | assert attribute in get_attributes 34 | # Launch test to update at null the setting 35 | response = index.update_filterable_attributes(None) 36 | index.wait_for_task(response.task_uid) 37 | response = index.get_filterable_attributes() 38 | assert response == [] 39 | 40 | 41 | def test_reset_filterable_attributes(empty_index): 42 | """Tests resetting the filterable attributes setting to its default value""" 43 | index = empty_index() 44 | # Update the settings first 45 | response = index.update_filterable_attributes(FILTERABLE_ATTRIBUTES) 46 | update = index.wait_for_task(response.task_uid) 47 | assert update.status == "succeeded" 48 | # Check the settings have been correctly updated 49 | get_attributes = index.get_filterable_attributes() 50 | assert len(get_attributes) == len(FILTERABLE_ATTRIBUTES) 51 | for attribute in FILTERABLE_ATTRIBUTES: 52 | assert attribute in get_attributes 53 | # Check the reset of the settings 54 | response = index.reset_filterable_attributes() 55 | index.wait_for_task(response.task_uid) 56 | response = index.get_filterable_attributes() 57 | assert response == [] 58 | -------------------------------------------------------------------------------- /tests/settings/test_settings_localized_attributes_meilisearch.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from meilisearch.models.index import LocalizedAttributes 4 | 5 | NEW_LOCALIZED_ATTRIBUTES = [{"attributePatterns": ["title"], "locales": ["eng"]}] 6 | 7 | 8 | def unpack_loc_attrs_response(response: List[LocalizedAttributes]): 9 | return [loc_attrs.model_dump(by_alias=True) for loc_attrs in response] 10 | 11 | 12 | def test_get_localized_attributes(empty_index): 13 | """Tests getting default localized_attributes.""" 14 | response = empty_index().get_localized_attributes() 15 | assert response is None 16 | 17 | 18 | def test_update_localized_attributes(empty_index): 19 | """Tests updating proximity precision.""" 20 | index = empty_index() 21 | response = index.update_localized_attributes(NEW_LOCALIZED_ATTRIBUTES) 22 | update = index.wait_for_task(response.task_uid) 23 | assert update.status == "succeeded" 24 | response = index.get_localized_attributes() 25 | assert NEW_LOCALIZED_ATTRIBUTES == unpack_loc_attrs_response(response) 26 | 27 | 28 | def test_reset_localized_attributes(empty_index): 29 | """Tests resetting the proximity precision to its default value.""" 30 | index = empty_index() 31 | # Update the settings first 32 | response = index.update_localized_attributes(NEW_LOCALIZED_ATTRIBUTES) 33 | update = index.wait_for_task(response.task_uid) 34 | assert update.status == "succeeded" 35 | # Check the settings have been correctly updated 36 | response = index.get_localized_attributes() 37 | assert NEW_LOCALIZED_ATTRIBUTES == unpack_loc_attrs_response(response) 38 | # Check the reset of the settings 39 | response = index.reset_localized_attributes() 40 | update = index.wait_for_task(response.task_uid) 41 | assert update.status == "succeeded" 42 | response = index.get_localized_attributes() 43 | assert response is None 44 | -------------------------------------------------------------------------------- /tests/settings/test_settings_pagination.py: -------------------------------------------------------------------------------- 1 | DEFAULT_MAX_TOTAL_HITS = 1000 2 | NEW_MAX_TOTAL_HITS = {"maxTotalHits": 2222} 3 | 4 | 5 | def test_get_pagination_settings(empty_index): 6 | response = empty_index().get_pagination_settings() 7 | 8 | assert DEFAULT_MAX_TOTAL_HITS == response.max_total_hits 9 | 10 | 11 | def test_update_pagination_settings(empty_index): 12 | index = empty_index() 13 | response = index.update_pagination_settings(NEW_MAX_TOTAL_HITS) 14 | index.wait_for_task(response.task_uid) 15 | response = index.get_pagination_settings() 16 | assert NEW_MAX_TOTAL_HITS["maxTotalHits"] == response.max_total_hits 17 | 18 | 19 | def test_delete_pagination_settings(empty_index): 20 | index = empty_index() 21 | response = index.reset_pagination_settings() 22 | 23 | index.wait_for_task(response.task_uid) 24 | response = index.get_pagination_settings() 25 | assert DEFAULT_MAX_TOTAL_HITS == response.max_total_hits 26 | -------------------------------------------------------------------------------- /tests/settings/test_settings_proximity_precision_meilisearch.py: -------------------------------------------------------------------------------- 1 | from meilisearch.models.index import ProximityPrecision 2 | 3 | NEW_PROXIMITY_PRECISION = ProximityPrecision.BY_ATTRIBUTE 4 | 5 | 6 | def test_get_proximity_precision(empty_index): 7 | """Tests getting default proximity precision.""" 8 | response = empty_index().get_proximity_precision() 9 | assert response == ProximityPrecision.BY_WORD 10 | 11 | 12 | def test_update_proximity_precision(empty_index): 13 | """Tests updating proximity precision.""" 14 | index = empty_index() 15 | response = index.update_proximity_precision(NEW_PROXIMITY_PRECISION) 16 | update = index.wait_for_task(response.task_uid) 17 | assert update.status == "succeeded" 18 | response = index.get_proximity_precision() 19 | assert NEW_PROXIMITY_PRECISION == response 20 | 21 | 22 | def test_reset_proximity_precision(empty_index): 23 | """Tests resetting the proximity precision to its default value.""" 24 | index = empty_index() 25 | # Update the settings first 26 | response = index.update_proximity_precision(NEW_PROXIMITY_PRECISION) 27 | update = index.wait_for_task(response.task_uid) 28 | assert update.status == "succeeded" 29 | # Check the settings have been correctly updated 30 | response = index.get_proximity_precision() 31 | assert NEW_PROXIMITY_PRECISION == response 32 | # Check the reset of the settings 33 | response = index.reset_proximity_precision() 34 | update = index.wait_for_task(response.task_uid) 35 | assert update.status == "succeeded" 36 | response = index.get_proximity_precision() 37 | assert response == ProximityPrecision.BY_WORD 38 | -------------------------------------------------------------------------------- /tests/settings/test_settings_ranking_rules_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_RANKING_RULES = ["typo", "exactness"] 2 | DEFAULT_RANKING_RULES = ["words", "typo", "proximity", "attribute", "sort", "exactness"] 3 | 4 | 5 | def test_get_ranking_rules_default(empty_index): 6 | """Tests getting the default ranking rules.""" 7 | response = empty_index().get_ranking_rules() 8 | for rule in DEFAULT_RANKING_RULES: 9 | assert rule in response 10 | 11 | 12 | def test_update_ranking_rules(empty_index): 13 | """Tests changing the ranking rules.""" 14 | index = empty_index() 15 | response = index.update_ranking_rules(NEW_RANKING_RULES) 16 | index.wait_for_task(response.task_uid) 17 | response = index.get_ranking_rules() 18 | for rule in NEW_RANKING_RULES: 19 | assert rule in response 20 | 21 | 22 | def test_update_ranking_rules_none(empty_index): 23 | """Tests updating the ranking rules at null.""" 24 | index = empty_index() 25 | # Update the settings first 26 | response = index.update_ranking_rules(NEW_RANKING_RULES) 27 | update = index.wait_for_task(response.task_uid) 28 | assert update.status == "succeeded" 29 | # Check the settings have been correctly updated 30 | response = index.get_ranking_rules() 31 | for rule in NEW_RANKING_RULES: 32 | assert rule in response 33 | # Launch test to update at null the setting 34 | response = index.update_ranking_rules(None) 35 | index.wait_for_task(response.task_uid) 36 | response = index.get_ranking_rules() 37 | for rule in DEFAULT_RANKING_RULES: 38 | assert rule in response 39 | 40 | 41 | def test_reset_ranking_rules(empty_index): 42 | """Tests resetting the ranking rules setting to its default value.""" 43 | index = empty_index() 44 | # Update the settings first 45 | response = index.update_ranking_rules(NEW_RANKING_RULES) 46 | update = index.wait_for_task(response.task_uid) 47 | assert update.status == "succeeded" 48 | # Check the settings have been correctly updated 49 | response = index.get_ranking_rules() 50 | for rule in NEW_RANKING_RULES: 51 | assert rule in response 52 | # Check the reset of the settings 53 | response = index.reset_ranking_rules() 54 | index.wait_for_task(response.task_uid) 55 | response = index.get_ranking_rules() 56 | for rule in DEFAULT_RANKING_RULES: 57 | assert rule in response 58 | -------------------------------------------------------------------------------- /tests/settings/test_settings_search_cutoff_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_SEARCH_CUTOFF_MS = 150 2 | 3 | 4 | def test_get_search_cutoff_ms(empty_index): 5 | """Tests getting default search cutoff in ms.""" 6 | response = empty_index().get_search_cutoff_ms() 7 | assert response is None 8 | 9 | 10 | def test_update_search_cutoff_ms(empty_index): 11 | """Tests updating search cutoff in ms.""" 12 | index = empty_index() 13 | response = index.update_search_cutoff_ms(NEW_SEARCH_CUTOFF_MS) 14 | update = index.wait_for_task(response.task_uid) 15 | assert update.status == "succeeded" 16 | response = index.get_search_cutoff_ms() 17 | assert NEW_SEARCH_CUTOFF_MS == response 18 | 19 | 20 | def test_reset_search_cutoff_ms(empty_index): 21 | """Tests resetting the search cutoff to its default value.""" 22 | index = empty_index() 23 | # Update the settings first 24 | response = index.update_search_cutoff_ms(NEW_SEARCH_CUTOFF_MS) 25 | update = index.wait_for_task(response.task_uid) 26 | assert update.status == "succeeded" 27 | # Check the settings have been correctly updated 28 | response = index.get_search_cutoff_ms() 29 | assert NEW_SEARCH_CUTOFF_MS == response 30 | # Check the reset of the settings 31 | response = index.reset_search_cutoff_ms() 32 | update = index.wait_for_task(response.task_uid) 33 | assert update.status == "succeeded" 34 | response = index.get_search_cutoff_ms() 35 | assert response is None 36 | -------------------------------------------------------------------------------- /tests/settings/test_settings_searchable_attributes_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | NEW_SEARCHABLE_ATTRIBUTES = ["something", "random"] 4 | 5 | 6 | def test_get_searchable_attributes(empty_index, small_movies): 7 | """Tests getting the searchable attributes on an empty and populated index.""" 8 | index = empty_index() 9 | response = index.get_searchable_attributes() 10 | assert response == ["*"] 11 | response = index.add_documents(small_movies, primary_key="id") 12 | index.wait_for_task(response.task_uid) 13 | get_attributes = index.get_searchable_attributes() 14 | assert get_attributes == ["*"] 15 | 16 | 17 | def test_update_searchable_attributes(empty_index): 18 | """Tests updating the searchable attributes.""" 19 | index = empty_index() 20 | response = index.update_searchable_attributes(NEW_SEARCHABLE_ATTRIBUTES) 21 | index.wait_for_task(response.task_uid) 22 | response = index.get_searchable_attributes() 23 | assert len(response) == len(NEW_SEARCHABLE_ATTRIBUTES) 24 | for attribute in NEW_SEARCHABLE_ATTRIBUTES: 25 | assert attribute in response 26 | 27 | 28 | def test_update_searchable_attributes_to_none(empty_index): 29 | """Tests updating the searchable attributes at null.""" 30 | index = empty_index() 31 | # Update the settings first 32 | response = index.update_searchable_attributes(NEW_SEARCHABLE_ATTRIBUTES) 33 | update = index.wait_for_task(response.task_uid) 34 | assert update.status == "succeeded" 35 | # Check the settings have been correctly updated 36 | response = index.get_searchable_attributes() 37 | for attribute in NEW_SEARCHABLE_ATTRIBUTES: 38 | assert attribute in response 39 | # Launch test to update at null the setting 40 | response = index.update_searchable_attributes(None) 41 | index.wait_for_task(response.task_uid) 42 | response = index.get_searchable_attributes() 43 | assert response == ["*"] 44 | 45 | 46 | def test_reset_searchable_attributes(empty_index): 47 | """Tests resetting the searchable attributes setting to its default value.""" 48 | index = empty_index() 49 | # Update the settings first 50 | response = index.update_searchable_attributes(NEW_SEARCHABLE_ATTRIBUTES) 51 | update = index.wait_for_task(response.task_uid) 52 | assert update.status == "succeeded" 53 | # Check the settings have been correctly updated 54 | response = index.get_searchable_attributes() 55 | assert len(response) == len(NEW_SEARCHABLE_ATTRIBUTES) 56 | for attribute in NEW_SEARCHABLE_ATTRIBUTES: 57 | assert attribute in response 58 | # Check the reset of the settings 59 | response = index.reset_searchable_attributes() 60 | index.wait_for_task(response.task_uid) 61 | response = index.get_searchable_attributes() 62 | assert response == ["*"] 63 | -------------------------------------------------------------------------------- /tests/settings/test_settings_sortable_attributes_meilisearch.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | 3 | SORTABLE_ATTRIBUTES = ["title", "release_date"] 4 | 5 | 6 | def test_get_sortable_attributes(empty_index): 7 | """Tests getting the sortable attributes.""" 8 | response = empty_index().get_sortable_attributes() 9 | assert response == [] 10 | 11 | 12 | def test_update_sortable_attributes(empty_index): 13 | """Tests updating the sortable attributes.""" 14 | index = empty_index() 15 | response = index.update_sortable_attributes(SORTABLE_ATTRIBUTES) 16 | index.wait_for_task(response.task_uid) 17 | get_attributes = index.get_sortable_attributes() 18 | assert len(get_attributes) == len(SORTABLE_ATTRIBUTES) 19 | for attribute in SORTABLE_ATTRIBUTES: 20 | assert attribute in get_attributes 21 | 22 | 23 | def test_update_sortable_attributes_to_none(empty_index): 24 | """Tests updating the sortable attributes at null.""" 25 | index = empty_index() 26 | # Update the settings first 27 | response = index.update_sortable_attributes(SORTABLE_ATTRIBUTES) 28 | update = index.wait_for_task(response.task_uid) 29 | assert update.status == "succeeded" 30 | # Check the settings have been correctly updated 31 | get_attributes = index.get_sortable_attributes() 32 | for attribute in SORTABLE_ATTRIBUTES: 33 | assert attribute in get_attributes 34 | # Launch test to update at null the setting 35 | response = index.update_sortable_attributes(None) 36 | index.wait_for_task(response.task_uid) 37 | response = index.get_sortable_attributes() 38 | assert response == [] 39 | 40 | 41 | def test_reset_sortable_attributes(empty_index): 42 | """Tests resetting the sortable attributes setting to its default value""" 43 | index = empty_index() 44 | # Update the settings first 45 | response = index.update_sortable_attributes(SORTABLE_ATTRIBUTES) 46 | update = index.wait_for_task(response.task_uid) 47 | assert update.status == "succeeded" 48 | # Check the settings have been correctly updated 49 | get_attributes = index.get_sortable_attributes() 50 | assert len(get_attributes) == len(SORTABLE_ATTRIBUTES) 51 | for attribute in SORTABLE_ATTRIBUTES: 52 | assert attribute in get_attributes 53 | # Check the reset of the settings 54 | response = index.reset_sortable_attributes() 55 | index.wait_for_task(response.task_uid) 56 | response = index.get_sortable_attributes() 57 | assert response == [] 58 | -------------------------------------------------------------------------------- /tests/settings/test_settings_stop_words_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_STOP_WORDS = ["of", "the"] 2 | 3 | 4 | def test_get_stop_words_default(empty_index): 5 | """Tests getting stop words by default.""" 6 | response = empty_index().get_stop_words() 7 | assert response == [] 8 | 9 | 10 | def test_update_stop_words(empty_index): 11 | """Tests updating the stop words.""" 12 | index = empty_index() 13 | response = index.update_stop_words(NEW_STOP_WORDS) 14 | update = index.wait_for_task(response.task_uid) 15 | assert update.status == "succeeded" 16 | response = index.get_stop_words() 17 | for stop_word in NEW_STOP_WORDS: 18 | assert stop_word in response 19 | 20 | 21 | def test_reset_stop_words(empty_index): 22 | """Tests resetting the stop words setting to its default value""" 23 | index = empty_index() 24 | # Update the settings first 25 | response = index.update_stop_words(NEW_STOP_WORDS) 26 | update = index.wait_for_task(response.task_uid) 27 | assert update.status == "succeeded" 28 | # Check the settings have been correctly updated 29 | response = index.get_stop_words() 30 | for stop_word in NEW_STOP_WORDS: 31 | assert stop_word in response 32 | # Check the reset of the settings 33 | response = index.reset_stop_words() 34 | update = index.wait_for_task(response.task_uid) 35 | assert update.status == "succeeded" 36 | response = index.get_stop_words() 37 | assert response == [] 38 | -------------------------------------------------------------------------------- /tests/settings/test_settings_synonyms_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_SYNONYMS = {"hp": ["harry potter"]} 2 | 3 | 4 | def test_get_synonyms_default(empty_index): 5 | """Tests getting default synonyms.""" 6 | response = empty_index().get_synonyms() 7 | assert response == {} 8 | 9 | 10 | def test_update_synonyms(empty_index): 11 | """Tests updating synonyms.""" 12 | index = empty_index() 13 | response = index.update_synonyms(NEW_SYNONYMS) 14 | update = index.wait_for_task(response.task_uid) 15 | assert update.status == "succeeded" 16 | response = index.get_synonyms() 17 | for synonym in NEW_SYNONYMS: 18 | assert synonym in response 19 | 20 | 21 | def test_reset_synonyms(empty_index): 22 | """Tests resetting the synonyms setting to its default value.""" 23 | index = empty_index() 24 | # Update the settings first 25 | response = index.update_synonyms(NEW_SYNONYMS) 26 | update = index.wait_for_task(response.task_uid) 27 | assert update.status == "succeeded" 28 | # Check the settings have been correctly updated 29 | response = index.get_synonyms() 30 | for synonym in NEW_SYNONYMS: 31 | assert synonym in response 32 | # Check the reset of the settings 33 | response = index.reset_synonyms() 34 | update = index.wait_for_task(response.task_uid) 35 | assert update.status == "succeeded" 36 | response = index.get_synonyms() 37 | assert response == {} 38 | -------------------------------------------------------------------------------- /tests/settings/test_settings_text_separators_meilisearch.py: -------------------------------------------------------------------------------- 1 | NEW_SEPARATOR_TOKENS = ["|", "…"] 2 | NEW_NON_SEPARATOR_TOKENS = ["@", "#"] 3 | 4 | 5 | def test_get_separator_tokens_default(empty_index): 6 | """Tests getting the default value of separator tokens.""" 7 | separator_tokens = empty_index().get_separator_tokens() 8 | assert separator_tokens == [] 9 | 10 | 11 | def test_get_non_separator_tokens_default(empty_index): 12 | """Tests getting the default value of separator tokens.""" 13 | non_separator_tokens = empty_index().get_separator_tokens() 14 | assert non_separator_tokens == [] 15 | 16 | 17 | def test_update_separator_tokens(empty_index): 18 | """Tests updating the separator tokens.""" 19 | index = empty_index() 20 | task = index.update_separator_tokens(NEW_SEPARATOR_TOKENS) 21 | task = index.wait_for_task(task.task_uid) 22 | assert task.status == "succeeded" 23 | 24 | separator_tokens = index.get_separator_tokens() 25 | for token in NEW_SEPARATOR_TOKENS: 26 | assert token in separator_tokens 27 | 28 | 29 | def test_update_non_separator_tokens(empty_index): 30 | """Tests updating the non separator tokens.""" 31 | index = empty_index() 32 | task = index.update_non_separator_tokens(NEW_NON_SEPARATOR_TOKENS) 33 | task = index.wait_for_task(task.task_uid) 34 | assert task.status == "succeeded" 35 | 36 | non_separator_tokens = index.get_non_separator_tokens() 37 | for token in NEW_NON_SEPARATOR_TOKENS: 38 | assert token in non_separator_tokens 39 | 40 | 41 | def test_reset_separator_tokens(empty_index): 42 | """Tests resetting the separator tokens to its default empty list.""" 43 | index = empty_index() 44 | task = index.update_separator_tokens(NEW_SEPARATOR_TOKENS) 45 | task = index.wait_for_task(task.task_uid) 46 | assert task.status == "succeeded" 47 | 48 | separator_tokens = index.get_separator_tokens() 49 | for token in NEW_SEPARATOR_TOKENS: 50 | assert token in separator_tokens 51 | 52 | reset_task = index.reset_separator_tokens() 53 | reset_task = index.wait_for_task(reset_task.task_uid) 54 | assert reset_task.status == "succeeded" 55 | 56 | separator_tokens = index.get_separator_tokens() 57 | assert separator_tokens == [] 58 | 59 | 60 | def test_non_reset_separator_tokens(empty_index): 61 | """Tests resetting the separator tokens to its default empty list.""" 62 | index = empty_index() 63 | task = index.update_non_separator_tokens(NEW_NON_SEPARATOR_TOKENS) 64 | task = index.wait_for_task(task.task_uid) 65 | assert task.status == "succeeded" 66 | 67 | non_separator_tokens = index.get_non_separator_tokens() 68 | for token in NEW_NON_SEPARATOR_TOKENS: 69 | assert token in non_separator_tokens 70 | 71 | reset_task = index.reset_non_separator_tokens() 72 | reset_task = index.wait_for_task(reset_task.task_uid) 73 | assert reset_task.status == "succeeded" 74 | 75 | non_separator_tokens = index.get_non_separator_tokens() 76 | assert non_separator_tokens == [] 77 | -------------------------------------------------------------------------------- /tests/settings/test_settings_typo_tolerance_meilisearch.py: -------------------------------------------------------------------------------- 1 | DEFAULT_TYPO_TOLERANCE = { 2 | "enabled": True, 3 | "minWordSizeForTypos": { 4 | "oneTypo": 5, 5 | "twoTypos": 9, 6 | }, 7 | "disableOnWords": [], 8 | "disableOnAttributes": [], 9 | } 10 | 11 | NEW_TYPO_TOLERANCE = { 12 | "enabled": True, 13 | "minWordSizeForTypos": { 14 | "oneTypo": 6, 15 | "twoTypos": 10, 16 | }, 17 | "disableOnWords": [], 18 | "disableOnAttributes": ["title"], 19 | } 20 | 21 | 22 | def test_get_typo_tolerance_default(empty_index): 23 | """Tests getting default typo_tolerance.""" 24 | response = empty_index().get_typo_tolerance() 25 | 26 | assert response.model_dump(by_alias=True) == DEFAULT_TYPO_TOLERANCE 27 | 28 | 29 | def test_update_typo_tolerance(empty_index): 30 | """Tests updating typo_tolerance.""" 31 | index = empty_index() 32 | response_update = index.update_typo_tolerance(NEW_TYPO_TOLERANCE) 33 | update = index.wait_for_task(response_update.task_uid) 34 | response_get = index.get_typo_tolerance() 35 | 36 | assert update.status == "succeeded" 37 | for typo_tolerance in NEW_TYPO_TOLERANCE: # pylint: disable=consider-using-dict-items 38 | assert typo_tolerance in response_get.model_dump(by_alias=True) 39 | assert ( 40 | NEW_TYPO_TOLERANCE[typo_tolerance] 41 | == response_get.model_dump(by_alias=True)[typo_tolerance] 42 | ) 43 | 44 | 45 | def test_reset_typo_tolerance(empty_index): 46 | """Tests resetting the typo_tolerance setting to its default value.""" 47 | index = empty_index() 48 | 49 | # Update the settings 50 | response_update = index.update_typo_tolerance(NEW_TYPO_TOLERANCE) 51 | update1 = index.wait_for_task(response_update.task_uid) 52 | # Get the setting after update 53 | response_get = index.get_typo_tolerance() 54 | # Reset the setting 55 | response_reset = index.reset_typo_tolerance() 56 | update2 = index.wait_for_task(response_reset.task_uid) 57 | # Get the setting after reset 58 | response_last = index.get_typo_tolerance() 59 | 60 | assert update1.status == "succeeded" 61 | for typo_tolerance in NEW_TYPO_TOLERANCE: # pylint: disable=consider-using-dict-items 62 | assert ( 63 | NEW_TYPO_TOLERANCE[typo_tolerance] 64 | == response_get.model_dump(by_alias=True)[typo_tolerance] 65 | ) 66 | assert update2.status == "succeeded" 67 | assert response_last.model_dump(by_alias=True) == DEFAULT_TYPO_TOLERANCE 68 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | import pytest 4 | 5 | from meilisearch._utils import is_pydantic_2, iso_to_date_time 6 | 7 | 8 | def test_is_pydantic_2(): 9 | assert is_pydantic_2() is True 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "iso_date, expected", 14 | [ 15 | ("2021-05-11T03:12:22.563960100Z", datetime(2021, 5, 11, 3, 12, 22, 563960)), 16 | ("2021-05-11T03:12:22.563960100+00:00", datetime(2021, 5, 11, 3, 12, 22, 563960)), 17 | (datetime(2021, 5, 11, 3, 12, 22, 563960), datetime(2021, 5, 11, 3, 12, 22, 563960)), 18 | ( 19 | datetime(2023, 7, 12, 1, 40, 11, 993699, tzinfo=timezone.utc), 20 | datetime(2023, 7, 12, 1, 40, 11, 993699, tzinfo=timezone.utc), 21 | ), 22 | (None, None), 23 | ], 24 | ) 25 | def test_iso_to_date_time(iso_date, expected): 26 | converted = iso_to_date_time(iso_date) 27 | 28 | assert converted == expected 29 | 30 | 31 | def test_iso_to_date_time_invalid_format(): 32 | with pytest.raises(ValueError): 33 | iso_to_date_time("2023-07-13T23:37:20Z") 34 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pylint, mypy, py38, py39, py310, py311 3 | 4 | [testenv:pylint] 5 | whitelist_externals = 6 | pipenv 7 | python 8 | deps = pylint 9 | commands = 10 | pipenv run pylint meilisearch tests 11 | 12 | [testenv:mypy] 13 | whitelist_externals = 14 | pipenv 15 | python 16 | deps = mypy 17 | commands = 18 | pipenv run mypy meilisearch 19 | 20 | [testenv] 21 | whitelist_externals = 22 | pipenv 23 | python 24 | deps = pytest 25 | commands = 26 | pipenv install --dev 27 | pipenv run pytest 28 | --------------------------------------------------------------------------------