diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 8869d73..0000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,22 +0,0 @@ - - -- Django version: -- Algolia Django integration version: -- Algolia Client Version: #.#.# -- Language Version: - -### Description - - -### Steps To Reproduce diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml new file mode 100644 index 0000000..da2ba62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -0,0 +1,73 @@ +name: Bug Report +description: File a bug report. +title: '[bug]: ' +labels: ['bug', 'triage'] +body: + - type: markdown + attributes: + value: | + ## Please help us help you! + + Before filing your issue, ask yourself: + - Is there an issue already opened for this bug? + - Can I reproduce it? + + 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. + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 24da431..6894e86 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 +🎟 Related Issue: -## Describe your change +### Changes included: -## What problem is this fixing? +## 🧪 Test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70632b3..cb72981 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 + # django 4.0 + - version: "3.8" + toxenv: py38-django40 + os: ubuntu-22.04 - version: "3.9" toxenv: py39-django40 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django41 - os: ubuntu-latest - - version: "3.9" - toxenv: py39-django42 - os: ubuntu-latest + os: ubuntu-22.04 - version: "3.10" toxenv: py310-django40 - os: ubuntu-latest + os: ubuntu-22.04 + # django 4.1 + - version: "3.8" + toxenv: py38-django41 + os: ubuntu-22.04 + - version: "3.9" + toxenv: py39-django41 + os: ubuntu-22.04 - version: "3.10" toxenv: py310-django41 - os: ubuntu-latest - - version: "3.10" - toxenv: py310-django42 - os: ubuntu-latest + os: ubuntu-22.04 - version: "3.11" toxenv: py311-django41 - os: ubuntu-latest + os: ubuntu-22.04 + # django 4.2 + - version: "3.8" + toxenv: py38-django42 + os: ubuntu-22.04 + - version: "3.9" + toxenv: py39-django42 + os: ubuntu-22.04 + - version: "3.10" + toxenv: py310-django42 + os: ubuntu-22.04 - version: "3.11" toxenv: py311-django42 - os: ubuntu-latest + os: ubuntu-22.04 + - version: "3.12" + toxenv: py312-django42 + os: ubuntu-22.04 + # django 5.0 + - version: "3.10" + toxenv: py310-django50 + os: ubuntu-22.04 + - version: "3.11" + toxenv: py311-django50 + os: ubuntu-22.04 + - version: "3.12" + toxenv: py312-django50 + os: ubuntu-22.04 + # django 5.1 + - version: "3.10" + toxenv: py310-django51 + os: ubuntu-22.04 + - version: "3.11" + toxenv: py311-django51 + os: ubuntu-22.04 + - version: "3.12" + toxenv: py312-django51 + os: ubuntu-22.04 + - version: "3.13" + toxenv: py313-django51 + os: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -72,10 +89,10 @@ jobs: python-version: ${{ matrix.version }} - name: Install dependencies and run tests + timeout-minutes: 20 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 TOXENV=${{ matrix.toxenv }} ALGOLIA_APPLICATION_ID=${{ secrets.ALGOLIA_APPLICATION_ID }} ALGOLIA_API_KEY=${{ secrets.ALGOLIA_API_KEY }} tox 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..ab0e2c7 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -1,5 +1,5 @@ ## `algolia/algoliasearch-django` maintainers -| Name | Email | -|-----------------|---------------------| -| Paul-Louis Nech | support@algolia.com | +| Name | Email | +|-----------------|------------------------| +| Algolia | https://alg.li/support | diff --git a/README.md b/README.md index abcc4e6..e430ff3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@
-
Documentation •
Community 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
@@ -358,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:
@@ -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/algoliasearch_django/__init__.py b/algoliasearch_django/__init__.py
index 95c7c7d..c4f1ef0 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,9 @@
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
-# Default log handler
-import logging
-
class NullHandler(logging.Handler):
def emit(self, record):
@@ -44,9 +41,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..9b9adea 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,21 +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:
@@ -47,11 +28,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
@@ -71,26 +53,26 @@ 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._AlgoliaEngine__registered_models # pyright: ignore
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, # pyright: ignore
+ sender=model,
)
pre_delete.disconnect(
- algolia_engine._AlgoliaEngine__pre_delete_receiver,
- sender=model
+ 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
+ algolia_engine._AlgoliaEngine__post_save_receiver, # pyright: ignore
+ sender=model,
)
pre_delete.connect(
- algolia_engine._AlgoliaEngine__pre_delete_receiver,
- sender=model
+ algolia_engine._AlgoliaEngine__pre_delete_receiver, # pyright: ignore
+ 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..2d12355 100644
--- a/algoliasearch_django/models.py
+++ b/algoliasearch_django/models.py
@@ -4,9 +4,12 @@
from functools import partial
from itertools import chain
import logging
+from typing import Callable, Iterable, Optional
-import sys
-from algoliasearch.exceptions import AlgoliaException
+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
@@ -26,8 +29,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 +45,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 = ()
@@ -56,7 +58,7 @@ class AlgoliaIndex(object):
tags = None
# Use to specify the index to target on Algolia.
- index_name = None
+ index_name: Optional[str] = None
# Use to specify the settings of the index.
settings = None
@@ -71,35 +73,52 @@ 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(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)
+
+ 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
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]
- 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,)
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 +126,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 +138,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,47 +173,28 @@ 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))
-
- 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)
-
- 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.__index = client.init_index(self.index_name)
- self.__tmp_index = client.init_index(self.tmp_index_name)
+ except Exception:
+ raise AlgoliaIndexError(
+ "{} is not an attribute nor a field of {}.".format(
+ self.should_index, model
+ )
+ )
@staticmethod
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 +209,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 +227,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"]) # pyright: ignore
- 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):
@@ -252,20 +257,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)
@@ -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):
@@ -299,35 +312,40 @@ 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.__index.partial_update_object(obj)
+ obj = self.get_raw_record(instance, update_fields=update_fields)
+ 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.__index.save_object(obj)
- logger.info('SAVE %s FROM %s', obj['objectID'], self.model)
- return result
+ 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)
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)
+ 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:
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,42 +368,43 @@ 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:
- self.__index.partial_update_objects(batch)
- batch = []
-
+ # TODO: pass batch_size to partial_update_objects
if len(batch) > 0:
- self.__index.partial_update_objects(batch)
+ self.__client.partial_update_objects(
+ index_name=self.index_name, objects=batch, wait_for_tasks=True
+ )
- 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 = {}
+ params = SearchParamsObject().to_dict()
+
+ params["query"] = query
try:
- return self.__index.search(query, params)
+ return self.__client.search_single_index(self.index_name, params).to_dict()
except AlgoliaException as e:
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):
+ def get_settings(self) -> Optional[dict]:
"""Returns the settings of the index."""
try:
- logger.info('GET SETTINGS ON %s', self.index_name)
- return self.__index.get_settings()
+ logger.info("GET SETTINGS ON %s", self.index_name)
+ return self.__client.get_settings(self.index_name).to_dict()
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."""
@@ -393,44 +412,43 @@ def set_settings(self):
return
try:
- self.__index.set_settings(self.settings)
- logger.info('APPLY SETTINGS ON %s', self.index_name)
+ _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:
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)
+ _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:
raise e
else:
- logger.warning('%s NOT CLEARED: %s', self.model, e)
-
- def clear_index(self):
- # TODO: add deprecated warning
- self.clear_objects()
+ logger.warning("%s NOT CLEARED: %s", self.model, e)
def wait_task(self, task_id):
try:
- self.__index.wait_task(task_id)
- logger.info('WAIT TASK %s', self.index_name)
+ self.__client.wait_for_task(self.index_name, task_id)
+ 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()
- if self.__tmp_index:
- self.__tmp_index.delete()
+ _resp = self.__client.delete_index(self.index_name)
+ self.__client.wait_for_task(self.index_name, _resp.task_id)
+ if 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):
"""
@@ -445,51 +463,60 @@ 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
else:
raise e # Unexpected error while getting settings
try:
+ should_keep_replicas = False
+ replicas = None
+
if self.settings:
- replicas = self.settings.get('replicas', None)
- slaves = self.settings.get('slaves', None)
+ replicas = self.settings.get("replicas", 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'] = []
- logger.debug("REMOVE SLAVES FROM SETTINGS")
- self.__tmp_index.set_settings(self.settings).wait()
- logger.debug('APPLY SETTINGS ON %s_tmp', self.index_name)
+ _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 = []
- 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.extend(_resp.hits)
+ )
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
+
+ synonyms = []
+ self.__client.browse_synonyms(
+ self.index_name, lambda _resp: synonyms.extend(_resp.hits)
+ )
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)
+ _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 = []
+ qs = []
- if hasattr(self, 'get_queryset'):
+ if hasattr(self, "get_queryset") and callable(self.get_queryset):
qs = self.get_queryset()
else:
qs = self.model.objects.all()
@@ -500,42 +527,56 @@ 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)
+ 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.__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(
+ 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)
+
+ _resp = self.__client.operation_index(
+ self.tmp_index_name,
+ OperationIndexParams(
+ operation=OperationType.MOVE,
+ destination=self.index_name, # pyright: ignore
+ ),
+ )
+ 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:
if should_keep_replicas:
- self.settings['replicas'] = 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:
- self.__index.set_settings(self.settings)
+ if should_keep_replicas:
+ _resp = self.__client.set_settings(self.index_name, self.settings)
+ self.__client.wait_for_task(self.index_name, _resp.task_id)
if should_keep_rules:
- response = self.__index.save_rules(rules, {'forwardToReplicas': True})
- response.wait()
- logger.info("Saved rules for index %s with response: {}".format(response), self.index_name)
+ _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(_resp),
+ self.index_name,
+ )
if should_keep_synonyms:
- response = self.__index.save_synonyms(synonyms, {'forwardToReplicas': True})
- response.wait()
- logger.info("Saved synonyms for index %s with response: {}".format(response), self.index_name)
+ _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(_resp),
+ 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..166a0bc 100644
--- a/algoliasearch_django/registration.py
+++ b/algoliasearch_django/registration.py
@@ -1,21 +1,17 @@
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.search_client import SearchClient
-from algoliasearch.user_agent import UserAgent
+from algoliasearch_django.version import VERSION as __version__
+from algoliasearch.search.client import SearchClientSync
from .models import AlgoliaIndex
from .settings import SETTINGS
-from .version import VERSION
-from django import get_version as django_version
logger = logging.getLogger(__name__)
-UserAgent.add("Algolia for Django", VERSION)
-UserAgent.add("Django", django_version())
-
class AlgoliaEngineError(Exception):
"""Something went wrong with Algolia Engine."""
@@ -30,17 +26,18 @@ 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 = {}
- self.client = SearchClient.create(app_id, api_key)
+ self.client = SearchClientSync(app_id, api_key)
+ 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"""
@@ -56,21 +53,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 +79,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 +100,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 +145,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 = {}
@@ -158,10 +158,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.
@@ -183,12 +179,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 c8cb9b4..0f30fda 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,14 @@
-django>=1.7
-algoliasearch>=3.0,<4.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
+wheel
diff --git a/runtests.py b/runtests.py
index 4318d94..8d97daa 100755
--- a/runtests.py
+++ b/runtests.py
@@ -9,12 +9,19 @@
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'])
+ test_runner = TestRunner(failfast=True)
+ # kept here to run a single test
+ # failures = test_runner.run_tests(
+ # [
+ # "tests.test_index.IndexTestCase.test_reindex_with_rules"
+ # ]
+ # )
+ 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 55a7bda..69affeb 100644
--- a/setup.py
+++ b/setup.py
@@ -10,45 +10,61 @@
# 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')
-if sys.version_info[0] == 3:
- exec(open(path_version).read())
+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:
- execfile(path_version)
+ exec(open(path_version).read())
setup(
- name='algoliasearch-django',
- version=VERSION,
- license='MIT License',
- packages=find_packages(exclude=['tests']),
- install_requires=['django>=1.7', 'algoliasearch>=3.0,<4.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"],
+ 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 :: 2',
- 'Programming Language :: Python :: 3',
- '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",
+ "Programming Language :: Python :: 3.13",
+ "Topic :: Internet :: WWW/HTTP",
+ ],
)
diff --git a/tests/factories.py b/tests/factories.py
index ad237d7..3f79076 100644
--- a/tests/factories.py
+++ b/tests/factories.py
@@ -1,37 +1,36 @@
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))
+ following_count = 0
+ followers_count = 0
- _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")
+ is_online = False
class Meta:
model = Website
diff --git a/tests/models.py b/tests/models.py
index 58e5723..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
@@ -19,13 +19,13 @@ def location(self):
return self._lat, self._lng
def permissions(self):
- return self._permissions.split(',')
+ return self._permissions.split(",")
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
@@ -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..c967350 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -1,46 +1,59 @@
"""
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__)))
-SECRET_KEY = 'MillisecondsMatter'
-DEBUG = False
+# 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"
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
# 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'
-)
+
+INSTALLED_APPS = [
+ "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.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,32 +70,44 @@
},
]
-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'),
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
}
}
# Internationalization
-LANGUAGE_CODE = 'en-us'
-TIME_ZONE = 'UTC'
+# 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())
+ 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..7dd8097 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
@@ -16,120 +16,145 @@ 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
- 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)
+ 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()
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)
+ 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..97be25c 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -1,13 +1,10 @@
import six
-import re
+from django import __version__ as __django__version__
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
+from algoliasearch_django import algolia_engine, __version__
from algoliasearch_django import AlgoliaIndex
from algoliasearch_django import AlgoliaEngine
from algoliasearch_django.registration import AlgoliaEngineError
@@ -26,20 +23,20 @@ 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):
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 ({}); Django ({})".format(
+ __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"""
@@ -50,7 +47,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 +57,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):
@@ -70,9 +66,9 @@ def test_get_adapter_exception(self):
def test_get_adapter_from_instance(self):
self.engine.register(Website)
instance = Website()
- self.assertEquals(
- AlgoliaIndex,
- self.engine.get_adapter_from_instance(instance).__class__)
+ self.assertEqual(
+ AlgoliaIndex, self.engine.get_adapter_from_instance(instance).__class__
+ )
def test_register(self):
self.engine.register(Website)
@@ -92,8 +88,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..33fbad3 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
@@ -15,41 +13,45 @@
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.website = Website(name="Algolia", url="https://algolia.com")
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 +59,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 +75,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 +92,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", # pyright: ignore
+ self.index.index_name + "_name_desc", # pyright: ignore
]
}
@@ -147,26 +137,21 @@ 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'
+ "replicas": [
+ self.index.index_name + "_name_asc", # pyright: ignore
+ self.index.index_name + "_name_desc", # pyright: ignore
]
}
- 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")
def test_reindex_no_settings(self):
self.maxDiff = None
@@ -182,37 +167,56 @@ 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(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")
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):
@@ -229,78 +233,101 @@ 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
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")
+ 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
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": {"hitsPerPage": 42}},
}
- underlying_index.save_rule(rule).wait()
+ self.assertIsNotNone(self.index.index_name)
+
+ if self.index.index_name is None:
+ return
+
+ _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)
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):
- copy = dict(rule)
- del copy["_metadata"]
- return copy
- rules = [r for r in underlying_index.browse_rules()]
- rules = list(map(remove_metadata, rules))
+ rules = []
+ self.client.browse_rules(
+ self.index.index_name,
+ lambda _resp: rules.extend([_hit.to_dict() for _hit in _resp.hits]),
+ )
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",
+ )
- @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
+
+ self.assertIsNotNone(self.index.index_name)
+
+ if self.index.index_name is None:
+ return
# Given some existing synonyms on the index
- synonym = {'objectID': 'street', 'type': 'altCorrection1', 'word': 'Street', 'corrections': ['St']}
- underlying_index.save_synonyms([synonym]).wait()
+ synonym = {
+ "objectID": "street",
+ "type": "altCorrection1",
+ "word": "Street",
+ "corrections": ["St"],
+ }
+ 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()
- 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.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")
+ 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.
@@ -308,365 +335,402 @@ 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
- time.sleep(10) # FIXME: Refactor reindex_all to return taskID
+ index.settings["hitsPerPage"] = old_hpp
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'
+ class ExampleIndex1(AlgoliaIndex):
+ fields = "name"
+ should_index = "property_should_index"
+
+ 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",
+ )
- 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")
+ class ExampleIndex2(AlgoliaIndex):
+ fields = "name"
+ should_index = "property_should_not_index"
- class ExampleIndex(AlgoliaIndex):
- fields = 'name'
- should_index = 'property_should_not_index'
+ 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",
+ )
- 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")
+ class ExampleIndex3(AlgoliaIndex):
+ fields = "name"
+ should_index = "property_string"
- class ExampleIndex(AlgoliaIndex):
- 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"):
+ self.index = ExampleIndex3(Example, self.client, settings.ALGOLIA)
+ 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
- )
self.index = AlgoliaIndex(Website, self.client, settings.ALGOLIA)
class WebsiteIndex(AlgoliaIndex):
+ custom_objectID = "name"
settings = {
- 'replicas': [
- self.index.index_name + '_name_asc',
- self.index.index_name + '_name_desc'
+ "replicas": [
+ self.index.index_name + "_name_asc", # pyright: ignore
+ self.index.index_name + "_name_desc", # pyright: ignore
]
}
- should_index = 'is_online'
+ 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):
- fields = ['bio', 'name']
+ fields = ["bio", "name"]
settings = {
- 'searchableAttributes': ['name', 'bio'],
+ "searchableAttributes": ["name", "bio"],
}
index_name = "test_cyrillic"
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 e576bd3..4ad9d02 100644
--- a/tests/test_signal.py
+++ b/tests/test_signal.py
@@ -1,12 +1,12 @@
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
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
@@ -14,16 +14,15 @@
class SignalTestCase(TestCase):
-
@classmethod
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:
+ 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(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')
+ 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 939d0c3..592e645 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,31 +13,26 @@ 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
+commands =
+ pip3 install -r requirements.txt
+ python runtests.py
[versions]
-twine = >=1.13,<2.0
-wheel = >=0.34,<1.0
+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.13
deps = coverage
passenv =
ALGOLIA*
@@ -55,7 +41,7 @@ commands =
coverage report
[testenv:coveralls]
-basepython = python3.8
+basepython = python3.13
deps =
coverage
coveralls
@@ -67,7 +53,7 @@ commands =
coveralls
[testenv:release]
-basepython = python3.8
+basepython = python3.13
deps =
twine {[versions]twine}
wheel {[versions]wheel}
@@ -78,3 +64,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