From 099b778676a86cee90b1fd9b813f78760d463fa4 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 15:10:52 +0100 Subject: [PATCH 01/35] chore: upgrade tooling --- DOCKER_README.md | 46 +++------------------ Dockerfile | 4 +- MAINTAINERS.md | 2 +- README.md | 105 ++++++++++++++++++----------------------------- requirements.txt | 4 +- setup.py | 14 ++++--- tox.ini | 34 ++++----------- 7 files changed, 67 insertions(+), 142 deletions(-) diff --git a/DOCKER_README.md b/DOCKER_README.md index 27c94e5..8d1b0aa 100644 --- a/DOCKER_README.md +++ b/DOCKER_README.md @@ -1,41 +1,7 @@ -In this page you will find our recommended way of installing Docker on your machine. -This guide is made for OSX users. - -## Install Docker - -First install Docker using [Homebrew](https://brew.sh/) -``` -$ brew install docker -``` - -You can then install [Docker Desktop](https://docs.docker.com/get-docker/) if you wish, or use `docker-machine`. As we prefer the second option, we will only document this one. - -## Setup your Docker - -Install `docker-machine` -``` -$ brew install docker-machine -``` - -Then install [VirtualBox](https://www.virtualbox.org/) with [Homebrew Cask](https://github.com/Homebrew/homebrew-cask) to get a driver for your Docker machine -``` -$ brew cask install virtualbox -``` - -You may need to enter your password and authorize the application in your `System Settings` > `Security & Privacy`. - -Create now a new machine, set it up as default and connect your shell to it (here we use zsh. The commands should anyway be displayed in each steps' output) - -``` -$ docker-machine create --driver virtualbox default -$ docker-machine env default -$ eval "$(docker-machine env default)" -``` - -Now you're all setup to use our provided Docker image! - ## Build the image +> Make sure to have [Docker installed](https://docs.docker.com/engine/install/) + ```bash docker build -t algoliasearch-django . ``` @@ -53,8 +19,8 @@ docker run -it --rm --env ALGOLIA_APPLICATION_ID=XXXXXX \ However, we advise you to export them. That way, you can use [Docker's shorten syntax](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file) to set your variables. ```bash -export ALGOLIA_APPLICATION_ID=XXXXXX -export ALGOLIA_API_KEY=XXX +export ALGOLIA_APPLICATION_ID=XXXXXX +export ALGOLIA_API_KEY=XXX docker run -it --rm --env ALGOLIA_APPLICATION_ID --env ALGOLIA_API_KEY -v $PWD:/code -w /code algoliasearch-django bash ``` @@ -64,8 +30,8 @@ Once your container is running, any changes you make in your IDE are directly re To launch the tests, you can use this command ```bash -tox -e py36-django31 +tox -e py313-django51 ``` -If you'd like to sue an env other that `py36-django31`, run `tox --listenvs` to see the list of available envs. +If you'd like to sue an env other that `py313-django51`, run `tox --listenvs` to see the list of available envs. Feel free to contact us if you have any questions. diff --git a/Dockerfile b/Dockerfile index d9a39ee..97b1d66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8-slim +FROM python:3.13-slim # Force the stdout and stderr streams to be unbuffered. # Ensure python output goes to your terminal @@ -7,6 +7,6 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /code COPY requirements.txt /code/ -RUN pip install -r requirements.txt +RUN pip3 install --upgrade pip && pip3 install -r requirements.txt COPY . /code/ diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 33c7143..0091440 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,4 +2,4 @@ | Name | Email | |-----------------|---------------------| -| Paul-Louis Nech | support@algolia.com | +| Algolia | support@algolia.com | diff --git a/README.md b/README.md index abcc4e6..94a3625 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@

-

