├── .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 |
3 |
4 |
5 | Meilisearch Python
6 |
7 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
--------------------------------------------------------------------------------