DocumentationCommunity Forum • @@ -26,55 +25,56 @@ You can find the full reference on [Algolia's website](https://www.algolia.com/doc/framework-integration/django/). - - 1. **[Setup](#setup)** - * [Introduction](#introduction) - * [Install](#install) - * [Setup](#setup) - * [Quick Start](#quick-start) + + - [Introduction](#introduction) + - [Install](#install) + - [Setup](#setup) + - [Quick Start](#quick-start) 1. **[Commands](#commands)** - * [Commands](#commands) + + - [Commands](#commands) 1. **[Search](#search)** - * [Search](#search) + + - [Search](#search) 1. **[Geo-Search](#geo-search)** - * [Geo-Search](#geo-search) + + - [Geo-Search](#geo-search) 1. **[Tags](#tags)** - * [Tags](#tags) + + - [Tags](#tags) 1. **[Options](#options)** - * [Custom objectID](#custom-codeobjectidcode) - * [Custom index name](#custom-index-name) - * [Field Preprocessing and Related objects](#field-preprocessing-and-related-objects) - * [Index settings](#index-settings) - * [Restrict indexing to a subset of your data](#restrict-indexing-to-a-subset-of-your-data) - * [Multiple indices per model](#multiple-indices-per-model) - * [Temporarily disable the auto-indexing](#temporarily-disable-the-auto-indexing) -1. **[Tests](#tests)** - * [Run Tests](#run-tests) + - [Custom objectID](#custom-codeobjectidcode) + - [Custom index name](#custom-index-name) + - [Field Preprocessing and Related objects](#field-preprocessing-and-related-objects) + - [Index settings](#index-settings) + - [Restrict indexing to a subset of your data](#restrict-indexing-to-a-subset-of-your-data) + - [Multiple indices per model](#multiple-indices-per-model) + - [Temporarily disable the auto-indexing](#temporarily-disable-the-auto-indexing) -1. **[Troubleshooting](#troubleshooting)** - * [Frequently asked questions](#frequently-asked-questions) +1. **[Tests](#tests)** + - [Run Tests](#run-tests) +1. **[Troubleshooting](#troubleshooting)** + - [Frequently asked questions](#frequently-asked-questions) # Setup - - ## Introduction This package lets you easily integrate the Algolia Search API to your [Django](https://www.djangoproject.com/) project. It's based on the [algoliasearch-client-python](https://github.com/algolia/algoliasearch-client-python) package. You might be interested in this sample Django application providing a typeahead.js based auto-completion and Google-like instant search: [algoliasearch-django-example](https://github.com/algolia/algoliasearch-django-example). -- Compatible with **Python 2.7** and **Python 3.4+**. -- Supports **Django 1.7+**, **2.x** and **3.x**. +- Compatible with **Python 3.8+**. +- Supports **Django 4.x** and **5.x**. ## Install @@ -95,10 +95,10 @@ ALGOLIA = { There are several optional settings: -* `INDEX_PREFIX`: prefix all indices. Use it to separate different applications, like `site1_Products` and `site2_Products`. -* `INDEX_SUFFIX`: suffix all indices. Use it to differentiate development and production environments, like `Location_dev` and `Location_prod`. -* `AUTO_INDEXING`: automatically synchronize the models with Algolia (default to **True**). -* `RAISE_EXCEPTIONS`: raise exceptions on network errors instead of logging them (default to **settings.DEBUG**). +- `INDEX_PREFIX`: prefix all indices. Use it to separate different applications, like `site1_Products` and `site2_Products`. +- `INDEX_SUFFIX`: suffix all indices. Use it to differentiate development and production environments, like `Location_dev` and `Location_prod`. +- `AUTO_INDEXING`: automatically synchronize the models with Algolia (default to **True**). +- `RAISE_EXCEPTIONS`: raise exceptions on network errors instead of logging them (default to **settings.DEBUG**). ## Quick Start @@ -134,25 +134,17 @@ class YourModelIndex(AlgoliaIndex): ``` - - # Commands - - ## Commands -* `python manage.py algolia_reindex`: reindex all the registered models. This command will first send all the record to a temporary index and then moves it. - * you can pass ``--model`` parameter to reindex a given model -* `python manage.py algolia_applysettings`: (re)apply the index settings. -* `python manage.py algolia_clearindex`: clear the index - - +- `python manage.py algolia_reindex`: reindex all the registered models. This command will first send all the record to a temporary index and then moves it. + - you can pass `--model` parameter to reindex a given model +- `python manage.py algolia_applysettings`: (re)apply the index settings. +- `python manage.py algolia_clearindex`: clear the index # Search - - ## Search We recommend using our [InstantSearch.js library](https://www.algolia.com/doc/guides/building-search-ui/what-is-instantsearch/js/) to build your search @@ -169,12 +161,8 @@ params = { "hitsPerPage": 5 } response = raw_search(Contact, "jim", params) ``` - - # Geo-Search - - ## Geo-Search Use the `geo_field` attribute to localize your record. `geo_field` should be a callable that returns a tuple (latitude, longitude). @@ -195,12 +183,8 @@ class ContactIndex(AlgoliaIndex): algoliasearch.register(Contact, ContactIndex) ``` - - # Tags - - ## Tags Use the `tags` attributes to add tags to your record. It can be a field or a callable. @@ -212,16 +196,12 @@ class ArticleIndex(AlgoliaIndex): At query time, specify `{ tagFilters: 'tagvalue' }` or `{ tagFilters: ['tagvalue1', 'tagvalue2'] }` as search parameters to restrict the result set to specific tags. - - # Options - - ## Custom `objectID` You can choose which field will be used as the `objectID `. The field should be unique and can - be a string or integer. By default, we use the `pk` field of the model. +be a string or integer. By default, we use the `pk` field of the model. ```python class ArticleIndex(AlgoliaIndex): @@ -279,8 +259,8 @@ class ContactIndex(AlgoliaIndex): - With this configuration, you can search for a `Contact` using its `Account` names - You can use the associated `account_ids` at search-time to fetch more data from your -model (you should **only proxy the fields relevant for search** to keep your records' size -as small as possible) + model (you should **only proxy the fields relevant for search** to keep your records' size + as small as possible) ## Index settings @@ -400,12 +380,8 @@ with disable_auto_indexing(MyModel): ``` - - # Tests - - ## Run Tests To run the tests, first find your Algolia application id and Admin API key (found on the Credentials page). @@ -414,8 +390,8 @@ To run the tests, first find your Algolia application id and Admin API key (foun ALGOLIA_APPLICATION_ID={APPLICATION_ID} ALGOLIA_API_KEY={ADMIN_API_KEY} tox ``` - To override settings for some tests, use the [settings method](https://docs.djangoproject.com/en/1.11/topics/testing/tools/#django.test.SimpleTestCase.settings): + ```python class OverrideSettingsTestCase(TestCase): def setUp(self): @@ -433,15 +409,12 @@ class OverrideSettingsTestCase(TestCase): # ... ``` - - # Troubleshooting # Use the Dockerfile + If you want to contribute to this project without installing all its dependencies, you can use our Docker image. Please check our [dedicated guide](DOCKER_README.md) to learn more. ## Frequently asked questions Encountering an issue? Before reaching out to support, we recommend heading to our [FAQ](https://www.algolia.com/doc/framework-integration/django/faq/) where you will find answers for the most common issues and gotchas with the package. - - diff --git a/requirements.txt b/requirements.txt index c8cb9b4..cf996c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -django>=1.7 -algoliasearch>=3.0,<4.0 +django>=4.0 +algoliasearch>=4.0,<5.0 # dev dependencies pypandoc wheel diff --git a/setup.py b/setup.py index 55a7bda..051d80b 100644 --- a/setup.py +++ b/setup.py @@ -20,10 +20,10 @@ path_version = os.path.join(os.path.dirname(__file__), 'algoliasearch_django/version.py') -if sys.version_info[0] == 3: - exec(open(path_version).read()) +if sys.version_info < (3, 8): + raise RuntimeError("algoliasearch_django 4.x requires Python 3.8+") else: - execfile(path_version) + exec(open(path_version).read()) setup( @@ -31,7 +31,7 @@ version=VERSION, license='MIT License', packages=find_packages(exclude=['tests']), - install_requires=['django>=1.7', 'algoliasearch>=3.0,<4.0'], + install_requires=['django>=4.0', 'algoliasearch>=4.0,<5.0'], description='Algolia Search integration for Django', long_description=README, long_description_content_type='text/markdown', @@ -47,8 +47,12 @@ 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Internet :: WWW/HTTP', ] ) diff --git a/tox.ini b/tox.ini index 939d0c3..cc5123d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,10 @@ [tox] envlist = - {py34}-django17 - {py34,py35,py36}-django18 - {py34,py35,py36}-django19 - {py34,py35,py36}-django110 - {py34,py35,py36}-django111 - {py34,py35,py36}-django20 - {py34,py35,py36}-django21 - {py34,py35,py36}-django22LTS - {py36,py37,py38,py39}-django30 - {py36,py37,py38,py39}-django31 - {py36,py37,py38,py39,py310}-django32 {py38,py39,py310}-django40 {py38,py39,py310,py311}-django41 - {py38,py39,py310,py311}-django42 + {py38,py39,py310,py311,py312}-django42 + {py310,py311,py312}-django50 + {py310,py311,py312,py313}-django51 coverage skip_missing_interpreters = True @@ -22,28 +13,19 @@ deps = six mock factory_boy - py{34,311}: Faker>=1.0,<2.0 - django17: Django>=1.7,<1.8 - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 - django111: Django>=1.11,<2.0 - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 - django22LTS: Django>=2.2,<3.0 - django30: Django>=3.0,<3.1 - django31: Django>=3.1,<3.2 - django32: Django>=3.2,<3.3 + py{38,313}: Faker>=5.0,<6.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 + django50: Django>=5.0,<5.1 + django51: Django>=5.1,<5.2 passenv = ALGOLIA* commands = python runtests.py [versions] -twine = >=1.13,<2.0 -wheel = >=0.34,<1.0 +twine = >=5.1,<6.0 +wheel = >=0.45,<1.0 [testenv:coverage] basepython = python3.8 From 6b398ad705fcef3f858e41d968ad95655b18c2f0 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 15:15:43 +0100 Subject: [PATCH 02/35] chore: update ci --- .github/workflows/main.yml | 97 ++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 40 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70632b3..32462aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,51 +17,68 @@ jobs: strategy: matrix: include: - - version: "3.5.4" - toxenv: py35-django20 - os: ubuntu-20.04 - - version: "3.6.7" - toxenv: py36-django32 - os: ubuntu-20.04 - - version: "3.7.5" - toxenv: py37-django32 - os: ubuntu-20.04 - - version: "3.8.15" - toxenv: py38-django32 - os: ubuntu-20.04 - - version: "3.9" - toxenv: py39-django30 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django31 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django32 - os: ubuntu-latest - - version: "3.9" + # django 4.0 + - version: "3.8.20" + toxenv: py38-django40 + os: ubuntu-20.04 + - version: "3.9.20" toxenv: py39-django40 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django41 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django42 - os: ubuntu-latest - - version: "3.10" + os: ubuntu-20.04 + - version: "3.10.15" toxenv: py310-django40 - os: ubuntu-latest - - version: "3.10" + os: ubuntu-20.04 + # django 4.1 + - version: "3.8.20" + toxenv: py38-django41 + os: ubuntu-20.04 + - version: "3.9.20" + toxenv: py39-django41 + os: ubuntu-20.04 + - version: "3.10.15" toxenv: py310-django41 - os: ubuntu-latest - - version: "3.10" - toxenv: py310-django42 - os: ubuntu-latest - - version: "3.11" + os: ubuntu-20.04 + - version: "3.11.10" toxenv: py311-django41 - os: ubuntu-latest - - version: "3.11" + os: ubuntu-20.04 + # django 4.2 + - version: "3.8.20" + toxenv: py38-django42 + os: ubuntu-20.04 + - version: "3.9.20" + toxenv: py39-django42 + os: ubuntu-20.04 + - version: "3.10.15" + toxenv: py310-django42 + os: ubuntu-20.04 + - version: "3.11.10" toxenv: py311-django42 - os: ubuntu-latest + os: ubuntu-20.04 + - version: "3.12.7" + toxenv: py312-django42 + os: ubuntu-20.04 + # django 5.0 + - version: "3.10.15" + toxenv: py310-django50 + os: ubuntu-20.04 + - version: "3.11.10" + toxenv: py311-django50 + os: ubuntu-20.04 + - version: "3.12.7" + toxenv: py312-django50 + os: ubuntu-20.04 + # django 5.1 + - version: "3.10.15" + toxenv: py310-django51 + os: ubuntu-20.04 + - version: "3.11.10" + toxenv: py311-django51 + os: ubuntu-20.04 + - version: "3.12.7" + toxenv: py312-django51 + os: ubuntu-20.04 + - version: "3.13.0" + toxenv: py313-django51 + os: ubuntu-20.04 steps: - uses: actions/checkout@v3 From 2587b37302f950f52b61b63d9b6da972fdd4c997 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 15:18:59 +0100 Subject: [PATCH 03/35] chore: update pr and issue template --- .github/ISSUE_TEMPLATE.md | 89 +++++++++++++++++++++++++------- .github/PULL_REQUEST_TEMPLATE.md | 17 +++--- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8869d73..da2ba62 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,22 +1,73 @@ - + If you are not sure about the origin of the issue, or if it impacts your customer experience, please contact [our support team](https://alg.li/support). + - type: textarea + attributes: + label: Description + description: A clear and concise description of what the bug is. + validations: + required: true + - type: dropdown + id: python version + attributes: + label: python version + description: What is the Python version you've reproduced the error with + options: + - 3.8 + - 3.9 + - 3.10 + - 3.11 + - 3.12 + - 3.13 + validations: + required: true + - type: dropdown + id: django version + attributes: + label: Django version + description: What is the Django version you've reproduced the error with + options: + - 4.0 + - 4.1 + - 4.2 + - 5.0 + - 5.1 + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: Write down the steps to reproduce the bug, please include any information that seems relevant for us to reproduce it properly + placeholder: | + 1. Use method `...` + 2. With parameters `...` + 3. See error + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + attributes: + label: Self-service + description: | + If you feel like you could contribute to this issue, please check the box below. This would tell us and other people looking for contributions that someone's working on it. + If you do check this box, please send a pull request within 7 days so we can still delegate this to someone else. + options: + - label: I'd be willing to fix this bug myself. -- Django version: -- Algolia Django integration version: -- Algolia Client Version: #.#.# -- Language Version: - -### Description - - -### Steps To Reproduce diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 24da431..3e0f401 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,17 @@ -| Q | A -| ----------------- | ---------- -| Bug fix? | yes/no -| New feature? | yes/no -| BC breaks? | no -| Related Issue | Fix #... -| Need Doc update | yes/no +## 🧭 What and Why +🎟 JIRA Ticket: -## Describe your change +### Changes included: -## What problem is this fixing? +## 🧪 Test From 9d9c8761141013a6138a03019c8b6cebb589f9b8 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 15:24:02 +0100 Subject: [PATCH 04/35] chore: minor python version --- .github/workflows/main.yml | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 32462aa..0ecf25b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,67 +18,67 @@ jobs: matrix: include: # django 4.0 - - version: "3.8.20" + - version: "3.8" toxenv: py38-django40 - os: ubuntu-20.04 - - version: "3.9.20" + os: ubuntu-22.04 + - version: "3.9" toxenv: py39-django40 - os: ubuntu-20.04 - - version: "3.10.15" + os: ubuntu-22.04 + - version: "3.10" toxenv: py310-django40 - os: ubuntu-20.04 + os: ubuntu-22.04 # django 4.1 - - version: "3.8.20" + - version: "3.8" toxenv: py38-django41 - os: ubuntu-20.04 - - version: "3.9.20" + os: ubuntu-22.04 + - version: "3.9" toxenv: py39-django41 - os: ubuntu-20.04 - - version: "3.10.15" + os: ubuntu-22.04 + - version: "3.10" toxenv: py310-django41 - os: ubuntu-20.04 - - version: "3.11.10" + os: ubuntu-22.04 + - version: "3.11" toxenv: py311-django41 - os: ubuntu-20.04 + os: ubuntu-22.04 # django 4.2 - - version: "3.8.20" + - version: "3.8" toxenv: py38-django42 - os: ubuntu-20.04 - - version: "3.9.20" + os: ubuntu-22.04 + - version: "3.9" toxenv: py39-django42 - os: ubuntu-20.04 - - version: "3.10.15" + os: ubuntu-22.04 + - version: "3.10" toxenv: py310-django42 - os: ubuntu-20.04 - - version: "3.11.10" + os: ubuntu-22.04 + - version: "3.11" toxenv: py311-django42 - os: ubuntu-20.04 - - version: "3.12.7" + os: ubuntu-22.04 + - version: "3.12" toxenv: py312-django42 - os: ubuntu-20.04 + os: ubuntu-22.04 # django 5.0 - - version: "3.10.15" + - version: "3.10" toxenv: py310-django50 - os: ubuntu-20.04 - - version: "3.11.10" + os: ubuntu-22.04 + - version: "3.11" toxenv: py311-django50 - os: ubuntu-20.04 - - version: "3.12.7" + os: ubuntu-22.04 + - version: "3.12" toxenv: py312-django50 - os: ubuntu-20.04 + os: ubuntu-22.04 # django 5.1 - - version: "3.10.15" + - version: "3.10" toxenv: py310-django51 - os: ubuntu-20.04 - - version: "3.11.10" + os: ubuntu-22.04 + - version: "3.11" toxenv: py311-django51 - os: ubuntu-20.04 - - version: "3.12.7" + os: ubuntu-22.04 + - version: "3.12" toxenv: py312-django51 - os: ubuntu-20.04 - - version: "3.13.0" + os: ubuntu-22.04 + - version: "3.13" toxenv: py313-django51 - os: ubuntu-20.04 + os: ubuntu-22.04 steps: - uses: actions/checkout@v3 From 2f9aa46b5a474caa07a6090f4b50bc0ff9410390 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 15:46:45 +0100 Subject: [PATCH 05/35] chore: use pip3 --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ecf25b..9d44ae2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -92,7 +92,7 @@ jobs: run: | python -m venv python-ci-run source python-ci-run/bin/activate - python -m pip install --upgrade pip - python -m pip install tox - python -m pip install -r requirements.txt + pip3 install --upgrade pip + pip3 install tox + pip3 install -r requirements.txt TOXENV=${{ matrix.toxenv }} ALGOLIA_APPLICATION_ID=${{ secrets.ALGOLIA_APPLICATION_ID }} ALGOLIA_API_KEY=${{ secrets.ALGOLIA_API_KEY }} tox From ca426a7f16c848c37013aa9925872a87a8319afc Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 16:47:11 +0100 Subject: [PATCH 06/35] chore: format and setup linter --- algoliasearch_django/__init__.py | 12 +- algoliasearch_django/apps.py | 2 +- algoliasearch_django/decorators.py | 17 +- .../commands/algolia_applysettings.py | 11 +- .../management/commands/algolia_clearindex.py | 11 +- .../management/commands/algolia_reindex.py | 15 +- algoliasearch_django/models.py | 212 ++++--- algoliasearch_django/registration.py | 34 +- algoliasearch_django/settings.py | 2 +- algoliasearch_django/version.py | 2 +- requirements.txt | 2 + runtests.py | 7 +- setup.py | 71 ++- tests/factories.py | 26 +- tests/models.py | 7 +- tests/settings.py | 59 +- tests/test_commands.py | 55 +- tests/test_decorators.py | 70 +-- tests/test_engine.py | 24 +- tests/test_index.py | 560 ++++++++++-------- tests/test_signal.py | 40 +- tox.ini | 18 +- 22 files changed, 672 insertions(+), 585 deletions(-) diff --git a/algoliasearch_django/__init__.py b/algoliasearch_django/__init__.py index 95c7c7d..08751ea 100644 --- a/algoliasearch_django/__init__.py +++ b/algoliasearch_django/__init__.py @@ -5,6 +5,7 @@ from django.utils.module_loading import autodiscover_modules +import logging from . import models from . import registration from . import settings @@ -30,13 +31,10 @@ delete_record = algolia_engine.delete_record update_records = algolia_engine.update_records raw_search = algolia_engine.raw_search -clear_index = algolia_engine.clear_index # TODO: deprecate +clear_index = algolia_engine.clear_index # TODO: deprecate clear_objects = algolia_engine.clear_objects reindex_all = algolia_engine.reindex_all -# Default log handler -import logging - class NullHandler(logging.Handler): def emit(self, record): @@ -44,9 +42,9 @@ def emit(self, record): def autodiscover(): - autodiscover_modules('index') + autodiscover_modules("index") -logging.getLogger(__name__.split('.')[0]).addHandler(NullHandler()) +logging.getLogger(__name__.split(".")[0]).addHandler(NullHandler()) -default_app_config = 'algoliasearch_django.apps.AlgoliaConfig' +default_app_config = "algoliasearch_django.apps.AlgoliaConfig" diff --git a/algoliasearch_django/apps.py b/algoliasearch_django/apps.py index a614999..26ae6c3 100644 --- a/algoliasearch_django/apps.py +++ b/algoliasearch_django/apps.py @@ -4,7 +4,7 @@ class AlgoliaConfig(AppConfig): """Simple AppConfig which does not do automatic discovery.""" - name = 'algoliasearch_django' + name = "algoliasearch_django" def ready(self): super(AlgoliaConfig, self).ready() diff --git a/algoliasearch_django/decorators.py b/algoliasearch_django/decorators.py index 23c4eae..a85e268 100644 --- a/algoliasearch_django/decorators.py +++ b/algoliasearch_django/decorators.py @@ -26,11 +26,13 @@ class ContextDecorator: """ A base class that enables a context manager to also be used as a decorator. """ + def __call__(self, func): @wraps(func, assigned=available_attrs(func)) def inner(*args, **kwargs): with self: return func(*args, **kwargs) + return inner @@ -47,11 +49,12 @@ class AuthorIndex(AlgoliaIndex): def _algolia_engine_wrapper(index_class): if not issubclass(index_class, AlgoliaIndex): - raise ValueError('Wrapped class must subclass AlgoliaIndex.') + raise ValueError("Wrapped class must subclass AlgoliaIndex.") register(model, index_class) return index_class + return _algolia_engine_wrapper @@ -76,21 +79,17 @@ def __init__(self, model=None): def __enter__(self): for model in self.models: post_save.disconnect( - algolia_engine._AlgoliaEngine__post_save_receiver, - sender=model + algolia_engine._AlgoliaEngine__post_save_receiver, sender=model ) pre_delete.disconnect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, - sender=model + algolia_engine._AlgoliaEngine__pre_delete_receiver, sender=model ) def __exit__(self, exc_type, exc_value, traceback): for model in self.models: post_save.connect( - algolia_engine._AlgoliaEngine__post_save_receiver, - sender=model + algolia_engine._AlgoliaEngine__post_save_receiver, sender=model ) pre_delete.connect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, - sender=model + algolia_engine._AlgoliaEngine__pre_delete_receiver, sender=model ) diff --git a/algoliasearch_django/management/commands/algolia_applysettings.py b/algoliasearch_django/management/commands/algolia_applysettings.py index cb034e4..69df31b 100644 --- a/algoliasearch_django/management/commands/algolia_applysettings.py +++ b/algoliasearch_django/management/commands/algolia_applysettings.py @@ -5,18 +5,17 @@ class Command(BaseCommand): - help = 'Apply index settings.' + help = "Apply index settings." def add_arguments(self, parser): - parser.add_argument('--model', nargs='+', type=str) + parser.add_argument("--model", nargs="+", type=str) def handle(self, *args, **options): """Run the management command.""" - self.stdout.write('Apply settings to index:') + self.stdout.write("Apply settings to index:") for model in get_registered_model(): - if options.get('model', None) and not (model.__name__ in - options['model']): + if options.get("model", None) and model.__name__ not in options["model"]: continue get_adapter(model).set_settings() - self.stdout.write('\t* {}'.format(model.__name__)) + self.stdout.write("\t* {}".format(model.__name__)) diff --git a/algoliasearch_django/management/commands/algolia_clearindex.py b/algoliasearch_django/management/commands/algolia_clearindex.py index a2c4ade..3e5dff0 100644 --- a/algoliasearch_django/management/commands/algolia_clearindex.py +++ b/algoliasearch_django/management/commands/algolia_clearindex.py @@ -5,18 +5,17 @@ class Command(BaseCommand): - help = 'Clear index.' + help = "Clear index." def add_arguments(self, parser): - parser.add_argument('--model', nargs='+', type=str) + parser.add_argument("--model", nargs="+", type=str) def handle(self, *args, **options): """Run the management command.""" - self.stdout.write('Clear index:') + self.stdout.write("Clear index:") for model in get_registered_model(): - if options.get('model', None) and not (model.__name__ in - options['model']): + if options.get("model", None) and model.__name__ not in options["model"]: continue clear_objects(model) - self.stdout.write('\t* {}'.format(model.__name__)) + self.stdout.write("\t* {}".format(model.__name__)) diff --git a/algoliasearch_django/management/commands/algolia_reindex.py b/algoliasearch_django/management/commands/algolia_reindex.py index ce9b9da..bd1ad50 100644 --- a/algoliasearch_django/management/commands/algolia_reindex.py +++ b/algoliasearch_django/management/commands/algolia_reindex.py @@ -5,25 +5,24 @@ class Command(BaseCommand): - help = 'Reindex all models to Algolia' + help = "Reindex all models to Algolia" def add_arguments(self, parser): - parser.add_argument('--batchsize', nargs='?', default=1000, type=int) - parser.add_argument('--model', nargs='+', type=str) + parser.add_argument("--batchsize", nargs="?", default=1000, type=int) + parser.add_argument("--model", nargs="+", type=str) def handle(self, *args, **options): """Run the management command.""" - batch_size = options.get('batchsize', None) + batch_size = options.get("batchsize", None) if not batch_size: # py34-django18: batchsize is set to None if the user don't set # the value, instead of not be present in the dict batch_size = 1000 - self.stdout.write('The following models were reindexed:') + self.stdout.write("The following models were reindexed:") for model in get_registered_model(): - if options.get('model', None) and not (model.__name__ in - options['model']): + if options.get("model", None) and model.__name__ not in options["model"]: continue counts = reindex_all(model, batch_size=batch_size) - self.stdout.write('\t* {} --> {}'.format(model.__name__, counts)) + self.stdout.write("\t* {} --> {}".format(model.__name__, counts)) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index bb3730e..63e7cb3 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -5,7 +5,6 @@ from itertools import chain import logging -import sys from algoliasearch.exceptions import AlgoliaException from django.db.models.query_utils import DeferredAttribute @@ -26,8 +25,7 @@ def check_and_get_attr(model, name): else: return get_model_attr(name) except AttributeError: - raise AlgoliaIndexError( - '{} is not an attribute of {}'.format(name, model)) + raise AlgoliaIndexError("{} is not an attribute of {}".format(name, model)) def get_model_attr(name): @@ -43,7 +41,7 @@ class AlgoliaIndex(object): # Use to specify a custom field that will be used for the objectID. # This field should be unique. - custom_objectID = 'pk' + custom_objectID = "pk" # Use to specify the fields that should be included in the index. fields = () @@ -80,11 +78,15 @@ def __init__(self, model, client, settings): self.__named_fields = {} self.__translate_fields = {} - if self.settings is None: # Only set settings if the actual index class does not define some + if ( + self.settings is None + ): # Only set settings if the actual index class does not define some self.settings = {} try: - all_model_fields = [f.name for f in model._meta.get_fields() if not f.is_relation] + all_model_fields = [ + f.name for f in model._meta.get_fields() if not f.is_relation + ] except AttributeError: # get_fields requires Django >= 1.8 all_model_fields = [f.name for f in model._meta.local_fields] @@ -93,13 +95,11 @@ def __init__(self, model, client, settings): elif isinstance(self.fields, (list, tuple, set)): self.fields = tuple(self.fields) else: - raise AlgoliaIndexError('Fields must be a str, list, tuple or set') + raise AlgoliaIndexError("Fields must be a str, list, tuple or set") # Check fields for field in self.fields: - # unicode is a type in python < 3.0, which we need to support (e.g. dev uses unicode_literals) - # noinspection PyUnresolvedReferences - if sys.version_info < (3, 0) and isinstance(field, unicode) or isinstance(field, str): + if isinstance(field, str): attr = field name = field elif isinstance(field, (list, tuple)) and len(field) == 2: @@ -107,7 +107,8 @@ def __init__(self, model, client, settings): name = field[1] else: raise AlgoliaIndexError( - 'Invalid fields syntax: {} (type: {})'.format(field, type(field))) + "Invalid fields syntax: {} (type: {})".format(field, type(field)) + ) self.__translate_fields[attr] = name if attr in all_model_fields: @@ -118,21 +119,25 @@ def __init__(self, model, client, settings): # If no fields are specified, index all the fields of the model if not self.fields: self.fields = set(all_model_fields) - for elt in ('pk', 'id', 'objectID'): + for elt in ("pk", "id", "objectID"): try: self.fields.remove(elt) except KeyError: continue self.__translate_fields = dict(zip(self.fields, self.fields)) - self.__named_fields = dict(zip(self.fields, map(get_model_attr, - self.fields))) + self.__named_fields = dict( + zip(self.fields, map(get_model_attr, self.fields)) + ) # Check custom_objectID - if self.custom_objectID in chain(['pk'], all_model_fields) or hasattr(model, self.custom_objectID): + if self.custom_objectID in chain(["pk"], all_model_fields) or hasattr( + model, self.custom_objectID + ): self.objectID = get_model_attr(self.custom_objectID) else: - raise AlgoliaIndexError('{} is not a model field of {}'.format( - self.custom_objectID, model)) + raise AlgoliaIndexError( + "{} is not a model field of {}".format(self.custom_objectID, model) + ) # Check tags if self.tags: @@ -149,34 +154,37 @@ def __init__(self, model, client, settings): if self.should_index: if hasattr(model, self.should_index): attr = getattr(model, self.should_index) - if type(attr) is not bool: # if attr is a bool, we keep attr=name to getattr on instance + if ( + type(attr) is not bool + ): # if attr is a bool, we keep attr=name to getattr on instance self.should_index = attr if callable(self.should_index): self._should_index_is_method = True else: try: model._meta.get_field_by_name(self.should_index) - except: - raise AlgoliaIndexError('{} is not an attribute nor a field of {}.'.format( - self.should_index, model)) + except Exception: + raise AlgoliaIndexError( + "{} is not an attribute nor a field of {}.".format( + self.should_index, model + ) + ) def __init_index(self, client, model, settings): if not self.index_name: self.index_name = model.__name__ - tmp_index_name = '{index_name}_tmp'.format(index_name=self.index_name) + tmp_index_name = "{index_name}_tmp".format(index_name=self.index_name) - if 'INDEX_PREFIX' in settings: - self.index_name = settings['INDEX_PREFIX'] + '_' + self.index_name - tmp_index_name = '{index_prefix}_{tmp_index_name}'.format( - tmp_index_name=tmp_index_name, - index_prefix=settings['INDEX_PREFIX'] + if "INDEX_PREFIX" in settings: + self.index_name = settings["INDEX_PREFIX"] + "_" + self.index_name + tmp_index_name = "{index_prefix}_{tmp_index_name}".format( + tmp_index_name=tmp_index_name, index_prefix=settings["INDEX_PREFIX"] ) - if 'INDEX_SUFFIX' in settings: - self.index_name += '_' + settings['INDEX_SUFFIX'] - tmp_index_name = '{tmp_index_name}_{index_suffix}'.format( - tmp_index_name=tmp_index_name, - index_suffix=settings['INDEX_SUFFIX'] + if "INDEX_SUFFIX" in settings: + self.index_name += "_" + settings["INDEX_SUFFIX"] + tmp_index_name = "{tmp_index_name}_{index_suffix}".format( + tmp_index_name=tmp_index_name, index_suffix=settings["INDEX_SUFFIX"] ) self.tmp_index_name = tmp_index_name @@ -189,7 +197,7 @@ def _validate_geolocation(geolocation): """ Make sure we have the proper geolocation format. """ - if set(geolocation) != {'lat', 'lng'}: + if set(geolocation) != {"lat", "lng"}: raise AlgoliaIndexError( 'Invalid geolocation format, requires "lat" and "lng" keys only got {}'.format( geolocation @@ -204,7 +212,7 @@ def get_raw_record(self, instance, update_fields=None): the objectID and the given fields. Also, `_geoloc` and `_tags` will not be included. """ - tmp = {'objectID': self.objectID(instance)} + tmp = {"objectID": self.objectID(instance)} if update_fields: if isinstance(update_fields, str): @@ -222,21 +230,21 @@ def get_raw_record(self, instance, update_fields=None): loc = self.geo_field(instance) if isinstance(loc, tuple): - tmp['_geoloc'] = {'lat': loc[0], 'lng': loc[1]} + tmp["_geoloc"] = {"lat": loc[0], "lng": loc[1]} elif isinstance(loc, dict): self._validate_geolocation(loc) - tmp['_geoloc'] = loc + tmp["_geoloc"] = loc elif isinstance(loc, list): [self._validate_geolocation(geo) for geo in loc] - tmp['_geoloc'] = loc + tmp["_geoloc"] = loc if self.tags: if callable(self.tags): - tmp['_tags'] = self.tags(instance) - if not isinstance(tmp['_tags'], list): - tmp['_tags'] = list(tmp['_tags']) + tmp["_tags"] = self.tags(instance) + if not isinstance(tmp["_tags"], list): + tmp["_tags"] = list(tmp["_tags"]) - logger.debug('BUILD %s FROM %s', tmp['objectID'], self.model) + logger.debug("BUILD %s FROM %s", tmp["objectID"], self.model) return tmp def _has_should_index(self): @@ -276,11 +284,16 @@ def _should_really_index(self, instance): elif attr_type is property: attr_value = self.should_index.__get__(instance) else: - raise AlgoliaIndexError('{} should be a boolean attribute or a method that returns a boolean.'.format( - self.should_index)) + raise AlgoliaIndexError( + "{} should be a boolean attribute or a method that returns a boolean.".format( + self.should_index + ) + ) if type(attr_value) is not bool: - raise AlgoliaIndexError("%s's should_index (%s) should be a boolean" % ( - instance.__class__.__name__, self.should_index)) + raise AlgoliaIndexError( + "%s's should_index (%s) should be a boolean" + % (instance.__class__.__name__, self.should_index) + ) return attr_value def save_record(self, instance, update_fields=None, **kwargs): @@ -301,33 +314,32 @@ def save_record(self, instance, update_fields=None, **kwargs): try: if update_fields: - obj = self.get_raw_record(instance, - update_fields=update_fields) + obj = self.get_raw_record(instance, update_fields=update_fields) result = self.__index.partial_update_object(obj) else: obj = self.get_raw_record(instance) result = self.__index.save_object(obj) - logger.info('SAVE %s FROM %s', obj['objectID'], self.model) + logger.info("SAVE %s FROM %s", obj["objectID"], self.model) return result except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('%s FROM %s NOT SAVED: %s', obj['objectID'], - self.model, e) + logger.warning( + "%s FROM %s NOT SAVED: %s", obj["objectID"], self.model, e + ) def delete_record(self, instance): """Deletes the record.""" objectID = self.objectID(instance) try: self.__index.delete_object(objectID) - logger.info('DELETE %s FROM %s', objectID, self.model) + logger.info("DELETE %s FROM %s", objectID, self.model) except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('%s FROM %s NOT DELETED: %s', objectID, - self.model, e) + logger.warning("%s FROM %s NOT DELETED: %s", objectID, self.model, e) def update_records(self, qs, batch_size=1000, **kwargs): """ @@ -350,9 +362,10 @@ def update_records(self, qs, batch_size=1000, **kwargs): batch = [] objectsIDs = qs.only(self.custom_objectID).values_list( - self.custom_objectID, flat=True) + self.custom_objectID, flat=True + ) for elt in objectsIDs: - tmp['objectID'] = elt + tmp["objectID"] = elt batch.append(dict(tmp)) if len(batch) >= batch_size: @@ -362,7 +375,7 @@ def update_records(self, qs, batch_size=1000, **kwargs): if len(batch) > 0: self.__index.partial_update_objects(batch) - def raw_search(self, query='', params=None): + def raw_search(self, query="", params=None): """Performs a search query and returns the parsed JSON.""" if params is None: params = {} @@ -373,19 +386,18 @@ def raw_search(self, query='', params=None): if DEBUG: raise e else: - logger.warning('ERROR DURING SEARCH ON %s: %s', self.index_name, e) + logger.warning("ERROR DURING SEARCH ON %s: %s", self.index_name, e) def get_settings(self): """Returns the settings of the index.""" try: - logger.info('GET SETTINGS ON %s', self.index_name) + logger.info("GET SETTINGS ON %s", self.index_name) return self.__index.get_settings() except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('ERROR DURING GET_SETTINGS ON %s: %s', - self.model, e) + logger.warning("ERROR DURING GET_SETTINGS ON %s: %s", self.model, e) def set_settings(self): """Applies the settings to the index.""" @@ -394,24 +406,23 @@ def set_settings(self): try: self.__index.set_settings(self.settings) - logger.info('APPLY SETTINGS ON %s', self.index_name) + logger.info("APPLY SETTINGS ON %s", self.index_name) except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('SETTINGS NOT APPLIED ON %s: %s', - self.model, e) + logger.warning("SETTINGS NOT APPLIED ON %s: %s", self.model, e) def clear_objects(self): """Clears all objects of an index.""" try: self.__index.clear_objects() - logger.info('CLEAR INDEX %s', self.index_name) + logger.info("CLEAR INDEX %s", self.index_name) except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('%s NOT CLEARED: %s', self.model, e) + logger.warning("%s NOT CLEARED: %s", self.model, e) def clear_index(self): # TODO: add deprecated warning @@ -420,12 +431,12 @@ def clear_index(self): def wait_task(self, task_id): try: self.__index.wait_task(task_id) - logger.info('WAIT TASK %s', self.index_name) + logger.info("WAIT TASK %s", self.index_name) except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('%s NOT WAIT: %s', self.model, e) + logger.warning("%s NOT WAIT: %s", self.model, e) def delete(self): self.__index.delete() @@ -445,9 +456,13 @@ def reindex_all(self, batch_size=1000): try: if not self.settings: self.settings = self.get_settings() - logger.debug('Got settings for index %s: %s', self.index_name, self.settings) + logger.debug( + "Got settings for index %s: %s", self.index_name, self.settings + ) else: - logger.debug("index %s already has settings: %s", self.index_name, self.settings) + logger.debug( + "index %s already has settings: %s", self.index_name, self.settings + ) except AlgoliaException as e: if any("Index does not exist" in arg for arg in e.args): pass # Expected, let's clear and recreate from scratch @@ -455,21 +470,21 @@ def reindex_all(self, batch_size=1000): raise e # Unexpected error while getting settings try: if self.settings: - replicas = self.settings.get('replicas', None) - slaves = self.settings.get('slaves', None) + replicas = self.settings.get("replicas", None) + slaves = self.settings.get("slaves", None) should_keep_replicas = replicas is not None should_keep_slaves = slaves is not None if should_keep_replicas: - self.settings['replicas'] = [] + self.settings["replicas"] = [] logger.debug("REMOVE REPLICAS FROM SETTINGS") if should_keep_slaves: - self.settings['slaves'] = [] + self.settings["slaves"] = [] logger.debug("REMOVE SLAVES FROM SETTINGS") self.__tmp_index.set_settings(self.settings).wait() - logger.debug('APPLY SETTINGS ON %s_tmp', self.index_name) + logger.debug("APPLY SETTINGS ON %s_tmp", self.index_name) rules = [] synonyms = [] for r in self.__index.browse_rules(): @@ -477,19 +492,19 @@ def reindex_all(self, batch_size=1000): for s in self.__index.browse_synonyms(): synonyms.append(s) if len(rules): - logger.debug('Got rules for index %s: %s', self.index_name, rules) + logger.debug("Got rules for index %s: %s", self.index_name, rules) should_keep_rules = True if len(synonyms): - logger.debug('Got synonyms for index %s: %s', self.index_name, rules) + logger.debug("Got synonyms for index %s: %s", self.index_name, rules) should_keep_synonyms = True self.__tmp_index.clear_objects() - logger.debug('CLEAR INDEX %s_tmp', self.index_name) + logger.debug("CLEAR INDEX %s_tmp", self.index_name) counts = 0 batch = [] - if hasattr(self, 'get_queryset'): + if hasattr(self, "get_queryset"): qs = self.get_queryset() else: qs = self.model.objects.all() @@ -501,41 +516,50 @@ def reindex_all(self, batch_size=1000): batch.append(self.get_raw_record(instance)) if len(batch) >= batch_size: self.__tmp_index.save_objects(batch) - logger.info('SAVE %d OBJECTS TO %s_tmp', len(batch), - self.index_name) + logger.info( + "SAVE %d OBJECTS TO %s_tmp", len(batch), self.index_name + ) batch = [] counts += 1 if len(batch) > 0: self.__tmp_index.save_objects(batch) - logger.info('SAVE %d OBJECTS TO %s_tmp', len(batch), - self.index_name) + logger.info("SAVE %d OBJECTS TO %s_tmp", len(batch), self.index_name) - self.__client.move_index(self.tmp_index_name, - self.index_name) - logger.info('MOVE INDEX %s_tmp TO %s', self.index_name, - self.index_name) + self.__client.move_index(self.tmp_index_name, self.index_name) + logger.info("MOVE INDEX %s_tmp TO %s", self.index_name, self.index_name) if self.settings: if should_keep_replicas: - self.settings['replicas'] = replicas + self.settings["replicas"] = replicas logger.debug("RESTORE REPLICAS") if should_keep_slaves: - self.settings['slaves'] = slaves + self.settings["slaves"] = slaves logger.debug("RESTORE SLAVES") if should_keep_replicas or should_keep_slaves: self.__index.set_settings(self.settings) if should_keep_rules: - response = self.__index.save_rules(rules, {'forwardToReplicas': True}) + response = self.__index.save_rules( + rules, {"forwardToReplicas": True} + ) response.wait() - logger.info("Saved rules for index %s with response: {}".format(response), self.index_name) + logger.info( + "Saved rules for index %s with response: {}".format(response), + self.index_name, + ) if should_keep_synonyms: - response = self.__index.save_synonyms(synonyms, {'forwardToReplicas': True}) + response = self.__index.save_synonyms( + synonyms, {"forwardToReplicas": True} + ) response.wait() - logger.info("Saved synonyms for index %s with response: {}".format(response), self.index_name) + logger.info( + "Saved synonyms for index %s with response: {}".format( + response + ), + self.index_name, + ) return counts except AlgoliaException as e: if DEBUG: raise e else: - logger.warning('ERROR DURING REINDEXING %s: %s', self.model, - e) + logger.warning("ERROR DURING REINDEXING %s: %s", self.model, e) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index 2616511..bd93ac9 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -30,13 +30,12 @@ def __init__(self, settings=SETTINGS): """Initializes the Algolia engine.""" try: - app_id = settings['APPLICATION_ID'] - api_key = settings['API_KEY'] + app_id = settings["APPLICATION_ID"] + api_key = settings["API_KEY"] except KeyError: - raise AlgoliaEngineError( - 'APPLICATION_ID and API_KEY must be defined.') + raise AlgoliaEngineError("APPLICATION_ID and API_KEY must be defined.") - self.__auto_indexing = settings.get('AUTO_INDEXING', True) + self.__auto_indexing = settings.get("AUTO_INDEXING", True) self.__settings = settings self.__registered_models = {} @@ -56,21 +55,22 @@ def register(self, model, index_cls=AlgoliaIndex, auto_indexing=None): # Check for existing registration. if self.is_registered(model): raise RegistrationError( - '{} is already registered with Algolia engine'.format(model)) + "{} is already registered with Algolia engine".format(model) + ) # Perform the registration. if not issubclass(index_cls, AlgoliaIndex): raise RegistrationError( - '{} should be a subclass of AlgoliaIndex'.format(index_cls)) + "{} should be a subclass of AlgoliaIndex".format(index_cls) + ) index_obj = index_cls(model, self.client, self.__settings) self.__registered_models[model] = index_obj - if (isinstance(auto_indexing, bool) and - auto_indexing) or self.__auto_indexing: + if (isinstance(auto_indexing, bool) and auto_indexing) or self.__auto_indexing: # Connect to the signalling framework. post_save.connect(self.__post_save_receiver, model) pre_delete.connect(self.__pre_delete_receiver, model) - logger.info('REGISTER %s', model) + logger.info("REGISTER %s", model) def unregister(self, model): """ @@ -81,14 +81,15 @@ def unregister(self, model): """ if not self.is_registered(model): raise RegistrationError( - '{} is not registered with Algolia engine'.format(model)) + "{} is not registered with Algolia engine".format(model) + ) # Perform the unregistration. del self.__registered_models[model] # Disconnect from the signalling framework. post_save.disconnect(self.__post_save_receiver, model) pre_delete.disconnect(self.__pre_delete_receiver, model) - logger.info('UNREGISTER %s', model) + logger.info("UNREGISTER %s", model) def get_registered_models(self): """ @@ -101,7 +102,8 @@ def get_adapter(self, model): """Returns the adapter associated with the given model.""" if not self.is_registered(model): raise RegistrationError( - '{} is not registered with Algolia engine'.format(model)) + "{} is not registered with Algolia engine".format(model) + ) return self.__registered_models[model] @@ -145,7 +147,7 @@ def update_records(self, model, qs, batch_size=1000, **kwargs): adapter = self.get_adapter(model) adapter.update_records(qs, batch_size=batch_size, **kwargs) - def raw_search(self, model, query='', params=None): + def raw_search(self, model, query="", params=None): """Performs a search query and returns the parsed JSON.""" if params is None: params = {} @@ -183,12 +185,12 @@ def reset(self, settings=None): def __post_save_receiver(self, instance, **kwargs): """Signal handler for when a registered model has been saved.""" - logger.debug('RECEIVE post_save FOR %s', instance.__class__) + logger.debug("RECEIVE post_save FOR %s", instance.__class__) self.save_record(instance, **kwargs) def __pre_delete_receiver(self, instance, **kwargs): """Signal handler for when a registered model has been deleted.""" - logger.debug('RECEIVE pre_delete FOR %s', instance.__class__) + logger.debug("RECEIVE pre_delete FOR %s", instance.__class__) self.delete_record(instance) diff --git a/algoliasearch_django/settings.py b/algoliasearch_django/settings.py index abbaa61..354f292 100644 --- a/algoliasearch_django/settings.py +++ b/algoliasearch_django/settings.py @@ -1,4 +1,4 @@ from django.conf import settings SETTINGS = settings.ALGOLIA -DEBUG = SETTINGS.get('RAISE_EXCEPTIONS', settings.DEBUG) +DEBUG = SETTINGS.get("RAISE_EXCEPTIONS", settings.DEBUG) diff --git a/algoliasearch_django/version.py b/algoliasearch_django/version.py index aaa4264..189c03b 100644 --- a/algoliasearch_django/version.py +++ b/algoliasearch_django/version.py @@ -1 +1 @@ -VERSION = '3.0.0' +VERSION = "4.0.0" diff --git a/requirements.txt b/requirements.txt index cf996c8..c98c7fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,5 @@ tox twine factory_boy mock +ruff>=0.7.4,<1.0 +pyright>=1.1.389,<2.0 diff --git a/runtests.py b/runtests.py index 4318d94..3e3da0c 100755 --- a/runtests.py +++ b/runtests.py @@ -9,12 +9,13 @@ def main(): - os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' + os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() - failures = test_runner.run_tests(['tests']) + failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/setup.py b/setup.py index 051d80b..208ae50 100644 --- a/setup.py +++ b/setup.py @@ -10,16 +10,18 @@ # Allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) -path_readme = os.path.join(os.path.dirname(__file__), 'README.md') +path_readme = os.path.join(os.path.dirname(__file__), "README.md") try: import pypandoc - README = pypandoc.convert_file(path_readme, 'rst') + + README = pypandoc.convert_file(path_readme, "rst") except (IOError, ImportError): with open(path_readme) as readme: README = readme.read() -path_version = os.path.join(os.path.dirname(__file__), - 'algoliasearch_django/version.py') +path_version = os.path.join( + os.path.dirname(__file__), "algoliasearch_django/version.py" +) if sys.version_info < (3, 8): raise RuntimeError("algoliasearch_django 4.x requires Python 3.8+") else: @@ -27,32 +29,41 @@ setup( - name='algoliasearch-django', - version=VERSION, - license='MIT License', - packages=find_packages(exclude=['tests']), - install_requires=['django>=4.0', 'algoliasearch>=4.0,<5.0'], - description='Algolia Search integration for Django', + name="algoliasearch-django", + version="4.0.0", + license="MIT License", + packages=find_packages(exclude=["tests"]), + install_requires=["django>=4.0", "algoliasearch>=4.0,<5.0"], + description="Algolia Search integration for Django", long_description=README, - long_description_content_type='text/markdown', - author='Algolia Team', - author_email='support@algolia.com', - url='https://github.com/algolia/algoliasearch-django', - keywords=['algolia', 'pyalgolia', 'search', 'backend', 'hosted', 'cloud', - 'full-text search', 'faceted search', 'django'], + long_description_content_type="text/markdown", + author="Algolia Team", + author_email="support@algolia.com", + url="https://github.com/algolia/algoliasearch-django", + keywords=[ + "algolia", + "pyalgolia", + "search", + "backend", + "hosted", + "cloud", + "full-text search", + "faceted search", + "django", + ], classifiers=[ - 'Environment :: Web Environment', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Topic :: Internet :: WWW/HTTP', - ] + "Environment :: Web Environment", + "Framework :: Django", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Internet :: WWW/HTTP", + ], ) diff --git a/tests/factories.py b/tests/factories.py index ad237d7..395e833 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,37 +1,33 @@ import factory -from .models import ( - Example, - User, - Website -) +from .models import Example, User, Website class ExampleFactory(factory.django.DjangoModelFactory): uid = factory.Sequence(lambda n: n) - name = factory.Sequence(lambda n: 'Example name-{}'.format(n)) - address = factory.Sequence(lambda n: 'Example address-{}'.format(n)) - lat = factory.Faker('latitude') - lng = factory.Faker('longitude') + name = factory.Sequence(lambda n: "Example name-{}".format(n)) + address = factory.Sequence(lambda n: "Example address-{}".format(n)) + lat = factory.Faker("latitude") + lng = factory.Faker("longitude") class Meta: model = Example class UserFactory(factory.django.DjangoModelFactory): - name = factory.Sequence(lambda n: 'User name-{}'.format(n)) - username = factory.Sequence(lambda n: 'User username-{}'.format(n)) + name = factory.Sequence(lambda n: "User name-{}".format(n)) + username = factory.Sequence(lambda n: "User username-{}".format(n)) - _lat = factory.Faker('latitude') - _lng = factory.Faker('longitude') + _lat = factory.Faker("latitude") + _lng = factory.Faker("longitude") class Meta: model = User class WebsiteFactory(factory.django.DjangoModelFactory): - name = factory.Sequence(lambda n: 'Website name-{}'.format(n)) - url = factory.Faker('url') + name = factory.Sequence(lambda n: "Website name-{}".format(n)) + url = factory.Faker("url") class Meta: model = Website diff --git a/tests/models.py b/tests/models.py index 58e5723..f216d49 100644 --- a/tests/models.py +++ b/tests/models.py @@ -19,7 +19,7 @@ def location(self): return self._lat, self._lng def permissions(self): - return self._permissions.split(',') + return self._permissions.split(",") class Website(models.Model): @@ -70,8 +70,5 @@ def property_string(self): class BlogPost(models.Model): - author = models.ForeignKey( - User, - on_delete=models.CASCADE - ) + author = models.ForeignKey(User, on_delete=models.CASCADE) text = models.TextField(default="") diff --git a/tests/settings.py b/tests/settings.py index e1066c2..746243d 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -15,30 +15,30 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SECRET_KEY = 'MillisecondsMatter' +SECRET_KEY = "MillisecondsMatter" DEBUG = False # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'algoliasearch_django', - 'tests' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "algoliasearch_django", + "tests", ) MIDDLEWARE = [ - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.security.SecurityMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.auth.middleware.SessionAuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", ] TEMPLATES = [ @@ -57,32 +57,33 @@ }, ] -ROOT_URLCONF = 'tests.urls' +ROOT_URLCONF = "tests.urls" # Database DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } # Internationalization -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True def safe_index_name(name): - return '{}_ci-{}'.format(name, time.time()) + return "{}_ci-{}".format(name, time.time()) + # AlgoliaSearch settings ALGOLIA = { - 'APPLICATION_ID': os.getenv('ALGOLIA_APPLICATION_ID'), - 'API_KEY': os.getenv('ALGOLIA_API_KEY'), - 'INDEX_PREFIX': 'test', - 'INDEX_SUFFIX': safe_index_name('django'), - 'RAISE_EXCEPTIONS': True + "APPLICATION_ID": os.getenv("ALGOLIA_APPLICATION_ID"), + "API_KEY": os.getenv("ALGOLIA_API_KEY"), + "INDEX_PREFIX": "test", + "INDEX_SUFFIX": safe_index_name("django"), + "RAISE_EXCEPTIONS": True, } diff --git a/tests/test_commands.py b/tests/test_commands.py index 5b7e300..6f124a3 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -21,12 +21,14 @@ def tearDownClass(cls): def setUp(self): # Create some records - User.objects.create(name='James Bond', username="jb") - User.objects.create(name='Captain America', username="captain") - User.objects.create(name='John Snow', username="john_snow", - _lat=120.2, _lng=42.1) - User.objects.create(name='Steve Jobs', username="genius", - followers_count=331213) + User.objects.create(name="James Bond", username="jb") + User.objects.create(name="Captain America", username="captain") + User.objects.create( + name="John Snow", username="john_snow", _lat=120.2, _lng=42.1 + ) + User.objects.create( + name="Steve Jobs", username="genius", followers_count=331213 + ) self.out = StringIO() @@ -35,101 +37,96 @@ def tearDown(self): clear_index(User) def test_reindex(self): - call_command('algolia_reindex', stdout=self.out) + call_command("algolia_reindex", stdout=self.out) result = self.out.getvalue() - regex = r'Website --> 0' + regex = r"Website --> 0" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User --> 4' + regex = r"User --> 4" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) def test_reindex_with_args(self): - call_command('algolia_reindex', stdout=self.out, model=['Website']) + call_command("algolia_reindex", stdout=self.out, model=["Website"]) result = self.out.getvalue() - regex = r'Website --> \d+' + regex = r"Website --> \d+" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User --> \d+' + regex = r"User --> \d+" try: self.assertNotRegex(result, regex) except AttributeError: self.assertNotRegexpMatches(result, regex) def test_clearindex(self): - call_command('algolia_clearindex', stdout=self.out) + call_command("algolia_clearindex", stdout=self.out) result = self.out.getvalue() - regex = r'Website' + regex = r"Website" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User' + regex = r"User" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) def test_clearindex_with_args(self): - call_command( - 'algolia_clearindex', - stdout=self.out, - model=['Website'] - ) + call_command("algolia_clearindex", stdout=self.out, model=["Website"]) result = self.out.getvalue() - regex = r'Website' + regex = r"Website" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User' + regex = r"User" try: self.assertNotRegex(result, regex) except AttributeError: self.assertNotRegexpMatches(result, regex) def test_applysettings(self): - call_command('algolia_applysettings', stdout=self.out) + call_command("algolia_applysettings", stdout=self.out) result = self.out.getvalue() - regex = r'Website' + regex = r"Website" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User' + regex = r"User" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) def test_applysettings_with_args(self): - call_command('algolia_applysettings', stdout=self.out, - model=['Website']) + call_command("algolia_applysettings", stdout=self.out, model=["Website"]) result = self.out.getvalue() - regex = r'Website' + regex = r"Website" try: self.assertRegex(result, regex) except AttributeError: self.assertRegexpMatches(result, regex) - regex = r'User' + regex = r"User" try: self.assertNotRegex(result, regex) except AttributeError: diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 53a9e16..46de051 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -1,8 +1,4 @@ -from mock import ( - ANY, - call, - patch -) +from mock import ANY, call, patch from django.test import TestCase @@ -26,28 +22,31 @@ def non_decorated_operation(): WebsiteFactory() UserFactory() - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: decorated_operation() # The decorated method should have prevented the indexing operations mocked_save_record.assert_not_called() - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: non_decorated_operation() # The non-decorated method is not preventing the indexing operations # (the signal was correctly re-connected for both of the models) - mocked_save_record.assert_has_calls([ - call( - ANY, - created=True, - raw=False, - sender=ANY, - signal=ANY, - update_fields=None, - using=ANY - ) - ] * 2) + mocked_save_record.assert_has_calls( + [ + call( + ANY, + created=True, + raw=False, + sender=ANY, + signal=ANY, + update_fields=None, + using=ANY, + ) + ] + * 2 + ) def test_disable_auto_indexing_as_decorator_for_model(self): """Test that the `disable_auto_indexing` should work as a decorator for a specific model""" @@ -61,7 +60,7 @@ def non_decorated_operation(): WebsiteFactory() UserFactory() - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: decorated_operation() # The decorated method should have prevented the indexing operation for the `User` model @@ -73,36 +72,39 @@ def non_decorated_operation(): sender=ANY, signal=ANY, update_fields=None, - using=ANY + using=ANY, ) - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: non_decorated_operation() # The non-decorated method is not preventing the indexing operations # (the signal was correctly re-connected for both of the models) - mocked_save_record.assert_has_calls([ - call( - ANY, - created=True, - raw=False, - sender=ANY, - signal=ANY, - update_fields=None, - using=ANY - ) - ] * 2) + mocked_save_record.assert_has_calls( + [ + call( + ANY, + created=True, + raw=False, + sender=ANY, + signal=ANY, + update_fields=None, + using=ANY, + ) + ] + * 2 + ) def test_disable_auto_indexing_as_context_manager(self): """Test that the `disable_auto_indexing` should work as a context manager""" - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: with disable_auto_indexing(): WebsiteFactory() mocked_save_record.assert_not_called() - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: WebsiteFactory() mocked_save_record.assert_called_once() diff --git a/tests/test_engine.py b/tests/test_engine.py index 1d97a79..c2d6d15 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -26,8 +26,8 @@ def tearDown(self): def test_init_exception(self): algolia_settings = dict(settings.ALGOLIA) - del algolia_settings['APPLICATION_ID'] - del algolia_settings['API_KEY'] + del algolia_settings["APPLICATION_ID"] + del algolia_settings["API_KEY"] with self.settings(ALGOLIA=algolia_settings): with self.assertRaises(AlgoliaEngineError): @@ -36,10 +36,10 @@ def test_init_exception(self): def test_user_agent(self): user_agent = UserAgent.get() - parts = re.split('\s*;\s*', user_agent) + parts = re.split("\s*;\s*", user_agent) - self.assertIn('Django (%s)' % django_version(), parts) - self.assertIn('Algolia for Django (%s)' % VERSION, parts) + self.assertIn("Django (%s)" % django_version(), parts) + self.assertIn("Algolia for Django (%s)" % VERSION, parts) def test_auto_discover_indexes(self): """Test that the `index` module was auto-discovered and the models registered""" @@ -50,7 +50,7 @@ def test_auto_discover_indexes(self): User, # Registered using the `register` decorator Website, # Registered using the `register` method ], - algolia_engine.get_registered_models() + algolia_engine.get_registered_models(), ) def test_is_register(self): @@ -60,8 +60,7 @@ def test_is_register(self): def test_get_adapter(self): self.engine.register(Website) - self.assertEquals(AlgoliaIndex, - self.engine.get_adapter(Website).__class__) + self.assertEquals(AlgoliaIndex, self.engine.get_adapter(Website).__class__) def test_get_adapter_exception(self): with self.assertRaises(RegistrationError): @@ -71,8 +70,8 @@ def test_get_adapter_from_instance(self): self.engine.register(Website) instance = Website() self.assertEquals( - AlgoliaIndex, - self.engine.get_adapter_from_instance(instance).__class__) + AlgoliaIndex, self.engine.get_adapter_from_instance(instance).__class__ + ) def test_register(self): self.engine.register(Website) @@ -92,8 +91,9 @@ class WebsiteIndex(AlgoliaIndex): pass self.engine.register(Website, WebsiteIndex) - self.assertEqual(WebsiteIndex.__name__, - self.engine.get_adapter(Website).__class__.__name__) + self.assertEqual( + WebsiteIndex.__name__, self.engine.get_adapter(Website).__class__.__name__ + ) def test_register_with_custom_index_exception(self): class WebsiteIndex(object): diff --git a/tests/test_index.py b/tests/test_index.py index 07b5622..c5136a3 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -15,41 +15,44 @@ class IndexTestCase(TestCase): def setUp(self): self.client = algolia_engine.client - self.user = User(name='Algolia', username="algolia", - bio='Milliseconds matter', followers_count=42001, - following_count=42, _lat=123, _lng=-42.24, - _permissions='read,write,admin') + self.user = User( + name="Algolia", + username="algolia", + bio="Milliseconds matter", + followers_count=42001, + following_count=42, + _lat=123, + _lng=-42.24, + _permissions="read,write,admin", + ) self.contributor = User( - name='Contributor', + name="Contributor", username="contributor", - bio='Contributions matter', + bio="Contributions matter", followers_count=7, following_count=5, _lat=52.0705, _lng=-4.3007, - _permissions='contribute,follow' + _permissions="contribute,follow", ) - self.example = Example(uid=4, - name='SuperK', - address='Finland', - lat=63.3, - lng=-32.0, - is_admin=True) - self.example.category = ['Shop', 'Grocery'] + self.example = Example( + uid=4, name="SuperK", address="Finland", lat=63.3, lng=-32.0, is_admin=True + ) + self.example.category = ["Shop", "Grocery"] self.example.locations = [ - {'lat': 10.3, 'lng': -20.0}, - {'lat': 22.3, 'lng': 10.0}, + {"lat": 10.3, "lng": -20.0}, + {"lat": 22.3, "lng": 10.0}, ] def tearDown(self): - if hasattr(self, 'index'): + if hasattr(self, "index"): self.index.delete() def test_default_index_name(self): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - regex = r'^test_Website_django(_ci-\d+.\d+)?$' + regex = r"^test_Website_django(_ci-\d+.\d+)?$" try: self.assertRegex(self.index.index_name, regex) except AttributeError: @@ -57,10 +60,10 @@ def test_default_index_name(self): def test_custom_index_name(self): class WebsiteIndex(AlgoliaIndex): - index_name = 'customName' + index_name = "customName" self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) - regex = r'^test_customName_django(_ci-\d+.\d+)?$' + regex = r"^test_customName_django(_ci-\d+.\d+)?$" try: self.assertRegex(self.index.index_name, regex) except AttributeError: @@ -73,12 +76,12 @@ def test_index_model_with_foreign_key_reference(self): def test_index_name_settings(self): algolia_settings = dict(settings.ALGOLIA) - del algolia_settings['INDEX_PREFIX'] - del algolia_settings['INDEX_SUFFIX'] + del algolia_settings["INDEX_PREFIX"] + del algolia_settings["INDEX_SUFFIX"] with self.settings(ALGOLIA=algolia_settings): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - regex = r'^Website$' + regex = r"^Website$" try: self.assertRegex(self.index.index_name, regex) except AttributeError: @@ -90,56 +93,44 @@ def test_tmp_index_name(self): algolia_settings = dict(settings.ALGOLIA) # With no suffix nor prefix - del algolia_settings['INDEX_PREFIX'] - del algolia_settings['INDEX_SUFFIX'] + del algolia_settings["INDEX_PREFIX"] + del algolia_settings["INDEX_SUFFIX"] with self.settings(ALGOLIA=algolia_settings): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - self.assertEqual( - self.index.tmp_index_name, - 'Website_tmp' - ) + self.assertEqual(self.index.tmp_index_name, "Website_tmp") # With only a prefix - algolia_settings['INDEX_PREFIX'] = 'prefix' + algolia_settings["INDEX_PREFIX"] = "prefix" with self.settings(ALGOLIA=algolia_settings): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - self.assertEqual( - self.index.tmp_index_name, - 'prefix_Website_tmp' - ) + self.assertEqual(self.index.tmp_index_name, "prefix_Website_tmp") # With only a suffix - del algolia_settings['INDEX_PREFIX'] - algolia_settings['INDEX_SUFFIX'] = 'suffix' + del algolia_settings["INDEX_PREFIX"] + algolia_settings["INDEX_SUFFIX"] = "suffix" with self.settings(ALGOLIA=algolia_settings): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - self.assertEqual( - self.index.tmp_index_name, - 'Website_tmp_suffix' - ) + self.assertEqual(self.index.tmp_index_name, "Website_tmp_suffix") # With a prefix and a suffix - algolia_settings['INDEX_PREFIX'] = 'prefix' - algolia_settings['INDEX_SUFFIX'] = 'suffix' + algolia_settings["INDEX_PREFIX"] = "prefix" + algolia_settings["INDEX_SUFFIX"] = "suffix" with self.settings(ALGOLIA=algolia_settings): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) - self.assertEqual( - self.index.tmp_index_name, - 'prefix_Website_tmp_suffix' - ) + self.assertEqual(self.index.tmp_index_name, "prefix_Website_tmp_suffix") def test_reindex_with_replicas(self): self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): settings = { - 'replicas': [ - self.index.index_name + '_name_asc', - self.index.index_name + '_name_desc' + "replicas": [ + self.index.index_name + "_name_asc", + self.index.index_name + "_name_desc", ] } @@ -148,25 +139,25 @@ class WebsiteIndex(AlgoliaIndex): def test_reindex_with_should_index_boolean(self): Website.objects.create( - name='Algolia', - url='https://algolia.com', - is_online=True + name="Algolia", url="https://algolia.com", is_online=True ) self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): settings = { - 'replicas': [ - self.index.index_name + '_name_asc', - self.index.index_name + '_name_desc' + "replicas": [ + self.index.index_name + "_name_asc", + self.index.index_name + "_name_desc", ] } - should_index = 'is_online' + should_index = "is_online" self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - @unittest.skip(reason="FIXME: it's a known issue that reindex all might not work properly") + @unittest.skip( + reason="FIXME: it's a known issue that reindex all might not work properly" + ) def test_reindex_no_settings(self): self.maxDiff = None @@ -185,34 +176,57 @@ class WebsiteIndex(AlgoliaIndex): time.sleep(10) # FIXME: Refactor reindex_all to return taskID # Expect the former settings to be kept across reindex - self.assertEqual(self.index.get_settings(), existing_settings, - "An index whose model has no settings should keep its settings after reindex") + self.assertEqual( + self.index.get_settings(), + existing_settings, + "An index whose model has no settings should keep its settings after reindex", + ) - @unittest.skip(reason="FIXME: it's a known issue that reindex all might not work properly") + @unittest.skip( + reason="FIXME: it's a known issue that reindex all might not work properly" + ) def test_reindex_with_settings(self): import uuid + id = str(uuid.uuid4()) self.maxDiff = None - index_settings = {'searchableAttributes': ['name', 'email', 'company', 'city', 'county', 'account_names', - 'unordered(address)', 'state', 'zip_code', 'phone', 'fax', - 'unordered(web)'], 'attributesForFaceting': ['city', 'company'], - 'customRanking': ['desc(followers)'], - 'queryType': 'prefixAll', - 'highlightPreTag': '', - 'ranking': [ - 'asc(name)', - 'typo', - 'geo', - 'words', - 'filters', - 'proximity', - 'attribute', - 'exact', - 'custom' - ], - 'replicas': ['WebsiteIndexReplica_' + id + '_name_asc', - 'WebsiteIndexReplica_' + id + '_name_desc'], - 'highlightPostTag': '', 'hitsPerPage': 15} + index_settings = { + "searchableAttributes": [ + "name", + "email", + "company", + "city", + "county", + "account_names", + "unordered(address)", + "state", + "zip_code", + "phone", + "fax", + "unordered(web)", + ], + "attributesForFaceting": ["city", "company"], + "customRanking": ["desc(followers)"], + "queryType": "prefixAll", + "highlightPreTag": "", + "ranking": [ + "asc(name)", + "typo", + "geo", + "words", + "filters", + "proximity", + "attribute", + "exact", + "custom", + ], + "replicas": [ + "WebsiteIndexReplica_" + id + "_name_asc", + "WebsiteIndexReplica_" + id + "_name_desc", + ], + "highlightPostTag": "", + "hitsPerPage": 15, + } # Given an existing index defined with settings class WebsiteIndex(AlgoliaIndex): @@ -236,27 +250,22 @@ class WebsiteIndex(AlgoliaIndex): former_settings["hitsPerPage"] = 15 self.assertDictEqual(self.index.get_settings(), former_settings) - @unittest.skip(reason="FIXME: it's a known issue that reindex all might not work properly") + @unittest.skip( + reason="FIXME: it's a known issue that reindex all might not work properly" + ) def test_reindex_with_rules(self): # Given an existing index defined with settings class WebsiteIndex(AlgoliaIndex): - settings = {'hitsPerPage': 42} + settings = {"hitsPerPage": 42} self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) underlying_index = self.index._AlgoliaIndex__index # Given some existing query rules on the index rule = { - 'objectID': 'my-rule', - 'condition': { - 'pattern': 'some text', - 'anchoring': 'is' - }, - 'consequence': { - 'params': { - 'query': 'other text' - } - } + "objectID": "my-rule", + "condition": {"pattern": "some text", "anchoring": "is"}, + "consequence": {"params": {"query": "other text"}}, } underlying_index.save_rule(rule).wait() @@ -277,17 +286,24 @@ def remove_metadata(rule): self.assertEqual(len(rules), 1, "There should only be one rule") self.assertIn(rule, rules, "The existing rule should be kept over reindex") - @unittest.skip(reason="FIXME: it's a known issue that reindex all might not work properly") + @unittest.skip( + reason="FIXME: it's a known issue that reindex all might not work properly" + ) def test_reindex_with_synonyms(self): # Given an existing index defined with settings class WebsiteIndex(AlgoliaIndex): - settings = {'hitsPerPage': 42} + settings = {"hitsPerPage": 42} self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) underlying_index = self.index._AlgoliaIndex__index # Given some existing synonyms on the index - synonym = {'objectID': 'street', 'type': 'altCorrection1', 'word': 'Street', 'corrections': ['St']} + synonym = { + "objectID": "street", + "type": "altCorrection1", + "word": "Street", + "corrections": ["St"], + } underlying_index.save_synonyms([synonym]).wait() # When reindexing with no settings on the instance @@ -298,7 +314,9 @@ class WebsiteIndex(AlgoliaIndex): # Expect the synonyms to be kept across reindex synonyms = [s for s in underlying_index.browse_synonyms()] self.assertEqual(len(synonyms), 1, "There should only be one synonym") - self.assertIn(synonym, synonyms, "The existing synonym should be kept over reindex") + self.assertIn( + synonym, synonyms, "The existing synonym should be kept over reindex" + ) def apply_some_settings(self, index): """ @@ -308,358 +326,392 @@ def apply_some_settings(self, index): :return: the new settings """ # When reindexing with settings on the instance - old_hpp = index.settings['hitsPerPage'] if 'hitsPerPage' in index.settings else None - index.settings['hitsPerPage'] = 42 + old_hpp = ( + index.settings["hitsPerPage"] if "hitsPerPage" in index.settings else None + ) + index.settings["hitsPerPage"] = 42 index.reindex_all() - index.settings['hitsPerPage'] = old_hpp + index.settings["hitsPerPage"] = old_hpp time.sleep(10) # FIXME: Refactor reindex_all to return taskID index_settings = index.get_settings() # Expect the instance's settings to be applied at reindex - self.assertEqual(index_settings['hitsPerPage'], 42, - "An index whose model has settings should apply those at reindex") + self.assertEqual( + index_settings["hitsPerPage"], + 42, + "An index whose model has settings should apply those at reindex", + ) return index_settings def test_custom_objectID(self): class UserIndex(AlgoliaIndex): - custom_objectID = 'username' + custom_objectID = "username" self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertEqual(obj['objectID'], 'algolia') + self.assertEqual(obj["objectID"], "algolia") def test_custom_objectID_property(self): class UserIndex(AlgoliaIndex): - custom_objectID = 'reverse_username' + custom_objectID = "reverse_username" self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertEqual(obj['objectID'], 'ailogla') + self.assertEqual(obj["objectID"], "ailogla") def test_invalid_custom_objectID(self): class UserIndex(AlgoliaIndex): - custom_objectID = 'uid' + custom_objectID = "uid" with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_geo_fields(self): class UserIndex(AlgoliaIndex): - geo_field = 'location' + geo_field = "location" self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertEqual(obj['_geoloc'], {'lat': 123, 'lng': -42.24}) + self.assertEqual(obj["_geoloc"], {"lat": 123, "lng": -42.24}) def test_several_geo_fields(self): class ExampleIndex(AlgoliaIndex): - geo_field = 'geolocations' + geo_field = "geolocations" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.example) - self.assertEqual(obj['_geoloc'], [ - {'lat': 10.3, 'lng': -20.0}, - {'lat': 22.3, 'lng': 10.0}, - ]) + self.assertEqual( + obj["_geoloc"], + [ + {"lat": 10.3, "lng": -20.0}, + {"lat": 22.3, "lng": 10.0}, + ], + ) def test_geo_fields_already_formatted(self): class ExampleIndex(AlgoliaIndex): - geo_field = 'geolocations' + geo_field = "geolocations" - self.example.locations = {'lat': 10.3, 'lng': -20.0} + self.example.locations = {"lat": 10.3, "lng": -20.0} self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.example) - self.assertEqual(obj['_geoloc'], {'lat': 10.3, 'lng': -20.0}) + self.assertEqual(obj["_geoloc"], {"lat": 10.3, "lng": -20.0}) def test_none_geo_fields(self): class ExampleIndex(AlgoliaIndex): - geo_field = 'location' + geo_field = "location" Example.location = lambda x: None self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.example) - self.assertIsNone(obj.get('_geoloc')) + self.assertIsNone(obj.get("_geoloc")) def test_invalid_geo_fields(self): class UserIndex(AlgoliaIndex): - geo_field = 'position' + geo_field = "position" with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_tags(self): class UserIndex(AlgoliaIndex): - tags = 'permissions' + tags = "permissions" self.index = UserIndex(User, self.client, settings.ALGOLIA) # Test the users' tag individually obj = self.index.get_raw_record(self.user) - self.assertListEqual(obj['_tags'], ['read', 'write', 'admin']) + self.assertListEqual(obj["_tags"], ["read", "write", "admin"]) obj = self.index.get_raw_record(self.contributor) - self.assertListEqual(obj['_tags'], ['contribute', 'follow']) + self.assertListEqual(obj["_tags"], ["contribute", "follow"]) def test_invalid_tags(self): class UserIndex(AlgoliaIndex): - tags = 'tags' + tags = "tags" with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_one_field(self): class UserIndex(AlgoliaIndex): - fields = 'name' + fields = "name" self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertIn('name', obj) - self.assertNotIn('username', obj) - self.assertNotIn('bio', obj) - self.assertNotIn('followers_count', obj) - self.assertNotIn('following_count', obj) - self.assertNotIn('_lat', obj) - self.assertNotIn('_lng', obj) - self.assertNotIn('_permissions', obj) - self.assertNotIn('location', obj) - self.assertNotIn('_geoloc', obj) - self.assertNotIn('permissions', obj) - self.assertNotIn('_tags', obj) + self.assertIn("name", obj) + self.assertNotIn("username", obj) + self.assertNotIn("bio", obj) + self.assertNotIn("followers_count", obj) + self.assertNotIn("following_count", obj) + self.assertNotIn("_lat", obj) + self.assertNotIn("_lng", obj) + self.assertNotIn("_permissions", obj) + self.assertNotIn("location", obj) + self.assertNotIn("_geoloc", obj) + self.assertNotIn("permissions", obj) + self.assertNotIn("_tags", obj) self.assertEqual(len(obj), 2) def test_multiple_fields(self): class UserIndex(AlgoliaIndex): - fields = ('name', 'username', 'bio') + fields = ("name", "username", "bio") self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertIn('name', obj) - self.assertIn('username', obj) - self.assertIn('bio', obj) - self.assertNotIn('followers_count', obj) - self.assertNotIn('following_count', obj) - self.assertNotIn('_lat', obj) - self.assertNotIn('_lng', obj) - self.assertNotIn('_permissions', obj) - self.assertNotIn('location', obj) - self.assertNotIn('_geoloc', obj) - self.assertNotIn('permissions', obj) - self.assertNotIn('_tags', obj) + self.assertIn("name", obj) + self.assertIn("username", obj) + self.assertIn("bio", obj) + self.assertNotIn("followers_count", obj) + self.assertNotIn("following_count", obj) + self.assertNotIn("_lat", obj) + self.assertNotIn("_lng", obj) + self.assertNotIn("_permissions", obj) + self.assertNotIn("location", obj) + self.assertNotIn("_geoloc", obj) + self.assertNotIn("permissions", obj) + self.assertNotIn("_tags", obj) self.assertEqual(len(obj), 4) def test_fields_with_custom_name(self): # tuple syntax class UserIndex(AlgoliaIndex): - fields = ('name', ('username', 'login'), 'bio') + fields = ("name", ("username", "login"), "bio") self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertIn('name', obj) - self.assertNotIn('username', obj) - self.assertIn('login', obj) - self.assertEqual(obj['login'], 'algolia') - self.assertIn('bio', obj) - self.assertNotIn('followers_count', obj) - self.assertNotIn('following_count', obj) - self.assertNotIn('_lat', obj) - self.assertNotIn('_lng', obj) - self.assertNotIn('_permissions', obj) - self.assertNotIn('location', obj) - self.assertNotIn('_geoloc', obj) - self.assertNotIn('permissions', obj) - self.assertNotIn('_tags', obj) + self.assertIn("name", obj) + self.assertNotIn("username", obj) + self.assertIn("login", obj) + self.assertEqual(obj["login"], "algolia") + self.assertIn("bio", obj) + self.assertNotIn("followers_count", obj) + self.assertNotIn("following_count", obj) + self.assertNotIn("_lat", obj) + self.assertNotIn("_lng", obj) + self.assertNotIn("_permissions", obj) + self.assertNotIn("location", obj) + self.assertNotIn("_geoloc", obj) + self.assertNotIn("permissions", obj) + self.assertNotIn("_tags", obj) self.assertEqual(len(obj), 4) # list syntax class UserIndex(AlgoliaIndex): - fields = ('name', ['username', 'login'], 'bio') + fields = ("name", ["username", "login"], "bio") self.index = UserIndex(User, self.client, settings.ALGOLIA) obj = self.index.get_raw_record(self.user) - self.assertIn('name', obj) - self.assertNotIn('username', obj) - self.assertIn('login', obj) - self.assertEqual(obj['login'], 'algolia') - self.assertIn('bio', obj) - self.assertNotIn('followers_count', obj) - self.assertNotIn('following_count', obj) - self.assertNotIn('_lat', obj) - self.assertNotIn('_lng', obj) - self.assertNotIn('_permissions', obj) - self.assertNotIn('location', obj) - self.assertNotIn('_geoloc', obj) - self.assertNotIn('permissions', obj) - self.assertNotIn('_tags', obj) + self.assertIn("name", obj) + self.assertNotIn("username", obj) + self.assertIn("login", obj) + self.assertEqual(obj["login"], "algolia") + self.assertIn("bio", obj) + self.assertNotIn("followers_count", obj) + self.assertNotIn("following_count", obj) + self.assertNotIn("_lat", obj) + self.assertNotIn("_lng", obj) + self.assertNotIn("_permissions", obj) + self.assertNotIn("location", obj) + self.assertNotIn("_geoloc", obj) + self.assertNotIn("permissions", obj) + self.assertNotIn("_tags", obj) self.assertEqual(len(obj), 4) def test_invalid_fields(self): class UserIndex(AlgoliaIndex): - fields = ('name', 'color') + fields = ("name", "color") with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_invalid_fields_syntax(self): class UserIndex(AlgoliaIndex): - fields = {'name': 'user_name'} + fields = {"name": "user_name"} with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_invalid_named_fields_syntax(self): class UserIndex(AlgoliaIndex): - fields = ('name', {'username': 'login'}) + fields = ("name", {"username": "login"}) with self.assertRaises(AlgoliaIndexError): UserIndex(User, self.client, settings.ALGOLIA) def test_get_raw_record_with_update_fields(self): class UserIndex(AlgoliaIndex): - fields = ('name', 'username', ['bio', 'description']) + fields = ("name", "username", ["bio", "description"]) self.index = UserIndex(User, self.client, settings.ALGOLIA) - obj = self.index.get_raw_record(self.user, - update_fields=('name', 'bio')) - self.assertIn('name', obj) - self.assertNotIn('username', obj) - self.assertNotIn('bio', obj) - self.assertIn('description', obj) - self.assertEqual(obj['description'], 'Milliseconds matter') - self.assertNotIn('followers_count', obj) - self.assertNotIn('following_count', obj) - self.assertNotIn('_lat', obj) - self.assertNotIn('_lng', obj) - self.assertNotIn('_permissions', obj) - self.assertNotIn('location', obj) - self.assertNotIn('_geoloc', obj) - self.assertNotIn('permissions', obj) - self.assertNotIn('_tags', obj) + obj = self.index.get_raw_record(self.user, update_fields=("name", "bio")) + self.assertIn("name", obj) + self.assertNotIn("username", obj) + self.assertNotIn("bio", obj) + self.assertIn("description", obj) + self.assertEqual(obj["description"], "Milliseconds matter") + self.assertNotIn("followers_count", obj) + self.assertNotIn("following_count", obj) + self.assertNotIn("_lat", obj) + self.assertNotIn("_lng", obj) + self.assertNotIn("_permissions", obj) + self.assertNotIn("location", obj) + self.assertNotIn("_geoloc", obj) + self.assertNotIn("permissions", obj) + self.assertNotIn("_tags", obj) self.assertEqual(len(obj), 3) def test_should_index_method(self): class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'has_name' + fields = "name" + should_index = "has_name" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertTrue(self.index._should_index(self.example), - "We should index an instance when should_index(instance) returns True") + self.assertTrue( + self.index._should_index(self.example), + "We should index an instance when should_index(instance) returns True", + ) instance_should_not = Example(name=None) - self.assertFalse(self.index._should_index(instance_should_not), - "We should not index an instance when should_index(instance) returns False") + self.assertFalse( + self.index._should_index(instance_should_not), + "We should not index an instance when should_index(instance) returns False", + ) def test_should_index_unbound(self): class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'static_should_index' + fields = "name" + should_index = "static_should_index" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertTrue(self.index._should_index(self.example), - "We should index an instance when should_index() returns True") + self.assertTrue( + self.index._should_index(self.example), + "We should index an instance when should_index() returns True", + ) class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'static_should_not_index' + fields = "name" + should_index = "static_should_not_index" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) instance_should_not = Example() - self.assertFalse(self.index._should_index(instance_should_not), - "We should not index an instance when should_index() returns False") + self.assertFalse( + self.index._should_index(instance_should_not), + "We should not index an instance when should_index() returns False", + ) def test_should_index_attr(self): class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'index_me' + fields = "name" + should_index = "index_me" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertTrue(self.index._should_index(self.example), - "We should index an instance when its should_index attr is True") + self.assertTrue( + self.index._should_index(self.example), + "We should index an instance when its should_index attr is True", + ) instance_should_not = Example() instance_should_not.index_me = False - self.assertFalse(self.index._should_index(instance_should_not), - "We should not index an instance when its should_index attr is False") + self.assertFalse( + self.index._should_index(instance_should_not), + "We should not index an instance when its should_index attr is False", + ) class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'category' + fields = "name" + should_index = "category" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index attr is not boolean"): + with self.assertRaises( + AlgoliaIndexError, + msg="We should raise when the should_index attr is not boolean", + ): self.index._should_index(self.example) def test_should_index_field(self): class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'is_admin' + fields = "name" + should_index = "is_admin" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertTrue(self.index._should_index(self.example), - "We should index an instance when its should_index field is True") + self.assertTrue( + self.index._should_index(self.example), + "We should index an instance when its should_index field is True", + ) instance_should_not = Example() instance_should_not.is_admin = False - self.assertFalse(self.index._should_index(instance_should_not), - "We should not index an instance when its should_index field is False") + self.assertFalse( + self.index._should_index(instance_should_not), + "We should not index an instance when its should_index field is False", + ) class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'name' + fields = "name" + should_index = "name" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index field is not boolean"): + with self.assertRaises( + AlgoliaIndexError, + msg="We should raise when the should_index field is not boolean", + ): self.index._should_index(self.example) def test_should_index_property(self): class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'property_should_index' + fields = "name" + should_index = "property_should_index" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertTrue(self.index._should_index(self.example), - "We should index an instance when its should_index property is True") + self.assertTrue( + self.index._should_index(self.example), + "We should index an instance when its should_index property is True", + ) class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'property_should_not_index' + fields = "name" + should_index = "property_should_not_index" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - self.assertFalse(self.index._should_index(self.example), - "We should not index an instance when its should_index property is False") + self.assertFalse( + self.index._should_index(self.example), + "We should not index an instance when its should_index property is False", + ) class ExampleIndex(AlgoliaIndex): - fields = 'name' - should_index = 'property_string' + fields = "name" + should_index = "property_string" self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) - with self.assertRaises(AlgoliaIndexError, msg="We should raise when the should_index property is not boolean"): + with self.assertRaises( + AlgoliaIndexError, + msg="We should raise when the should_index property is not boolean", + ): self.index._should_index(self.example) def test_save_record_should_index_boolean(self): website = Website.objects.create( - name='Algolia', - url='https://algolia.com', - is_online=True + name="Algolia", url="https://algolia.com", is_online=True ) self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): settings = { - 'replicas': [ - self.index.index_name + '_name_asc', - self.index.index_name + '_name_desc' + "replicas": [ + self.index.index_name + "_name_asc", + self.index.index_name + "_name_desc", ] } - should_index = 'is_online' + should_index = "is_online" self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.save_record(website) def test_cyrillic(self): class CyrillicIndex(AlgoliaIndex): - fields = ['bio', 'name'] + fields = ["bio", "name"] settings = { - 'searchableAttributes': ['name', 'bio'], + "searchableAttributes": ["name", "bio"], } index_name = "test_cyrillic" @@ -668,5 +720,7 @@ class CyrillicIndex(AlgoliaIndex): self.index = CyrillicIndex(User, self.client, settings.ALGOLIA) self.index.save_record(self.user).wait() result = self.index.raw_search("крупнейших") - self.assertEqual(result['nbHits'], 1, "Search should return one result") - self.assertEqual(result['hits'][0]['name'], 'Algolia', "The result should be self.user") + self.assertEqual(result["nbHits"], 1, "Search should return one result") + self.assertEqual( + result["hits"][0]["name"], "Algolia", "The result should be self.user" + ) diff --git a/tests/test_signal.py b/tests/test_signal.py index e576bd3..f09b659 100644 --- a/tests/test_signal.py +++ b/tests/test_signal.py @@ -1,7 +1,7 @@ import time from mock import patch, call, ANY -from django.test import TestCase, override_settings +from django.test import TestCase from algoliasearch_django import algolia_engine from algoliasearch_django import get_adapter @@ -14,7 +14,6 @@ class SignalTestCase(TestCase): - @classmethod def tearDownClass(cls): get_adapter(Website).delete() @@ -23,7 +22,7 @@ def tearDown(self): clear_index(Website) def test_save_signal(self): - with patch.object(algolia_engine, 'save_record') as mocked_save_record: + with patch.object(algolia_engine, "save_record") as mocked_save_record: websites = WebsiteFactory.create_batch(3) mocked_save_record.assert_has_calls( @@ -35,7 +34,7 @@ def test_save_signal(self): sender=ANY, signal=ANY, update_fields=None, - using=ANY + using=ANY, ) for website in websites ] @@ -44,31 +43,26 @@ def test_save_signal(self): def test_delete_signal(self): websites = WebsiteFactory.create_batch(3) - with patch.object(algolia_engine, 'delete_record') as mocked_delete_record: + with patch.object(algolia_engine, "delete_record") as mocked_delete_record: websites[0].delete() websites[1].delete() - mocked_delete_record.assert_has_calls( - [ - call(websites[0]), - call(websites[1]) - ] - ) + mocked_delete_record.assert_has_calls([call(websites[0]), call(websites[1])]) def test_update_records(self): - Website.objects.create(name='Algolia', url='https://www.algolia.com') - Website.objects.create(name='Google', url='https://www.google.com') - Website.objects.create(name='Facebook', url='https://www.facebook.com') - Website.objects.create(name='Facebook', url='https://www.facebook.fr') - Website.objects.create(name='Facebook', url='https://fb.com') + Website.objects.create(name="Algolia", url="https://www.algolia.com") + Website.objects.create(name="Google", url="https://www.google.com") + Website.objects.create(name="Facebook", url="https://www.facebook.com") + Website.objects.create(name="Facebook", url="https://www.facebook.fr") + Website.objects.create(name="Facebook", url="https://fb.com") - qs = Website.objects.filter(name='Facebook') - update_records(Website, qs, url='https://facebook.com') + qs = Website.objects.filter(name="Facebook") + update_records(Website, qs, url="https://facebook.com") time.sleep(10) - qs.update(url='https://facebook.com') + qs.update(url="https://facebook.com") time.sleep(10) - result = raw_search(Website, 'Facebook') - self.assertEqual(result['nbHits'], qs.count()) - for res, url in zip(result['hits'], qs.values_list('url', flat=True)): - self.assertEqual(res['url'], url) + result = raw_search(Website, "Facebook") + self.assertEqual(result["nbHits"], qs.count()) + for res, url in zip(result["hits"], qs.values_list("url", flat=True)): + self.assertEqual(res["url"], url) diff --git a/tox.ini b/tox.ini index cc5123d..ddc98c9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,9 +26,11 @@ commands = python runtests.py [versions] twine = >=5.1,<6.0 wheel = >=0.45,<1.0 +ruff = >=0.7.4,<1.0 +pyright = >=1.1.389,<2.0 [testenv:coverage] -basepython = python3.8 +basepython = python3.11 deps = coverage passenv = ALGOLIA* @@ -37,7 +39,7 @@ commands = coverage report [testenv:coveralls] -basepython = python3.8 +basepython = python3.11 deps = coverage coveralls @@ -49,7 +51,7 @@ commands = coveralls [testenv:release] -basepython = python3.8 +basepython = python3.11 deps = twine {[versions]twine} wheel {[versions]wheel} @@ -60,3 +62,13 @@ commands = python setup.py sdist bdist_wheel twine check dist/* twine upload -u {env:PYPI_USER} -p {env:PYPI_PASSWORD} --repository-url https://upload.pypi.org/legacy/ dist/* + +[testenv:lint] +basepython = python3.11 +deps = + ruff {[versions]ruff} + pyright {[versions]pyright} +commands = + pip3 freeze > requirements.txt + ruff check --fix --unsafe-fixes + ruff format . From 5907b3efdca6ff63aa6df9c6e0dbb2f281fa14cc Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 20 Nov 2024 16:57:04 +0100 Subject: [PATCH 07/35] chore: setup pyright --- algoliasearch_django/models.py | 2 +- tox.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 63e7cb3..e48da08 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -5,7 +5,7 @@ from itertools import chain import logging -from algoliasearch.exceptions import AlgoliaException +from algoliasearch.http.exceptions import AlgoliaException from django.db.models.query_utils import DeferredAttribute from .settings import DEBUG diff --git a/tox.ini b/tox.ini index ddc98c9..d835757 100644 --- a/tox.ini +++ b/tox.ini @@ -72,3 +72,4 @@ commands = pip3 freeze > requirements.txt ruff check --fix --unsafe-fixes ruff format . + pyright From 523958e563d4774b2313cebfe19ff6d5f9ee917b Mon Sep 17 00:00:00 2001 From: shortcuts Date: Thu, 21 Nov 2024 15:58:01 +0100 Subject: [PATCH 08/35] fix: use algoliasearch v4 syntax --- algoliasearch_django/decorators.py | 25 +----- algoliasearch_django/models.py | 115 +++++++++++++++++---------- algoliasearch_django/registration.py | 11 ++- tests/test_commands.py | 4 +- tests/test_engine.py | 12 +-- 5 files changed, 86 insertions(+), 81 deletions(-) diff --git a/algoliasearch_django/decorators.py b/algoliasearch_django/decorators.py index a85e268..a8056fb 100644 --- a/algoliasearch_django/decorators.py +++ b/algoliasearch_django/decorators.py @@ -1,9 +1,5 @@ -try: - # ContextDecorator was introduced in Python 3.2 - from contextlib import ContextDecorator -except ImportError: - ContextDecorator = None -from functools import WRAPPER_ASSIGNMENTS, wraps +from contextlib import ContextDecorator +from functools import WRAPPER_ASSIGNMENTS from django.db.models.signals import post_save, pre_delete @@ -19,23 +15,6 @@ def available_attrs(fn): return WRAPPER_ASSIGNMENTS -if ContextDecorator is None: - # ContextDecorator was introduced in Python 3.2 - # See https://docs.python.org/3/library/contextlib.html#contextlib.ContextDecorator - class ContextDecorator: - """ - A base class that enables a context manager to also be used as a decorator. - """ - - def __call__(self, func): - @wraps(func, assigned=available_attrs(func)) - def inner(*args, **kwargs): - with self: - return func(*args, **kwargs) - - return inner - - def register(model): """ Register the given model class and wrapped AlgoliaIndex class with the Algolia engine: diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index e48da08..38b62a0 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -6,6 +6,9 @@ import logging from algoliasearch.http.exceptions import AlgoliaException +from algoliasearch.search.models.operation_index_params import OperationIndexParams +from algoliasearch.search.models.operation_type import OperationType +from algoliasearch.search.models.search_params_object import SearchParamsObject from django.db.models.query_utils import DeferredAttribute from .settings import DEBUG @@ -71,7 +74,7 @@ class AlgoliaIndex(object): def __init__(self, model, client, settings): """Initializes the index.""" - self.__init_index(client, model, settings) + self.__init_index(model, settings) self.model = model self.__client = client @@ -170,7 +173,7 @@ def __init__(self, model, client, settings): ) ) - def __init_index(self, client, model, settings): + def __init_index(self, model, settings): if not self.index_name: self.index_name = model.__name__ @@ -189,9 +192,6 @@ def __init_index(self, client, model, settings): self.tmp_index_name = tmp_index_name - self.__index = client.init_index(self.index_name) - self.__tmp_index = client.init_index(self.tmp_index_name) - @staticmethod def _validate_geolocation(geolocation): """ @@ -315,10 +315,14 @@ def save_record(self, instance, update_fields=None, **kwargs): try: if update_fields: obj = self.get_raw_record(instance, update_fields=update_fields) - result = self.__index.partial_update_object(obj) + result = self.__client.partial_update_object( + self.index_name, obj.get("objectID"), obj + ) else: obj = self.get_raw_record(instance) - result = self.__index.save_object(obj) + result = self.__client.save_object( + self.index_name, obj.get("objectID"), obj + ) logger.info("SAVE %s FROM %s", obj["objectID"], self.model) return result except AlgoliaException as e: @@ -333,7 +337,7 @@ def delete_record(self, instance): """Deletes the record.""" objectID = self.objectID(instance) try: - self.__index.delete_object(objectID) + self.__client.delete_object(self.index_name, objectID) logger.info("DELETE %s FROM %s", objectID, self.model) except AlgoliaException as e: if DEBUG: @@ -369,19 +373,21 @@ def update_records(self, qs, batch_size=1000, **kwargs): batch.append(dict(tmp)) if len(batch) >= batch_size: - self.__index.partial_update_objects(batch) + self.__client.partial_update_objects(self.index_name, batch) batch = [] if len(batch) > 0: - self.__index.partial_update_objects(batch) + self.__client.partial_update_objects(self.index_name, batch) def raw_search(self, query="", params=None): """Performs a search query and returns the parsed JSON.""" if params is None: - params = {} + params = SearchParamsObject() + + params.query = query try: - return self.__index.search(query, params) + return self.__client.search_single_index(self.index_name, params) except AlgoliaException as e: if DEBUG: raise e @@ -392,7 +398,7 @@ def get_settings(self): """Returns the settings of the index.""" try: logger.info("GET SETTINGS ON %s", self.index_name) - return self.__index.get_settings() + return self.__client.get_settings(self.index_name) except AlgoliaException as e: if DEBUG: raise e @@ -405,7 +411,7 @@ def set_settings(self): return try: - self.__index.set_settings(self.settings) + self.__client.set_settings(self.index_name, self.settings) logger.info("APPLY SETTINGS ON %s", self.index_name) except AlgoliaException as e: if DEBUG: @@ -416,7 +422,7 @@ def set_settings(self): def clear_objects(self): """Clears all objects of an index.""" try: - self.__index.clear_objects() + self.__client.clear_objects(self.index_name) logger.info("CLEAR INDEX %s", self.index_name) except AlgoliaException as e: if DEBUG: @@ -430,7 +436,7 @@ def clear_index(self): def wait_task(self, task_id): try: - self.__index.wait_task(task_id) + self.__client.wait_for_task(self.index_name, task_id) logger.info("WAIT TASK %s", self.index_name) except AlgoliaException as e: if DEBUG: @@ -439,9 +445,9 @@ def wait_task(self, task_id): logger.warning("%s NOT WAIT: %s", self.model, e) def delete(self): - self.__index.delete() - if self.__tmp_index: - self.__tmp_index.delete() + self.__client.delete_index(self.index_name) + if self.tmp_index_name: + self.__client.delete_index(self.tmp_index_name) def reindex_all(self, batch_size=1000): """ @@ -469,6 +475,11 @@ def reindex_all(self, batch_size=1000): else: raise e # Unexpected error while getting settings try: + should_keep_replicas = False + should_keep_slaves = False + replicas = None + slaves = None + if self.settings: replicas = self.settings.get("replicas", None) slaves = self.settings.get("slaves", None) @@ -483,22 +494,31 @@ def reindex_all(self, batch_size=1000): self.settings["slaves"] = [] logger.debug("REMOVE SLAVES FROM SETTINGS") - self.__tmp_index.set_settings(self.settings).wait() + set_settings_response = self.__client.set_settings( + self.tmp_index_name, self.settings + ) + self.__client.wait_for_task( + self.tmp_index_name, set_settings_response.task_id + ) logger.debug("APPLY SETTINGS ON %s_tmp", self.index_name) + rules = [] - synonyms = [] - for r in self.__index.browse_rules(): - rules.append(r) - for s in self.__index.browse_synonyms(): - synonyms.append(s) + self.__client.browse_rules( + self.index_name, lambda _resp: rules.append(_resp) + ) if len(rules): logger.debug("Got rules for index %s: %s", self.index_name, rules) should_keep_rules = True + + synonyms = [] + self.__client.browse_synonyms( + self.index_name, lambda _resp: synonyms.append(_resp) + ) if len(synonyms): logger.debug("Got synonyms for index %s: %s", self.index_name, rules) should_keep_synonyms = True - self.__tmp_index.clear_objects() + self.__client.clear_objects(self.tmp_index_name) logger.debug("CLEAR INDEX %s_tmp", self.index_name) counts = 0 @@ -515,18 +535,23 @@ def reindex_all(self, batch_size=1000): batch.append(self.get_raw_record(instance)) if len(batch) >= batch_size: - self.__tmp_index.save_objects(batch) + self.__client.save_objects(self.tmp_index_name, batch, True) logger.info( - "SAVE %d OBJECTS TO %s_tmp", len(batch), self.index_name + "SAVE %d OBJECTS TO %s", len(batch), self.tmp_index_name ) batch = [] counts += 1 if len(batch) > 0: - self.__tmp_index.save_objects(batch) - logger.info("SAVE %d OBJECTS TO %s_tmp", len(batch), self.index_name) - - self.__client.move_index(self.tmp_index_name, self.index_name) - logger.info("MOVE INDEX %s_tmp TO %s", self.index_name, self.index_name) + self.__client.save_objects(self.tmp_index_name, batch, True) + logger.info("SAVE %d OBJECTS TO %s", len(batch), self.tmp_index_name) + + self.__client.operation_index( + self.tmp_index_name, + OperationIndexParams( + operation=OperationType.MOVE, destination=self.index_name + ), + ) + logger.info("MOVE INDEX %s TO %s", self.tmp_index_name, self.index_name) if self.settings: if should_keep_replicas: @@ -536,24 +561,30 @@ def reindex_all(self, batch_size=1000): self.settings["slaves"] = slaves logger.debug("RESTORE SLAVES") if should_keep_replicas or should_keep_slaves: - self.__index.set_settings(self.settings) + self.__client.set_settings(self.index_name, self.settings) if should_keep_rules: - response = self.__index.save_rules( - rules, {"forwardToReplicas": True} + save_rules_response = self.__client.save_rules( + self.index_name, rules, True + ) + self.__client.wait_for_task( + self.index_name, save_rules_response.task_id ) - response.wait() logger.info( - "Saved rules for index %s with response: {}".format(response), + "Saved rules for index %s with response: {}".format( + save_rules_response + ), self.index_name, ) if should_keep_synonyms: - response = self.__index.save_synonyms( - synonyms, {"forwardToReplicas": True} + save_synonyms_response = self.__client.save_synonyms( + self.index_name, synonyms, True + ) + self.__client.wait_for_task( + self.index_name, save_synonyms_response.task_id ) - response.wait() logger.info( "Saved synonyms for index %s with response: {}".format( - response + save_synonyms_response ), self.index_name, ) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index bd93ac9..9b1a79c 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -3,8 +3,7 @@ from django.db.models.signals import post_save from django.db.models.signals import pre_delete -from algoliasearch.search_client import SearchClient -from algoliasearch.user_agent import UserAgent +from algoliasearch.search.client import SearchClientSync from .models import AlgoliaIndex from .settings import SETTINGS @@ -13,9 +12,6 @@ logger = logging.getLogger(__name__) -UserAgent.add("Algolia for Django", VERSION) -UserAgent.add("Django", django_version()) - class AlgoliaEngineError(Exception): """Something went wrong with Algolia Engine.""" @@ -39,7 +35,10 @@ def __init__(self, settings=SETTINGS): self.__settings = settings self.__registered_models = {} - self.client = SearchClient.create(app_id, api_key) + self.client = SearchClientSync(app_id, api_key) + self.client._config.user_agent.add("Algolia for Django", VERSION).add( + "Django", django_version() + ) def is_registered(self, model): """Checks whether the given models is registered with Algolia engine""" diff --git a/tests/test_commands.py b/tests/test_commands.py index 6f124a3..964d642 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -16,8 +16,8 @@ def tearDownClass(cls): user_index_name = get_adapter(User).index_name website_index_name = get_adapter(Website).index_name - algolia_engine.client.init_index(user_index_name).delete() - algolia_engine.client.init_index(website_index_name).delete() + algolia_engine.client.delete_index(user_index_name) + algolia_engine.client.delete_index(website_index_name) def setUp(self): # Create some records diff --git a/tests/test_engine.py b/tests/test_engine.py index c2d6d15..0e417a5 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,10 +1,8 @@ import six -import re from django.conf import settings from django.test import TestCase -from algoliasearch.user_agent import UserAgent from django import get_version as django_version from algoliasearch_django.version import VERSION from algoliasearch_django import algolia_engine @@ -34,12 +32,10 @@ def test_init_exception(self): AlgoliaEngine(settings=settings.ALGOLIA) def test_user_agent(self): - user_agent = UserAgent.get() - - parts = re.split("\s*;\s*", user_agent) - - self.assertIn("Django (%s)" % django_version(), parts) - self.assertIn("Algolia for Django (%s)" % VERSION, parts) + self.assertIn( + "Algolia for Django (%s); Django (%s)" % VERSION % django_version(), + self.engine.client._config.user_agent.get(), + ) def test_auto_discover_indexes(self): """Test that the `index` module was auto-discovered and the models registered""" From f6dd4e7087f0775f858e3993b98df5de297d0b9c Mon Sep 17 00:00:00 2001 From: shortcuts Date: Thu, 21 Nov 2024 17:21:05 +0100 Subject: [PATCH 09/35] fix: some types --- algoliasearch_django/registration.py | 5 ++--- tests/models.py | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index 9b1a79c..026d331 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -36,9 +36,8 @@ def __init__(self, settings=SETTINGS): self.__registered_models = {} self.client = SearchClientSync(app_id, api_key) - self.client._config.user_agent.add("Algolia for Django", VERSION).add( - "Django", django_version() - ) + self.client.add_user_agent("Algolia for Django", VERSION) + self.client.add_user_agent("Django", django_version()) def is_registered(self, model): """Checks whether the given models is registered with Algolia engine""" diff --git a/tests/models.py b/tests/models.py index f216d49..679d4d7 100644 --- a/tests/models.py +++ b/tests/models.py @@ -5,10 +5,10 @@ class User(models.Model): name = models.CharField(max_length=30) username = models.CharField(max_length=30, unique=True) bio = models.CharField(max_length=140, blank=True) - followers_count = models.BigIntegerField(default=0) - following_count = models.BigIntegerField(default=0) - _lat = models.FloatField(default=0) - _lng = models.FloatField(default=0) + followers_count = models.BigIntegerField(0) + following_count = models.BigIntegerField(0) + _lat = models.FloatField(0) + _lng = models.FloatField(0) _permissions = models.CharField(max_length=30, blank=True) @property @@ -25,7 +25,7 @@ def permissions(self): class Website(models.Model): name = models.CharField(max_length=100) url = models.URLField() - is_online = models.BooleanField(default=False) + is_online = models.BooleanField(False) class Example(models.Model): @@ -34,7 +34,7 @@ class Example(models.Model): address = models.CharField(max_length=200) lat = models.FloatField() lng = models.FloatField() - is_admin = models.BooleanField(default=False) + is_admin = models.BooleanField(False) category = [] locations = [] index_me = True From 1e0054b4b4fa82537f1718463c90a72c615d2afc Mon Sep 17 00:00:00 2001 From: shortcuts Date: Fri, 22 Nov 2024 11:47:22 +0100 Subject: [PATCH 10/35] fix: remove user agent for now --- algoliasearch_django/registration.py | 6 ++---- tests/test_engine.py | 12 +++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index 026d331..5ac0ec0 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -7,8 +7,6 @@ from .models import AlgoliaIndex from .settings import SETTINGS -from .version import VERSION -from django import get_version as django_version logger = logging.getLogger(__name__) @@ -36,8 +34,8 @@ def __init__(self, settings=SETTINGS): self.__registered_models = {} self.client = SearchClientSync(app_id, api_key) - self.client.add_user_agent("Algolia for Django", VERSION) - self.client.add_user_agent("Django", django_version()) + # self.client.add_user_agent("Algolia for Django", VERSION) + # self.client.add_user_agent("Django", django_version()) def is_registered(self, model): """Checks whether the given models is registered with Algolia engine""" diff --git a/tests/test_engine.py b/tests/test_engine.py index 0e417a5..9a41b43 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -3,8 +3,6 @@ from django.conf import settings from django.test import TestCase -from django import get_version as django_version -from algoliasearch_django.version import VERSION from algoliasearch_django import algolia_engine from algoliasearch_django import AlgoliaIndex from algoliasearch_django import AlgoliaEngine @@ -31,11 +29,11 @@ def test_init_exception(self): with self.assertRaises(AlgoliaEngineError): AlgoliaEngine(settings=settings.ALGOLIA) - def test_user_agent(self): - self.assertIn( - "Algolia for Django (%s); Django (%s)" % VERSION % django_version(), - self.engine.client._config.user_agent.get(), - ) + # def test_user_agent(self): + # self.assertIn( + # "Algolia for Django (%s); Django (%s)" % VERSION % django_version(), + # self.engine.client._config.user_agent.get(), + # ) def test_auto_discover_indexes(self): """Test that the `index` module was auto-discovered and the models registered""" From d232e1a2141ba58cc276783b9e75a0ee973c0f81 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Fri, 22 Nov 2024 16:28:08 +0100 Subject: [PATCH 11/35] fix: some tests and types --- algoliasearch_django/models.py | 114 +++++++++++++++++---------------- tests/factories.py | 3 + tests/test_commands.py | 16 +++-- tests/test_index.py | 69 ++++++++++---------- tests/test_signal.py | 10 +-- 5 files changed, 111 insertions(+), 101 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 38b62a0..9e306bc 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -260,20 +260,23 @@ def _should_index(self, instance): def _should_really_index(self, instance): """Return True if according to should_index the object should be indexed.""" + if self.should_index is None: + raise AlgoliaIndexError("{} should be defined.".format(self.should_index)) + if self._should_index_is_method: is_method = inspect.ismethod(self.should_index) try: - count_args = len(inspect.signature(self.should_index).parameters) + count_args = len(inspect.signature(self.should_index).parameters) # pyright: ignore -- should_index_is_method except AttributeError: # noinspection PyDeprecation - count_args = len(inspect.getargspec(self.should_index).args) + count_args = len(inspect.getfullargspec(self.should_index).args) if is_method or count_args == 1: # bound method, call with instance - return self.should_index(instance) + return self.should_index(instance) # pyright: ignore -- should_index_is_method else: # unbound method, simply call without arguments - return self.should_index() + return self.should_index() # pyright: ignore -- should_index_is_method else: # property/attribute/Field, evaluate as bool attr_type = type(self.should_index) @@ -312,19 +315,19 @@ def save_record(self, instance, update_fields=None, **kwargs): self.delete_record(instance) return + obj = {} try: if update_fields: obj = self.get_raw_record(instance, update_fields=update_fields) - result = self.__client.partial_update_object( - self.index_name, obj.get("objectID"), obj + self.__client.partial_update_objects( + index_name=self.index_name, objects=[obj], wait_for_tasks=True ) else: obj = self.get_raw_record(instance) - result = self.__client.save_object( - self.index_name, obj.get("objectID"), obj + self.__client.save_objects( + index_name=self.index_name, objects=[obj], wait_for_tasks=True ) logger.info("SAVE %s FROM %s", obj["objectID"], self.model) - return result except AlgoliaException as e: if DEBUG: raise e @@ -337,7 +340,9 @@ def delete_record(self, instance): """Deletes the record.""" objectID = self.objectID(instance) try: - self.__client.delete_object(self.index_name, objectID) + self.__client.delete_objects( + index_name=self.index_name, object_ids=[objectID], wait_for_tasks=True + ) logger.info("DELETE %s FROM %s", objectID, self.model) except AlgoliaException as e: if DEBUG: @@ -373,11 +378,15 @@ def update_records(self, qs, batch_size=1000, **kwargs): batch.append(dict(tmp)) if len(batch) >= batch_size: - self.__client.partial_update_objects(self.index_name, batch) + self.__client.partial_update_objects( + index_name=self.index_name, objects=batch, wait_for_tasks=True + ) batch = [] if len(batch) > 0: - self.__client.partial_update_objects(self.index_name, batch) + self.__client.partial_update_objects( + index_name=self.index_name, objects=batch, wait_for_tasks=True + ) def raw_search(self, query="", params=None): """Performs a search query and returns the parsed JSON.""" @@ -387,7 +396,7 @@ def raw_search(self, query="", params=None): params.query = query try: - return self.__client.search_single_index(self.index_name, params) + return self.__client.search_single_index(self.index_name, params).to_dict() except AlgoliaException as e: if DEBUG: raise e @@ -398,7 +407,7 @@ def get_settings(self): """Returns the settings of the index.""" try: logger.info("GET SETTINGS ON %s", self.index_name) - return self.__client.get_settings(self.index_name) + return self.__client.get_settings(self.index_name).to_dict() except AlgoliaException as e: if DEBUG: raise e @@ -411,7 +420,8 @@ def set_settings(self): return try: - self.__client.set_settings(self.index_name, self.settings) + _resp = self.__client.set_settings(self.index_name, self.settings) + self.__client.wait_for_task(self.index_name, _resp.task_id) logger.info("APPLY SETTINGS ON %s", self.index_name) except AlgoliaException as e: if DEBUG: @@ -422,7 +432,8 @@ def set_settings(self): def clear_objects(self): """Clears all objects of an index.""" try: - self.__client.clear_objects(self.index_name) + _resp = self.__client.clear_objects(self.index_name) + self.__client.wait_for_task(self.index_name, _resp.task_id) logger.info("CLEAR INDEX %s", self.index_name) except AlgoliaException as e: if DEBUG: @@ -430,10 +441,6 @@ def clear_objects(self): else: logger.warning("%s NOT CLEARED: %s", self.model, e) - def clear_index(self): - # TODO: add deprecated warning - self.clear_objects() - def wait_task(self, task_id): try: self.__client.wait_for_task(self.index_name, task_id) @@ -445,9 +452,11 @@ def wait_task(self, task_id): logger.warning("%s NOT WAIT: %s", self.model, e) def delete(self): - self.__client.delete_index(self.index_name) + _resp = self.__client.delete_index(self.index_name) + self.__client.wait_for_task(self.index_name, _resp.task_id) if self.tmp_index_name: - self.__client.delete_index(self.tmp_index_name) + _resp = self.__client.delete_index(self.tmp_index_name) + self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) def reindex_all(self, batch_size=1000): """ @@ -494,17 +503,13 @@ def reindex_all(self, batch_size=1000): self.settings["slaves"] = [] logger.debug("REMOVE SLAVES FROM SETTINGS") - set_settings_response = self.__client.set_settings( - self.tmp_index_name, self.settings - ) - self.__client.wait_for_task( - self.tmp_index_name, set_settings_response.task_id - ) + _resp = self.__client.set_settings(self.tmp_index_name, self.settings) + self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) logger.debug("APPLY SETTINGS ON %s_tmp", self.index_name) rules = [] self.__client.browse_rules( - self.index_name, lambda _resp: rules.append(_resp) + self.index_name, lambda _resp: rules.extend(_resp.hits) ) if len(rules): logger.debug("Got rules for index %s: %s", self.index_name, rules) @@ -512,20 +517,21 @@ def reindex_all(self, batch_size=1000): synonyms = [] self.__client.browse_synonyms( - self.index_name, lambda _resp: synonyms.append(_resp) + self.index_name, lambda _resp: synonyms.extend(_resp.hits) ) if len(synonyms): logger.debug("Got synonyms for index %s: %s", self.index_name, rules) should_keep_synonyms = True - self.__client.clear_objects(self.tmp_index_name) - logger.debug("CLEAR INDEX %s_tmp", self.index_name) + _resp = self.__client.clear_objects(self.tmp_index_name) + self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) + logger.debug("CLEAR INDEX %s", self.tmp_index_name) counts = 0 batch = [] - if hasattr(self, "get_queryset"): - qs = self.get_queryset() + if hasattr(self, "get_queryset") and callable(self.get_queryset): # pyright: ignore + qs = self.get_queryset() # pyright: ignore else: qs = self.model.objects.all() @@ -535,22 +541,29 @@ def reindex_all(self, batch_size=1000): batch.append(self.get_raw_record(instance)) if len(batch) >= batch_size: - self.__client.save_objects(self.tmp_index_name, batch, True) + self.__client.save_objects( + index_name=self.tmp_index_name, + objects=batch, + wait_for_tasks=True, + ) logger.info( "SAVE %d OBJECTS TO %s", len(batch), self.tmp_index_name ) batch = [] counts += 1 if len(batch) > 0: - self.__client.save_objects(self.tmp_index_name, batch, True) + self.__client.save_objects( + index_name=self.tmp_index_name, objects=batch, wait_for_tasks=True + ) logger.info("SAVE %d OBJECTS TO %s", len(batch), self.tmp_index_name) - self.__client.operation_index( + _resp = self.__client.operation_index( self.tmp_index_name, OperationIndexParams( operation=OperationType.MOVE, destination=self.index_name ), ) + self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) logger.info("MOVE INDEX %s TO %s", self.tmp_index_name, self.index_name) if self.settings: @@ -561,31 +574,20 @@ def reindex_all(self, batch_size=1000): self.settings["slaves"] = slaves logger.debug("RESTORE SLAVES") if should_keep_replicas or should_keep_slaves: - self.__client.set_settings(self.index_name, self.settings) + _resp = self.__client.set_settings(self.index_name, self.settings) + self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) if should_keep_rules: - save_rules_response = self.__client.save_rules( - self.index_name, rules, True - ) - self.__client.wait_for_task( - self.index_name, save_rules_response.task_id - ) + _resp = self.__client.save_rules(self.index_name, rules, True) + self.__client.wait_for_task(self.index_name, _resp.task_id) logger.info( - "Saved rules for index %s with response: {}".format( - save_rules_response - ), + "Saved rules for index %s with response: {}".format(_resp), self.index_name, ) if should_keep_synonyms: - save_synonyms_response = self.__client.save_synonyms( - self.index_name, synonyms, True - ) - self.__client.wait_for_task( - self.index_name, save_synonyms_response.task_id - ) + _resp = self.__client.save_synonyms(self.index_name, synonyms, True) + self.__client.wait_for_task(self.index_name, _resp.task_id) logger.info( - "Saved synonyms for index %s with response: {}".format( - save_synonyms_response - ), + "Saved synonyms for index %s with response: {}".format(_resp), self.index_name, ) return counts diff --git a/tests/factories.py b/tests/factories.py index 395e833..3f79076 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -17,6 +17,8 @@ class Meta: class UserFactory(factory.django.DjangoModelFactory): name = factory.Sequence(lambda n: "User name-{}".format(n)) username = factory.Sequence(lambda n: "User username-{}".format(n)) + following_count = 0 + followers_count = 0 _lat = factory.Faker("latitude") _lng = factory.Faker("longitude") @@ -28,6 +30,7 @@ class Meta: class WebsiteFactory(factory.django.DjangoModelFactory): name = factory.Sequence(lambda n: "Website name-{}".format(n)) url = factory.Faker("url") + is_online = False class Meta: model = Website diff --git a/tests/test_commands.py b/tests/test_commands.py index 964d642..18da393 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -21,14 +21,16 @@ def tearDownClass(cls): def setUp(self): # Create some records - User.objects.create(name="James Bond", username="jb") - User.objects.create(name="Captain America", username="captain") - User.objects.create( - name="John Snow", username="john_snow", _lat=120.2, _lng=42.1 - ) - User.objects.create( - name="Steve Jobs", username="genius", followers_count=331213 + User(name="James Bond", username="jb", followers_count=0) + User(name="Captain America", username="captain", followers_count=0) + User( + name="John Snow", + username="john_snow", + _lat=120.2, + _lng=42.1, + followers_count=0, ) + User(name="Steve Jobs", username="genius", followers_count=331213) self.out = StringIO() diff --git a/tests/test_index.py b/tests/test_index.py index c5136a3..4f491ae 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -129,8 +129,8 @@ def test_reindex_with_replicas(self): class WebsiteIndex(AlgoliaIndex): settings = { "replicas": [ - self.index.index_name + "_name_asc", - self.index.index_name + "_name_desc", + self.index.index_name + "_name_asc", # pyright: ignore + self.index.index_name + "_name_desc", # pyright: ignore ] } @@ -138,16 +138,14 @@ class WebsiteIndex(AlgoliaIndex): self.index.reindex_all() def test_reindex_with_should_index_boolean(self): - Website.objects.create( - name="Algolia", url="https://algolia.com", is_online=True - ) + Website(name="Algolia", url="https://algolia.com", is_online=True) self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): settings = { "replicas": [ - self.index.index_name + "_name_asc", - self.index.index_name + "_name_desc", + self.index.index_name + "_name_asc", # pyright: ignore + self.index.index_name + "_name_desc", # pyright: ignore ] } should_index = "is_online" @@ -173,7 +171,6 @@ class WebsiteIndex(AlgoliaIndex): # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - time.sleep(10) # FIXME: Refactor reindex_all to return taskID # Expect the former settings to be kept across reindex self.assertEqual( @@ -243,7 +240,6 @@ class WebsiteIndex(AlgoliaIndex): # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - time.sleep(10) # FIXME: Refactor reindex_all to return taskID # Expect the settings to be reset to model definition over reindex former_settings = existing_settings @@ -259,7 +255,6 @@ class WebsiteIndex(AlgoliaIndex): settings = {"hitsPerPage": 42} self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) - underlying_index = self.index._AlgoliaIndex__index # Given some existing query rules on the index rule = { @@ -268,12 +263,11 @@ class WebsiteIndex(AlgoliaIndex): "consequence": {"params": {"query": "other text"}}, } - underlying_index.save_rule(rule).wait() + self.index.__client.save_rule(self.index.index_name, rule["objectID"], rule) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - time.sleep(10) # FIXME: Refactor reindex_all to return taskID # Expect the rules to be kept across reindex def remove_metadata(rule): @@ -281,7 +275,10 @@ def remove_metadata(rule): del copy["_metadata"] return copy - rules = [r for r in underlying_index.browse_rules()] + rules = [] + self.index.__client.browse_rules( + self.index.index_name, lambda _resp: rules.extend(_resp.hits) + ) rules = list(map(remove_metadata, rules)) self.assertEqual(len(rules), 1, "There should only be one rule") self.assertIn(rule, rules, "The existing rule should be kept over reindex") @@ -295,7 +292,6 @@ class WebsiteIndex(AlgoliaIndex): settings = {"hitsPerPage": 42} self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) - underlying_index = self.index._AlgoliaIndex__index # Given some existing synonyms on the index synonym = { @@ -304,15 +300,19 @@ class WebsiteIndex(AlgoliaIndex): "word": "Street", "corrections": ["St"], } - underlying_index.save_synonyms([synonym]).wait() + self.index.__client.save_synonyms( + self.index.index_name, synonym["objectID"], synonym + ) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - time.sleep(10) # FIXME: Refactor reindex_all to return taskID # Expect the synonyms to be kept across reindex - synonyms = [s for s in underlying_index.browse_synonyms()] + synonyms = [] + self.index.__client.browse_synonyms( + self.index.index_name, lambda _resp: synonyms.extend(_resp.hits) + ) self.assertEqual(len(synonyms), 1, "There should only be one synonym") self.assertIn( synonym, synonyms, "The existing synonym should be kept over reindex" @@ -658,31 +658,31 @@ class ExampleIndex(AlgoliaIndex): self.index._should_index(self.example) def test_should_index_property(self): - class ExampleIndex(AlgoliaIndex): + class ExampleIndex1(AlgoliaIndex): fields = "name" should_index = "property_should_index" - self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) + self.index = ExampleIndex1(Example, self.client, settings.ALGOLIA) self.assertTrue( self.index._should_index(self.example), "We should index an instance when its should_index property is True", ) - class ExampleIndex(AlgoliaIndex): + class ExampleIndex2(AlgoliaIndex): fields = "name" should_index = "property_should_not_index" - self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) + self.index = ExampleIndex2(Example, self.client, settings.ALGOLIA) self.assertFalse( self.index._should_index(self.example), "We should not index an instance when its should_index property is False", ) - class ExampleIndex(AlgoliaIndex): + class ExampleIndex3(AlgoliaIndex): fields = "name" should_index = "property_string" - self.index = ExampleIndex(Example, self.client, settings.ALGOLIA) + self.index = ExampleIndex3(Example, self.client, settings.ALGOLIA) with self.assertRaises( AlgoliaIndexError, msg="We should raise when the should_index property is not boolean", @@ -690,16 +690,16 @@ class ExampleIndex(AlgoliaIndex): self.index._should_index(self.example) def test_save_record_should_index_boolean(self): - website = Website.objects.create( - name="Algolia", url="https://algolia.com", is_online=True - ) + website = Website(name="Algolia", url="https://algolia.com", is_online=True) self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): + self.assertIsNotNone(self.index.index_name) + settings = { "replicas": [ - self.index.index_name + "_name_asc", - self.index.index_name + "_name_desc", + self.index.index_name + "_name_asc", # pyright: ignore + self.index.index_name + "_name_desc", # pyright: ignore ] } should_index = "is_online" @@ -718,9 +718,12 @@ class CyrillicIndex(AlgoliaIndex): self.user.bio = "крупнейших" self.user.save() self.index = CyrillicIndex(User, self.client, settings.ALGOLIA) - self.index.save_record(self.user).wait() + self.index.save_record(self.user) result = self.index.raw_search("крупнейших") - self.assertEqual(result["nbHits"], 1, "Search should return one result") - self.assertEqual( - result["hits"][0]["name"], "Algolia", "The result should be self.user" - ) + self.assertIsNotNone(result) + + if result is not None: + self.assertEqual(result["nbHits"], 1, "Search should return one result") + self.assertEqual( + result["hits"][0]["name"], "Algolia", "The result should be self.user" + ) diff --git a/tests/test_signal.py b/tests/test_signal.py index f09b659..ccd3364 100644 --- a/tests/test_signal.py +++ b/tests/test_signal.py @@ -50,11 +50,11 @@ def test_delete_signal(self): mocked_delete_record.assert_has_calls([call(websites[0]), call(websites[1])]) def test_update_records(self): - Website.objects.create(name="Algolia", url="https://www.algolia.com") - Website.objects.create(name="Google", url="https://www.google.com") - Website.objects.create(name="Facebook", url="https://www.facebook.com") - Website.objects.create(name="Facebook", url="https://www.facebook.fr") - Website.objects.create(name="Facebook", url="https://fb.com") + Website(name="Algolia", url="https://www.algolia.com", is_online=False) + Website(name="Google", url="https://www.google.com", is_online=False) + Website(name="Facebook", url="https://www.facebook.com", is_online=False) + Website(name="Facebook", url="https://www.facebook.fr", is_online=False) + Website(name="Facebook", url="https://fb.com", is_online=False) qs = Website.objects.filter(name="Facebook") update_records(Website, qs, url="https://facebook.com") From 2efcc03d9f978263e40b34d7dc31bb2b714ca9b2 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Mon, 25 Nov 2024 14:34:52 +0100 Subject: [PATCH 12/35] chore: remove deprecation notices --- README.md | 4 ++-- algoliasearch_django/__init__.py | 1 - algoliasearch_django/registration.py | 4 ---- tests/test_commands.py | 6 +++--- tests/test_index.py | 17 +---------------- tests/test_signal.py | 4 ++-- 6 files changed, 8 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 94a3625..e430ff3 100644 --- a/README.md +++ b/README.md @@ -338,9 +338,9 @@ class MyModelMetaIndex(AlgoliaIndex): for index in self.indices: index.set_settings() - def clear_index(self): + def clear_objects(self): for index in self.indices: - index.clear_index() + index.clear_objects() def save_record(self, instance, update_fields=None, **kwargs): for index in self.indices: diff --git a/algoliasearch_django/__init__.py b/algoliasearch_django/__init__.py index 08751ea..c4f1ef0 100644 --- a/algoliasearch_django/__init__.py +++ b/algoliasearch_django/__init__.py @@ -31,7 +31,6 @@ delete_record = algolia_engine.delete_record update_records = algolia_engine.update_records raw_search = algolia_engine.raw_search -clear_index = algolia_engine.clear_index # TODO: deprecate clear_objects = algolia_engine.clear_objects reindex_all = algolia_engine.reindex_all diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index 5ac0ec0..99e7d5b 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -156,10 +156,6 @@ def clear_objects(self, model): adapter = self.get_adapter(model) adapter.clear_objects() - def clear_index(self, model): - # TODO: add deprecatd warning - self.clear_objects(model) - def reindex_all(self, model, batch_size=1000): """ Reindex all the records. diff --git a/tests/test_commands.py b/tests/test_commands.py index 18da393..f975f5d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -4,7 +4,7 @@ from algoliasearch_django import algolia_engine from algoliasearch_django import get_adapter -from algoliasearch_django import clear_index +from algoliasearch_django import clear_objects from .models import Website from .models import User @@ -35,8 +35,8 @@ def setUp(self): self.out = StringIO() def tearDown(self): - clear_index(Website) - clear_index(User) + clear_objects(Website) + clear_objects(User) def test_reindex(self): call_command("algolia_reindex", stdout=self.out) diff --git a/tests/test_index.py b/tests/test_index.py index 4f491ae..fa5be02 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,9 +1,7 @@ # coding=utf-8 -import time from django.conf import settings from django.test import TestCase -import unittest from algoliasearch_django import AlgoliaIndex from algoliasearch_django import algolia_engine @@ -153,9 +151,6 @@ class WebsiteIndex(AlgoliaIndex): self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - @unittest.skip( - reason="FIXME: it's a known issue that reindex all might not work properly" - ) def test_reindex_no_settings(self): self.maxDiff = None @@ -179,9 +174,6 @@ class WebsiteIndex(AlgoliaIndex): "An index whose model has no settings should keep its settings after reindex", ) - @unittest.skip( - reason="FIXME: it's a known issue that reindex all might not work properly" - ) def test_reindex_with_settings(self): import uuid @@ -232,7 +224,7 @@ class WebsiteIndex(AlgoliaIndex): self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) # Given some existing query rules on the index - # index.__index.save_rule() # TODO: Check query rules are kept + self.index.__client.save_rule() # Given some existing settings on the index existing_settings = self.apply_some_settings(self.index) @@ -246,9 +238,6 @@ class WebsiteIndex(AlgoliaIndex): former_settings["hitsPerPage"] = 15 self.assertDictEqual(self.index.get_settings(), former_settings) - @unittest.skip( - reason="FIXME: it's a known issue that reindex all might not work properly" - ) def test_reindex_with_rules(self): # Given an existing index defined with settings class WebsiteIndex(AlgoliaIndex): @@ -283,9 +272,6 @@ def remove_metadata(rule): self.assertEqual(len(rules), 1, "There should only be one rule") self.assertIn(rule, rules, "The existing rule should be kept over reindex") - @unittest.skip( - reason="FIXME: it's a known issue that reindex all might not work properly" - ) def test_reindex_with_synonyms(self): # Given an existing index defined with settings class WebsiteIndex(AlgoliaIndex): @@ -332,7 +318,6 @@ def apply_some_settings(self, index): index.settings["hitsPerPage"] = 42 index.reindex_all() index.settings["hitsPerPage"] = old_hpp - time.sleep(10) # FIXME: Refactor reindex_all to return taskID index_settings = index.get_settings() # Expect the instance's settings to be applied at reindex self.assertEqual( diff --git a/tests/test_signal.py b/tests/test_signal.py index ccd3364..4ad9d02 100644 --- a/tests/test_signal.py +++ b/tests/test_signal.py @@ -6,7 +6,7 @@ from algoliasearch_django import algolia_engine from algoliasearch_django import get_adapter from algoliasearch_django import raw_search -from algoliasearch_django import clear_index +from algoliasearch_django import clear_objects from algoliasearch_django import update_records from .factories import WebsiteFactory @@ -19,7 +19,7 @@ def tearDownClass(cls): get_adapter(Website).delete() def tearDown(self): - clear_index(Website) + clear_objects(Website) def test_save_signal(self): with patch.object(algolia_engine, "save_record") as mocked_save_record: From f0db28dd25a0a6a6aa410ce74de83ac7a1760fdb Mon Sep 17 00:00:00 2001 From: shortcuts Date: Mon, 25 Nov 2024 15:36:03 +0100 Subject: [PATCH 13/35] fix: assertions --- algoliasearch_django/models.py | 5 +++-- tests/test_engine.py | 4 ++-- tests/test_index.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 9e306bc..4c9fc5b 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -324,6 +324,7 @@ def save_record(self, instance, update_fields=None, **kwargs): ) else: obj = self.get_raw_record(instance) + print(obj) self.__client.save_objects( index_name=self.index_name, objects=[obj], wait_for_tasks=True ) @@ -391,9 +392,9 @@ def update_records(self, qs, batch_size=1000, **kwargs): def raw_search(self, query="", params=None): """Performs a search query and returns the parsed JSON.""" if params is None: - params = SearchParamsObject() + params = SearchParamsObject().to_dict() - params.query = query + params["query"] = query try: return self.__client.search_single_index(self.index_name, params).to_dict() diff --git a/tests/test_engine.py b/tests/test_engine.py index 9a41b43..3963841 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -54,7 +54,7 @@ def test_is_register(self): def test_get_adapter(self): self.engine.register(Website) - self.assertEquals(AlgoliaIndex, self.engine.get_adapter(Website).__class__) + self.assertEqual(AlgoliaIndex, self.engine.get_adapter(Website).__class__) def test_get_adapter_exception(self): with self.assertRaises(RegistrationError): @@ -63,7 +63,7 @@ def test_get_adapter_exception(self): def test_get_adapter_from_instance(self): self.engine.register(Website) instance = Website() - self.assertEquals( + self.assertEqual( AlgoliaIndex, self.engine.get_adapter_from_instance(instance).__class__ ) diff --git a/tests/test_index.py b/tests/test_index.py index fa5be02..d7b24a9 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -252,7 +252,7 @@ class WebsiteIndex(AlgoliaIndex): "consequence": {"params": {"query": "other text"}}, } - self.index.__client.save_rule(self.index.index_name, rule["objectID"], rule) + self.client.save_rule(self.index.index_name, rule["objectID"], rule) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) From ace44b124e623107668222a2c3076742d3b953c4 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Mon, 25 Nov 2024 22:16:33 +0100 Subject: [PATCH 14/35] fix: django settings upgrade and some cleanup --- algoliasearch_django/models.py | 10 ++----- runtests.py | 6 ++-- tests/settings.py | 50 +++++++++++++++++++++++++--------- tests/test_index.py | 8 +++--- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 4c9fc5b..19f31cc 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -86,12 +86,9 @@ def __init__(self, model, client, settings): ): # Only set settings if the actual index class does not define some self.settings = {} - try: - all_model_fields = [ - f.name for f in model._meta.get_fields() if not f.is_relation - ] - except AttributeError: # get_fields requires Django >= 1.8 - all_model_fields = [f.name for f in model._meta.local_fields] + all_model_fields = [ + f.name for f in model._meta.get_fields() if not f.is_relation + ] if isinstance(self.fields, str): self.fields = (self.fields,) @@ -324,7 +321,6 @@ def save_record(self, instance, update_fields=None, **kwargs): ) else: obj = self.get_raw_record(instance) - print(obj) self.__client.save_objects( index_name=self.index_name, objects=[obj], wait_for_tasks=True ) diff --git a/runtests.py b/runtests.py index 3e3da0c..90dc364 100755 --- a/runtests.py +++ b/runtests.py @@ -12,8 +12,10 @@ def main(): os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" django.setup() TestRunner = get_runner(settings) - test_runner = TestRunner() - failures = test_runner.run_tests(["tests"]) + test_runner = TestRunner(failfase=True) + # kept here to run a single test + failures = test_runner.run_tests(["tests.test_index.IndexTestCase"]) + # failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) diff --git a/tests/settings.py b/tests/settings.py index 746243d..c967350 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,25 +1,36 @@ """ Django settings for core project. -Generated by 'django-admin startproject' using Django 1.8.2. +Generated by 'django-admin startproject' using Django 5.1.3. For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ +https://docs.djangoproject.com/en/5.1/topics/settings/ For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ +https://docs.djangoproject.com/en/5.1/ref/settings/ """ import os import time +from pathlib import Path +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "MillisecondsMatter" -DEBUG = False + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] # Application definition -INSTALLED_APPS = ( + +INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -28,19 +39,21 @@ "django.contrib.staticfiles", "algoliasearch_django", "tests", -) +] MIDDLEWARE = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.auth.middleware.SessionAuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", "django.middleware.security.SecurityMiddleware", ] +ROOT_URLCONF = "tests.urls" + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", @@ -57,23 +70,34 @@ }, ] -ROOT_URLCONF = "tests.urls" - # Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases + DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + "NAME": BASE_DIR / "db.sqlite3", } } # Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + LANGUAGE_CODE = "en-us" + TIME_ZONE = "UTC" + USE_I18N = True + USE_L10N = True + USE_TZ = True +# Default primary key field type +# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + def safe_index_name(name): return "{}_ci-{}".format(name, time.time()) diff --git a/tests/test_index.py b/tests/test_index.py index d7b24a9..e682e24 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -23,6 +23,7 @@ def setUp(self): _lng=-42.24, _permissions="read,write,admin", ) + self.website = Website(name="Algolia", url="https://algolia.com") self.contributor = User( name="Contributor", @@ -675,12 +676,10 @@ class ExampleIndex3(AlgoliaIndex): self.index._should_index(self.example) def test_save_record_should_index_boolean(self): - website = Website(name="Algolia", url="https://algolia.com", is_online=True) self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA) class WebsiteIndex(AlgoliaIndex): - self.assertIsNotNone(self.index.index_name) - + custom_objectID = "name" settings = { "replicas": [ self.index.index_name + "_name_asc", # pyright: ignore @@ -689,8 +688,9 @@ class WebsiteIndex(AlgoliaIndex): } should_index = "is_online" + self.website.is_online = True self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) - self.index.save_record(website) + self.index.save_record(self.website) def test_cyrillic(self): class CyrillicIndex(AlgoliaIndex): From 466d528a810c9bc442e1069085f832d1e16cfba1 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 11:32:08 +0100 Subject: [PATCH 15/35] fix: prepare for next python release --- algoliasearch_django/models.py | 14 ++---------- algoliasearch_django/registration.py | 4 ++-- runtests.py | 4 ++-- tests/test_engine.py | 13 ++++++----- tests/test_index.py | 34 ++++++++++++++++++++-------- 5 files changed, 38 insertions(+), 31 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 19f31cc..381f5fb 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -400,7 +400,7 @@ def raw_search(self, query="", params=None): else: logger.warning("ERROR DURING SEARCH ON %s: %s", self.index_name, e) - def get_settings(self): + def get_settings(self) -> dict | None: """Returns the settings of the index.""" try: logger.info("GET SETTINGS ON %s", self.index_name) @@ -482,23 +482,16 @@ def reindex_all(self, batch_size=1000): raise e # Unexpected error while getting settings try: should_keep_replicas = False - should_keep_slaves = False replicas = None - slaves = None if self.settings: replicas = self.settings.get("replicas", None) - slaves = self.settings.get("slaves", None) should_keep_replicas = replicas is not None - should_keep_slaves = slaves is not None if should_keep_replicas: self.settings["replicas"] = [] logger.debug("REMOVE REPLICAS FROM SETTINGS") - if should_keep_slaves: - self.settings["slaves"] = [] - logger.debug("REMOVE SLAVES FROM SETTINGS") _resp = self.__client.set_settings(self.tmp_index_name, self.settings) self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) @@ -567,10 +560,7 @@ def reindex_all(self, batch_size=1000): if should_keep_replicas: self.settings["replicas"] = replicas logger.debug("RESTORE REPLICAS") - if should_keep_slaves: - self.settings["slaves"] = slaves - logger.debug("RESTORE SLAVES") - if should_keep_replicas or should_keep_slaves: + if should_keep_replicas: _resp = self.__client.set_settings(self.index_name, self.settings) self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) if should_keep_rules: diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index 99e7d5b..c217565 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -34,8 +34,8 @@ def __init__(self, settings=SETTINGS): self.__registered_models = {} self.client = SearchClientSync(app_id, api_key) - # self.client.add_user_agent("Algolia for Django", VERSION) - # self.client.add_user_agent("Django", django_version()) + self.client.add_user_agent("Algolia for Django", VERSION) + self.client.add_user_agent("Django", django_version()) def is_registered(self, model): """Checks whether the given models is registered with Algolia engine""" diff --git a/runtests.py b/runtests.py index 90dc364..f05758f 100755 --- a/runtests.py +++ b/runtests.py @@ -14,8 +14,8 @@ def main(): TestRunner = get_runner(settings) test_runner = TestRunner(failfase=True) # kept here to run a single test - failures = test_runner.run_tests(["tests.test_index.IndexTestCase"]) - # failures = test_runner.run_tests(["tests"]) + # failures = test_runner.run_tests(["tests.test_index.IndexTestCase.test_reindex_with_synonyms"]) + failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) diff --git a/tests/test_engine.py b/tests/test_engine.py index 3963841..c67672e 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -1,9 +1,10 @@ import six +from django import __version__ as __django__version__ from django.conf import settings from django.test import TestCase -from algoliasearch_django import algolia_engine +from algoliasearch_django import algolia_engine, __version__ from algoliasearch_django import AlgoliaIndex from algoliasearch_django import AlgoliaEngine from algoliasearch_django.registration import AlgoliaEngineError @@ -29,11 +30,11 @@ def test_init_exception(self): with self.assertRaises(AlgoliaEngineError): AlgoliaEngine(settings=settings.ALGOLIA) - # def test_user_agent(self): - # self.assertIn( - # "Algolia for Django (%s); Django (%s)" % VERSION % django_version(), - # self.engine.client._config.user_agent.get(), - # ) + def test_user_agent(self): + self.assertIn( + "Algolia for Django (%s); Django (%s)" % __version__ % __django__version__, + self.engine.client._config.user_agent.get(), + ) def test_auto_discover_indexes(self): """Test that the `index` module was auto-discovered and the models registered""" diff --git a/tests/test_index.py b/tests/test_index.py index e682e24..91f5dd1 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -225,7 +225,7 @@ class WebsiteIndex(AlgoliaIndex): self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) # Given some existing query rules on the index - self.index.__client.save_rule() + # index.__index.save_rule() # TODO: Check query rules are kept # Given some existing settings on the index existing_settings = self.apply_some_settings(self.index) @@ -237,7 +237,13 @@ class WebsiteIndex(AlgoliaIndex): # Expect the settings to be reset to model definition over reindex former_settings = existing_settings former_settings["hitsPerPage"] = 15 - self.assertDictEqual(self.index.get_settings(), former_settings) + + new_settings = self.index.get_settings() + + self.assertIsNotNone(new_settings) + + if new_settings is not None: + self.assertDictEqual(new_settings, former_settings) def test_reindex_with_rules(self): # Given an existing index defined with settings @@ -253,6 +259,11 @@ class WebsiteIndex(AlgoliaIndex): "consequence": {"params": {"query": "other text"}}, } + self.assertIsNotNone(self.index.index_name) + + if self.index.index_name is None: + return + self.client.save_rule(self.index.index_name, rule["objectID"], rule) # When reindexing with no settings on the instance @@ -280,6 +291,11 @@ class WebsiteIndex(AlgoliaIndex): self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) + self.assertIsNotNone(self.index.index_name) + + if self.index.index_name is None: + return + # Given some existing synonyms on the index synonym = { "objectID": "street", @@ -287,25 +303,25 @@ class WebsiteIndex(AlgoliaIndex): "word": "Street", "corrections": ["St"], } - self.index.__client.save_synonyms( - self.index.index_name, synonym["objectID"], synonym - ) + save_synonyms_response = self.client.save_synonyms(self.index.index_name, synonym_hit=[synonym]) + self.client.wait_for_task(self.index.index_name, save_synonyms_response.task_id) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() # Expect the synonyms to be kept across reindex + resp = self.client.search_synonyms(index_name=self.index.index_name) + print(resp, self.index.index_name) + synonyms = [] - self.index.__client.browse_synonyms( - self.index.index_name, lambda _resp: synonyms.extend(_resp.hits) - ) + self.client.browse_synonyms(self.index.index_name, lambda _resp: print(_resp)) self.assertEqual(len(synonyms), 1, "There should only be one synonym") self.assertIn( synonym, synonyms, "The existing synonym should be kept over reindex" ) - def apply_some_settings(self, index): + def apply_some_settings(self, index) -> dict: """ Applies a sample setting to the index. From bf70da45ee972f16e1cbbf718cf37e293ab67156 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 11:33:42 +0100 Subject: [PATCH 16/35] chore: format --- algoliasearch_django/registration.py | 6 ++++-- tests/test_index.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index c217565..c0e8176 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals import logging +from django import __version__ as __django__version__ from django.db.models.signals import post_save from django.db.models.signals import pre_delete +from algoliasearch_django import __version__ from algoliasearch.search.client import SearchClientSync from .models import AlgoliaIndex @@ -34,8 +36,8 @@ def __init__(self, settings=SETTINGS): self.__registered_models = {} self.client = SearchClientSync(app_id, api_key) - self.client.add_user_agent("Algolia for Django", VERSION) - self.client.add_user_agent("Django", django_version()) + self.client.add_user_agent("Algolia for Django", __version__) + self.client.add_user_agent("Django", __django__version__) def is_registered(self, model): """Checks whether the given models is registered with Algolia engine""" diff --git a/tests/test_index.py b/tests/test_index.py index 91f5dd1..6dc5e80 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -303,7 +303,9 @@ class WebsiteIndex(AlgoliaIndex): "word": "Street", "corrections": ["St"], } - save_synonyms_response = self.client.save_synonyms(self.index.index_name, synonym_hit=[synonym]) + save_synonyms_response = self.client.save_synonyms( + self.index.index_name, synonym_hit=[synonym] + ) self.client.wait_for_task(self.index.index_name, save_synonyms_response.task_id) # When reindexing with no settings on the instance From 2cf07af2b5c5b4c9162fe0618515ec4f7780c972 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 12:03:58 +0100 Subject: [PATCH 17/35] chore: some types improvements --- algoliasearch_django/models.py | 59 ++++++++++++++++------------------ tox.ini | 2 +- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 381f5fb..0363c82 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -4,6 +4,7 @@ from functools import partial from itertools import chain import logging +from typing import Callable, Iterable, Optional from algoliasearch.http.exceptions import AlgoliaException from algoliasearch.search.models.operation_index_params import OperationIndexParams @@ -57,7 +58,7 @@ class AlgoliaIndex(object): tags = None # Use to specify the index to target on Algolia. - index_name = None + index_name: str | None = None # Use to specify the settings of the index. settings = None @@ -72,9 +73,27 @@ class AlgoliaIndex(object): # Name of the attribute to check on instances if should_index is not a callable _should_index_is_method = False + get_queryset: Optional[Callable[[], Iterable]] = None + def __init__(self, model, client, settings): """Initializes the index.""" - self.__init_index(model, settings) + if not self.index_name: + self.index_name = model.__name__ + + tmp_index_name = "{index_name}_tmp".format(index_name=self.index_name) + + if "INDEX_PREFIX" in settings: + self.index_name = settings["INDEX_PREFIX"] + "_" + self.index_name + tmp_index_name = "{index_prefix}_{tmp_index_name}".format( + tmp_index_name=tmp_index_name, index_prefix=settings["INDEX_PREFIX"] + ) + if "INDEX_SUFFIX" in settings: + self.index_name += "_" + settings["INDEX_SUFFIX"] + tmp_index_name = "{tmp_index_name}_{index_suffix}".format( + tmp_index_name=tmp_index_name, index_suffix=settings["INDEX_SUFFIX"] + ) + + self.tmp_index_name = tmp_index_name self.model = model self.__client = client @@ -170,25 +189,6 @@ def __init__(self, model, client, settings): ) ) - def __init_index(self, model, settings): - if not self.index_name: - self.index_name = model.__name__ - - tmp_index_name = "{index_name}_tmp".format(index_name=self.index_name) - - if "INDEX_PREFIX" in settings: - self.index_name = settings["INDEX_PREFIX"] + "_" + self.index_name - tmp_index_name = "{index_prefix}_{tmp_index_name}".format( - tmp_index_name=tmp_index_name, index_prefix=settings["INDEX_PREFIX"] - ) - if "INDEX_SUFFIX" in settings: - self.index_name += "_" + settings["INDEX_SUFFIX"] - tmp_index_name = "{tmp_index_name}_{index_suffix}".format( - tmp_index_name=tmp_index_name, index_suffix=settings["INDEX_SUFFIX"] - ) - - self.tmp_index_name = tmp_index_name - @staticmethod def _validate_geolocation(geolocation): """ @@ -239,7 +239,7 @@ def get_raw_record(self, instance, update_fields=None): if callable(self.tags): tmp["_tags"] = self.tags(instance) if not isinstance(tmp["_tags"], list): - tmp["_tags"] = list(tmp["_tags"]) + tmp["_tags"] = list(tmp["_tags"]) # pyright: ignore logger.debug("BUILD %s FROM %s", tmp["objectID"], self.model) return tmp @@ -374,12 +374,7 @@ def update_records(self, qs, batch_size=1000, **kwargs): tmp["objectID"] = elt batch.append(dict(tmp)) - if len(batch) >= batch_size: - self.__client.partial_update_objects( - index_name=self.index_name, objects=batch, wait_for_tasks=True - ) - batch = [] - + # TODO: pass batch_size to partial_update_objects if len(batch) > 0: self.__client.partial_update_objects( index_name=self.index_name, objects=batch, wait_for_tasks=True @@ -519,9 +514,10 @@ def reindex_all(self, batch_size=1000): counts = 0 batch = [] + qs = [] - if hasattr(self, "get_queryset") and callable(self.get_queryset): # pyright: ignore - qs = self.get_queryset() # pyright: ignore + if hasattr(self, "get_queryset") and callable(self.get_queryset): + qs = self.get_queryset() else: qs = self.model.objects.all() @@ -550,7 +546,8 @@ def reindex_all(self, batch_size=1000): _resp = self.__client.operation_index( self.tmp_index_name, OperationIndexParams( - operation=OperationType.MOVE, destination=self.index_name + operation=OperationType.MOVE, + destination=self.index_name, # pyright: ignore ), ) self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) diff --git a/tox.ini b/tox.ini index d835757..33fc455 100644 --- a/tox.ini +++ b/tox.ini @@ -72,4 +72,4 @@ commands = pip3 freeze > requirements.txt ruff check --fix --unsafe-fixes ruff format . - pyright + pyright algoliasearch_django From bc74b9d020747010a065257f1fdbbb1803545f76 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 13:15:25 +0100 Subject: [PATCH 18/35] fix: decorators --- algoliasearch_django/decorators.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/algoliasearch_django/decorators.py b/algoliasearch_django/decorators.py index a8056fb..d1a075c 100644 --- a/algoliasearch_django/decorators.py +++ b/algoliasearch_django/decorators.py @@ -53,22 +53,14 @@ def __init__(self, model=None): if model is not None: self.models = [model] else: - self.models = algolia_engine._AlgoliaEngine__registered_models + self.models = algolia_engine.get_registered_models() def __enter__(self): for model in self.models: - post_save.disconnect( - algolia_engine._AlgoliaEngine__post_save_receiver, sender=model - ) - pre_delete.disconnect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, sender=model - ) + post_save.disconnect(algolia_engine.__post_save_receiver, sender=model) + pre_delete.disconnect(algolia_engine.__pre_delete_receiver, sender=model) def __exit__(self, exc_type, exc_value, traceback): for model in self.models: - post_save.connect( - algolia_engine._AlgoliaEngine__post_save_receiver, sender=model - ) - pre_delete.connect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, sender=model - ) + post_save.connect(algolia_engine.__post_save_receiver, sender=model) + pre_delete.connect(algolia_engine.__pre_delete_receiver, sender=model) From 19f93b7a09cbfc50694367f067f91a0c1a70ae42 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 13:52:11 +0100 Subject: [PATCH 19/35] fix: requirements and install from source --- requirements.txt | 19 +++++++++++-------- tox.ini | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index c98c7fa..0f30fda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,14 @@ -django>=4.0 -algoliasearch>=4.0,<5.0 +Django>=4.0 +# algoliasearch>=4.0,<5.0 +algoliasearch @ git+https://github.com/algolia/algoliasearch-client-python@main # dev dependencies -pypandoc -wheel +factory_boy>=3.0,<4.0 +mock>=5.0,<6.0 +pypandoc>=1.0,<2.0 +pyright>=1.1.389,<2.0 +ruff>=0.7.4,<1.0 +setuptools>=75.0,<76.0 +six>=1.16,<2.0 tox twine -factory_boy -mock -ruff>=0.7.4,<1.0 -pyright>=1.1.389,<2.0 +wheel diff --git a/tox.ini b/tox.ini index 33fc455..9e7ddc5 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,7 @@ deps = ruff {[versions]ruff} pyright {[versions]pyright} commands = - pip3 freeze > requirements.txt + pip3 install -r requirements.txt ruff check --fix --unsafe-fixes ruff format . pyright algoliasearch_django From 3b5b25b3c84ff99cd84ca6eabf87607d38cf9941 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 13:58:02 +0100 Subject: [PATCH 20/35] fix: circular import --- algoliasearch_django/registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/algoliasearch_django/registration.py b/algoliasearch_django/registration.py index c0e8176..166a0bc 100644 --- a/algoliasearch_django/registration.py +++ b/algoliasearch_django/registration.py @@ -4,7 +4,7 @@ from django import __version__ as __django__version__ from django.db.models.signals import post_save from django.db.models.signals import pre_delete -from algoliasearch_django import __version__ +from algoliasearch_django.version import VERSION as __version__ from algoliasearch.search.client import SearchClientSync from .models import AlgoliaIndex From c85fce28d11818a259b815a42a5ae6d0557fe107 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 14:09:40 +0100 Subject: [PATCH 21/35] fix: pull from source until end of bfcm --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 208ae50..ff7cd0f 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ version="4.0.0", license="MIT License", packages=find_packages(exclude=["tests"]), - install_requires=["django>=4.0", "algoliasearch>=4.0,<5.0"], + install_requires=["django>=4.0"], description="Algolia Search integration for Django", long_description=README, long_description_content_type="text/markdown", From 8f1b5617d51e4ac3ebb185014c9fd33664da3c1c Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 14:22:27 +0100 Subject: [PATCH 22/35] fix: lint before tests --- tox.ini | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/tox.ini b/tox.ini index 9e7ddc5..d57176e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,12 +7,15 @@ envlist = {py310,py311,py312,py313}-django51 coverage skip_missing_interpreters = True +allowlist_externals = True [testenv] deps = six mock factory_boy + ruff {[versions]ruff} + pyright {[versions]pyright} py{38,313}: Faker>=5.0,<6.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 @@ -21,7 +24,12 @@ deps = django51: Django>=5.1,<5.2 passenv = ALGOLIA* -commands = python runtests.py +commands = + pip3 install -r requirements.txt + ruff check --fix --unsafe-fixes + ruff format . + pyright algoliasearch_django + python runtests.py [versions] twine = >=5.1,<6.0 @@ -62,14 +70,3 @@ commands = python setup.py sdist bdist_wheel twine check dist/* twine upload -u {env:PYPI_USER} -p {env:PYPI_PASSWORD} --repository-url https://upload.pypi.org/legacy/ dist/* - -[testenv:lint] -basepython = python3.11 -deps = - ruff {[versions]ruff} - pyright {[versions]pyright} -commands = - pip3 install -r requirements.txt - ruff check --fix --unsafe-fixes - ruff format . - pyright algoliasearch_django From 790dce6f4a2f3631ac4f555268f3b99f782f5a6e Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 14:33:48 +0100 Subject: [PATCH 23/35] chore: add timeout --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d44ae2..766d58a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,6 +89,7 @@ jobs: python-version: ${{ matrix.version }} - name: Install dependencies and run tests + timeout-minutes: 5 run: | python -m venv python-ci-run source python-ci-run/bin/activate From 893f3f4d928da5dc98c239496e7a926b8e87b17a Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 15:23:39 +0100 Subject: [PATCH 24/35] fix: types and --- .github/workflows/main.yml | 2 +- algoliasearch_django/models.py | 4 ++-- tox.ini | 15 ++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 766d58a..98d2624 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: python-version: ${{ matrix.version }} - name: Install dependencies and run tests - timeout-minutes: 5 + timeout-minutes: 3 run: | python -m venv python-ci-run source python-ci-run/bin/activate diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 0363c82..af4050c 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -58,7 +58,7 @@ class AlgoliaIndex(object): tags = None # Use to specify the index to target on Algolia. - index_name: str | None = None + index_name: Optional[str] = None # Use to specify the settings of the index. settings = None @@ -395,7 +395,7 @@ def raw_search(self, query="", params=None): else: logger.warning("ERROR DURING SEARCH ON %s: %s", self.index_name, e) - def get_settings(self) -> dict | None: + def get_settings(self) -> Optional[dict]: """Returns the settings of the index.""" try: logger.info("GET SETTINGS ON %s", self.index_name) diff --git a/tox.ini b/tox.ini index d57176e..83a3d63 100644 --- a/tox.ini +++ b/tox.ini @@ -14,8 +14,6 @@ deps = six mock factory_boy - ruff {[versions]ruff} - pyright {[versions]pyright} py{38,313}: Faker>=5.0,<6.0 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 @@ -26,9 +24,6 @@ passenv = ALGOLIA* commands = pip3 install -r requirements.txt - ruff check --fix --unsafe-fixes - ruff format . - pyright algoliasearch_django python runtests.py [versions] @@ -70,3 +65,13 @@ commands = python setup.py sdist bdist_wheel twine check dist/* twine upload -u {env:PYPI_USER} -p {env:PYPI_PASSWORD} --repository-url https://upload.pypi.org/legacy/ dist/* + +[testenv:lint] +deps = + ruff {[versions]ruff} + pyright {[versions]pyright} +commands = + pip3 install -r requirements.txt + ruff check --fix --unsafe-fixes + ruff format . + pyright algoliasearch_django From 999f1158aa19951011b98a45808e7d631aef6d1a Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 17:27:45 +0100 Subject: [PATCH 25/35] fix: more tests --- algoliasearch_django/decorators.py | 22 ++++++++++++++---- algoliasearch_django/models.py | 2 +- runtests.py | 6 ++++- tests/test_engine.py | 6 +++-- tests/test_index.py | 37 +++++++++++++++++------------- 5 files changed, 48 insertions(+), 25 deletions(-) diff --git a/algoliasearch_django/decorators.py b/algoliasearch_django/decorators.py index d1a075c..b5e422e 100644 --- a/algoliasearch_django/decorators.py +++ b/algoliasearch_django/decorators.py @@ -53,14 +53,26 @@ def __init__(self, model=None): if model is not None: self.models = [model] else: - self.models = algolia_engine.get_registered_models() + self.models = algolia_engine._AlgoliaEngine__registered_models # pyright: ignore def __enter__(self): for model in self.models: - post_save.disconnect(algolia_engine.__post_save_receiver, sender=model) - pre_delete.disconnect(algolia_engine.__pre_delete_receiver, sender=model) + post_save.disconnect( + algolia_engine._AlgoliaEngine__post_save_receiver, + sender=model, # pyright: ignore + ) + pre_delete.disconnect( + algolia_engine._AlgoliaEngine__pre_delete_receiver, + sender=model, # pyright: ignore + ) def __exit__(self, exc_type, exc_value, traceback): for model in self.models: - post_save.connect(algolia_engine.__post_save_receiver, sender=model) - pre_delete.connect(algolia_engine.__pre_delete_receiver, sender=model) + post_save.connect( + algolia_engine._AlgoliaEngine__post_save_receiver, + sender=model, # pyright: ignore + ) + pre_delete.connect( + algolia_engine._AlgoliaEngine__pre_delete_receiver, + sender=model, # pyright: ignore + ) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index af4050c..2d12355 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -559,7 +559,7 @@ def reindex_all(self, batch_size=1000): logger.debug("RESTORE REPLICAS") if should_keep_replicas: _resp = self.__client.set_settings(self.index_name, self.settings) - self.__client.wait_for_task(self.tmp_index_name, _resp.task_id) + self.__client.wait_for_task(self.index_name, _resp.task_id) if should_keep_rules: _resp = self.__client.save_rules(self.index_name, rules, True) self.__client.wait_for_task(self.index_name, _resp.task_id) diff --git a/runtests.py b/runtests.py index f05758f..a2de077 100755 --- a/runtests.py +++ b/runtests.py @@ -14,7 +14,11 @@ def main(): TestRunner = get_runner(settings) test_runner = TestRunner(failfase=True) # kept here to run a single test - # failures = test_runner.run_tests(["tests.test_index.IndexTestCase.test_reindex_with_synonyms"]) + # failures = test_runner.run_tests( + # [ + # "tests.test_index.IndexTestCase.test_reindex_with_rules" + # ] + # ) failures = test_runner.run_tests(["tests"]) sys.exit(bool(failures)) diff --git a/tests/test_engine.py b/tests/test_engine.py index c67672e..97be25c 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -32,8 +32,10 @@ def test_init_exception(self): def test_user_agent(self): self.assertIn( - "Algolia for Django (%s); Django (%s)" % __version__ % __django__version__, - self.engine.client._config.user_agent.get(), + "Algolia for Django ({}); Django ({})".format( + __version__, __django__version__ + ), + self.engine.client._config._user_agent.get(), ) def test_auto_discover_indexes(self): diff --git a/tests/test_index.py b/tests/test_index.py index 6dc5e80..684d0af 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -256,7 +256,7 @@ class WebsiteIndex(AlgoliaIndex): rule = { "objectID": "my-rule", "condition": {"pattern": "some text", "anchoring": "is"}, - "consequence": {"params": {"query": "other text"}}, + "consequence": {"params": {"hitsPerPage": 42}}, } self.assertIsNotNone(self.index.index_name) @@ -264,25 +264,30 @@ class WebsiteIndex(AlgoliaIndex): if self.index.index_name is None: return - self.client.save_rule(self.index.index_name, rule["objectID"], rule) + self.client.save_rule_with_http_info( + self.index.index_name, rule["objectID"], rule + ) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) self.index.reindex_all() - # Expect the rules to be kept across reindex - def remove_metadata(rule): - copy = dict(rule) - del copy["_metadata"] - return copy - rules = [] - self.index.__client.browse_rules( - self.index.index_name, lambda _resp: rules.extend(_resp.hits) + self.client.browse_rules( + self.index.index_name, + lambda _resp: rules.extend([_hit.to_dict() for _hit in _resp.hits]), ) - rules = list(map(remove_metadata, rules)) self.assertEqual(len(rules), 1, "There should only be one rule") - self.assertIn(rule, rules, "The existing rule should be kept over reindex") + self.assertEqual( + rules[0]["consequence"], + rule["consequence"], + "The existing rule should be kept over reindex", + ) + self.assertEqual( + rules[0]["objectID"], + rule["objectID"], + "The existing rule should be kept over reindex", + ) def test_reindex_with_synonyms(self): # Given an existing index defined with settings @@ -313,11 +318,11 @@ class WebsiteIndex(AlgoliaIndex): self.index.reindex_all() # Expect the synonyms to be kept across reindex - resp = self.client.search_synonyms(index_name=self.index.index_name) - print(resp, self.index.index_name) - synonyms = [] - self.client.browse_synonyms(self.index.index_name, lambda _resp: print(_resp)) + self.client.browse_synonyms( + self.index.index_name, + lambda _resp: synonyms.extend([_hit.to_dict() for _hit in _resp.hits]), + ) self.assertEqual(len(synonyms), 1, "There should only be one synonym") self.assertIn( synonym, synonyms, "The existing synonym should be kept over reindex" From f7e12822f72492eb5330c6c0bb1090ef408a83d1 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 18:01:46 +0100 Subject: [PATCH 26/35] fix: commands and index tests --- runtests.py | 2 +- tests/test_commands.py | 18 ++++++++---------- tests/test_index.py | 5 ++--- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/runtests.py b/runtests.py index a2de077..8d97daa 100755 --- a/runtests.py +++ b/runtests.py @@ -12,7 +12,7 @@ def main(): os.environ["DJANGO_SETTINGS_MODULE"] = "tests.settings" django.setup() TestRunner = get_runner(settings) - test_runner = TestRunner(failfase=True) + test_runner = TestRunner(failfast=True) # kept here to run a single test # failures = test_runner.run_tests( # [ diff --git a/tests/test_commands.py b/tests/test_commands.py index f975f5d..373c605 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -21,16 +21,14 @@ def tearDownClass(cls): def setUp(self): # Create some records - User(name="James Bond", username="jb", followers_count=0) - User(name="Captain America", username="captain", followers_count=0) - User( - name="John Snow", - username="john_snow", - _lat=120.2, - _lng=42.1, - followers_count=0, - ) - User(name="Steve Jobs", username="genius", followers_count=331213) + u = User(name="James Bond", username="jb", followers_count=0, following_count=0, _lat=0, _lng=0) + u.save() + u = User(name="Captain America", username="captain", followers_count=0, following_count=0, _lat=0, _lng=0) + u.save() + u = User(name="John Snow", username="john_snow", _lat=120.2, _lng=42.1, followers_count=0, following_count=0) + u.save() + u = User(name="Steve Jobs", username="genius", followers_count=331213, following_count=0, _lat=0, _lng=0) + u.save() self.out = StringIO() diff --git a/tests/test_index.py b/tests/test_index.py index 684d0af..33fbad3 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -264,9 +264,8 @@ class WebsiteIndex(AlgoliaIndex): if self.index.index_name is None: return - self.client.save_rule_with_http_info( - self.index.index_name, rule["objectID"], rule - ) + _resp = self.client.save_rule(self.index.index_name, rule["objectID"], rule) + self.client.wait_for_task(self.index.index_name, _resp.task_id) # When reindexing with no settings on the instance self.index = WebsiteIndex(Website, self.client, settings.ALGOLIA) From be333a169bbbbc0992e6b702b90e7ef417e84770 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 22:21:40 +0100 Subject: [PATCH 27/35] trigger From 651654e1cd307338af8117ac8ac22623d3956bb1 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Tue, 26 Nov 2024 22:54:47 +0100 Subject: [PATCH 28/35] fix: lint and ci --- .github/workflows/main.yml | 3 +-- tests/test_commands.py | 36 ++++++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 98d2624..1af0c10 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,11 +89,10 @@ jobs: python-version: ${{ matrix.version }} - name: Install dependencies and run tests - timeout-minutes: 3 + timeout-minutes: 10 run: | python -m venv python-ci-run source python-ci-run/bin/activate pip3 install --upgrade pip pip3 install tox - pip3 install -r requirements.txt TOXENV=${{ matrix.toxenv }} ALGOLIA_APPLICATION_ID=${{ secrets.ALGOLIA_APPLICATION_ID }} ALGOLIA_API_KEY=${{ secrets.ALGOLIA_API_KEY }} tox diff --git a/tests/test_commands.py b/tests/test_commands.py index 373c605..7dd8097 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -21,13 +21,41 @@ def tearDownClass(cls): def setUp(self): # Create some records - u = User(name="James Bond", username="jb", followers_count=0, following_count=0, _lat=0, _lng=0) + u = User( + name="James Bond", + username="jb", + followers_count=0, + following_count=0, + _lat=0, + _lng=0, + ) u.save() - u = User(name="Captain America", username="captain", followers_count=0, following_count=0, _lat=0, _lng=0) + u = User( + name="Captain America", + username="captain", + followers_count=0, + following_count=0, + _lat=0, + _lng=0, + ) u.save() - u = User(name="John Snow", username="john_snow", _lat=120.2, _lng=42.1, followers_count=0, following_count=0) + u = User( + name="John Snow", + username="john_snow", + _lat=120.2, + _lng=42.1, + followers_count=0, + following_count=0, + ) u.save() - u = User(name="Steve Jobs", username="genius", followers_count=331213, following_count=0, _lat=0, _lng=0) + u = User( + name="Steve Jobs", + username="genius", + followers_count=331213, + following_count=0, + _lat=0, + _lng=0, + ) u.save() self.out = StringIO() From f4f601257301b5591d08c95220e29669ccacde9c Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 27 Nov 2024 10:36:08 +0100 Subject: [PATCH 29/35] trigger From 4a5d0542633c757d77ed51878c03ed21eef44c18 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 27 Nov 2024 11:21:23 +0100 Subject: [PATCH 30/35] chore: longer timeout? --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1af0c10..cb72981 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: python-version: ${{ matrix.version }} - name: Install dependencies and run tests - timeout-minutes: 10 + timeout-minutes: 20 run: | python -m venv python-ci-run source python-ci-run/bin/activate From d4b2085777f24910de716ec9361352885745e479 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 27 Nov 2024 11:40:54 +0100 Subject: [PATCH 31/35] chore: ignore at the correct spot --- algoliasearch_django/decorators.py | 16 ++++++++-------- setup.py | 1 + tox.ini | 7 +++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/algoliasearch_django/decorators.py b/algoliasearch_django/decorators.py index b5e422e..9b9adea 100644 --- a/algoliasearch_django/decorators.py +++ b/algoliasearch_django/decorators.py @@ -58,21 +58,21 @@ def __init__(self, model=None): def __enter__(self): for model in self.models: post_save.disconnect( - algolia_engine._AlgoliaEngine__post_save_receiver, - sender=model, # pyright: ignore + algolia_engine._AlgoliaEngine__post_save_receiver, # pyright: ignore + sender=model, ) pre_delete.disconnect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, - sender=model, # pyright: ignore + algolia_engine._AlgoliaEngine__pre_delete_receiver, # pyright: ignore + sender=model, ) def __exit__(self, exc_type, exc_value, traceback): for model in self.models: post_save.connect( - algolia_engine._AlgoliaEngine__post_save_receiver, - sender=model, # pyright: ignore + algolia_engine._AlgoliaEngine__post_save_receiver, # pyright: ignore + sender=model, ) pre_delete.connect( - algolia_engine._AlgoliaEngine__pre_delete_receiver, - sender=model, # pyright: ignore + algolia_engine._AlgoliaEngine__pre_delete_receiver, # pyright: ignore + sender=model, ) diff --git a/setup.py b/setup.py index ff7cd0f..69affeb 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", ], ) diff --git a/tox.ini b/tox.ini index 83a3d63..592e645 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = {py310,py311,py312,py313}-django51 coverage skip_missing_interpreters = True -allowlist_externals = True [testenv] deps = @@ -33,7 +32,7 @@ ruff = >=0.7.4,<1.0 pyright = >=1.1.389,<2.0 [testenv:coverage] -basepython = python3.11 +basepython = python3.13 deps = coverage passenv = ALGOLIA* @@ -42,7 +41,7 @@ commands = coverage report [testenv:coveralls] -basepython = python3.11 +basepython = python3.13 deps = coverage coveralls @@ -54,7 +53,7 @@ commands = coveralls [testenv:release] -basepython = python3.11 +basepython = python3.13 deps = twine {[versions]twine} wheel {[versions]wheel} From 2fb46fff2828c79989314ea682c50e089e9fcc81 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Wed, 27 Nov 2024 12:10:56 +0100 Subject: [PATCH 32/35] chore: issue template dir --- .github/{ISSUE_TEMPLATE.md => ISSUE_TEMPLATE/Bug_report.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{ISSUE_TEMPLATE.md => ISSUE_TEMPLATE/Bug_report.yml} (100%) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/Bug_report.yml similarity index 100% rename from .github/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE/Bug_report.yml From 5df5c84b7e82538941a503af93ff1fe5b569b4b7 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Thu, 28 Nov 2024 10:05:09 +0100 Subject: [PATCH 33/35] chore: review --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- MAINTAINERS.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3e0f401..6894e86 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ## 🧭 What and Why -🎟 JIRA Ticket: +🎟 Related Issue: ### Changes included: diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 0091440..ab0e2c7 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,5 +1,5 @@ ## `algolia/algoliasearch-django` maintainers -| Name | Email | -|-----------------|---------------------| -| Algolia | support@algolia.com | +| Name | Email | +|-----------------|------------------------| +| Algolia | https://alg.li/support | From 4a4031f57cda545faf603e9cf3c19c42498214fe Mon Sep 17 00:00:00 2001 From: shortcuts Date: Mon, 16 Dec 2024 14:26:43 +0100 Subject: [PATCH 34/35] chore: pull latest --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0f30fda..8aa678e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ Django>=4.0 -# algoliasearch>=4.0,<5.0 -algoliasearch @ git+https://github.com/algolia/algoliasearch-client-python@main +algoliasearch>=4.0,<5.0 # dev dependencies factory_boy>=3.0,<4.0 mock>=5.0,<6.0 From 5a07fe6d9e3e8d066f0985c728f6bbddf0d2cb44 Mon Sep 17 00:00:00 2001 From: shortcuts Date: Mon, 16 Dec 2024 14:27:45 +0100 Subject: [PATCH 35/35] chore: remove TODO --- algoliasearch_django/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/algoliasearch_django/models.py b/algoliasearch_django/models.py index 2d12355..5e55822 100644 --- a/algoliasearch_django/models.py +++ b/algoliasearch_django/models.py @@ -374,10 +374,9 @@ def update_records(self, qs, batch_size=1000, **kwargs): tmp["objectID"] = elt batch.append(dict(tmp)) - # TODO: pass batch_size to partial_update_objects if len(batch) > 0: self.__client.partial_update_objects( - index_name=self.index_name, objects=batch, wait_for_tasks=True + index_name=self.index_name, objects=batch, wait_for_tasks=True, batch_size=batch_size, ) def raw_search(self, query="", params=None):