diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae0e1d37..87b114016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ * Adds `replace_all_rules` and `replace_all_synonyms` methods on client - PR [#390](https://github.com/algolia/algoliasearch-client-python/pull/390) +* Adds `AccountClient.copy_index` methods on client - PR [#391](https://github.com/algolia/algoliasearch-client-python/pull/391) + ### 1.17.0 - 2018-06-19 * Introduce AB Testing feature - PR [#408](https://github.com/algolia/algoliasearch-client-php/pull/#408) diff --git a/algoliasearch/__init__.py b/algoliasearch/__init__.py index 4d4e5a10b..1182e2a9b 100644 --- a/algoliasearch/__init__.py +++ b/algoliasearch/__init__.py @@ -22,6 +22,7 @@ THE SOFTWARE. """ +from . import account_client from . import client from . import index from . import helpers @@ -32,6 +33,7 @@ class algoliasearch(object): VERSION = version.VERSION + AccountClient = account_client.AccountClient Client = client.Client Index = index.Index AlgoliaException = helpers.AlgoliaException diff --git a/algoliasearch/account_client.py b/algoliasearch/account_client.py new file mode 100644 index 000000000..375d27a4a --- /dev/null +++ b/algoliasearch/account_client.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2013 Algolia +http://www.algolia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights lw1 +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +from .helpers import AlgoliaException + + +class AccountClient: + """ + The Account Client + """ + + @staticmethod + def copy_index(source_index, destination_index, request_options=None): + """ + The method copy settings, synonyms, rules and objects from the source + index to the destination index. The replicas of the source index will + not be copied. + + Throw an exception if the destination index already exists + Throw an exception if the indices are on the same application + + @param source_index the source index + @param destination_index the destination index + @param request_options + """ + if source_index.client.app_id == destination_index.client.app_id: + raise AlgoliaException('The indexes are on the same application. Use client.copy_index instead.') + + try: + destination_index.get_settings() + except AlgoliaException: + pass + else: + raise AlgoliaException( + 'Destination index already exists. Please delete it before copying index across applications.') + + responses = [] + + # Copy settings + settings = source_index.get_settings() + responses.append(destination_index.set_settings(settings, False, False, request_options)) + + # Copy synonyms + synonyms = list(source_index.iter_synonyms()) + responses.append(destination_index.batch_synonyms(synonyms, False, False, False, request_options)) + + # Copy rules + rules = list(source_index.iter_rules()) + responses.append(destination_index.batch_rules(rules, False, False, request_options)) + + # Copy objects + responses = [] + batch = [] + batch_size = 1000 + count = 0 + for obj in source_index.browse_all(): + batch.append(obj) + count += 1 + + if count == batch_size: + response = destination_index.save_objects(batch, request_options) + responses.append(response) + batch = [] + count = 0 + + if batch: + response = destination_index.save_objects(batch, request_options) + responses.append(response) + + return responses diff --git a/tests/conftest.py b/tests/conftest.py index 89ba620c2..023dfc351 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,36 @@ def mcm_client(): return create_client('ALGOLIA_APP_ID_MCM', 'ALGOLIA_API_KEY_MCM') +@pytest.fixture +def client_1(): + return create_client('ALGOLIA_APPLICATION_ID_1', 'ALGOLIA_ADMIN_KEY_1') + + +@pytest.fixture +def client_2(): + return create_client('ALGOLIA_APPLICATION_ID_2', 'ALGOLIA_ADMIN_KEY_2') + + +@pytest.fixture +def index_1(client_1): + idx = create_index(client_1) + yield idx + client_1.delete_index(idx.index_name) # Tear down + + +@pytest.fixture +def index_2(client_2): + idx = create_index(client_2) + yield idx + client_2.delete_index(idx.index_name) # Tear down + + +@pytest.fixture +def mcm_index(mcm_client): + idx = create_index(mcm_client) + yield idx + mcm_client.delete_index(idx.index_name) # Tear down + @pytest.fixture def client(): return create_client() diff --git a/tests/helpers.py b/tests/helpers.py index 9ef964f19..eb1fc752b 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,6 @@ import os import time +import datetime from random import randint from algoliasearch import algoliasearch @@ -12,7 +13,12 @@ def check_credentials(): 'ALGOLIA_API_KEY', 'ALGOLIA_SEARCH_API_KEY', 'ALGOLIA_APP_ID_MCM', - 'ALGOLIA_API_KEY_MCM' + 'ALGOLIA_API_KEY_MCM', + # CTS: + 'ALGOLIA_APPLICATION_ID_1', + 'ALGOLIA_ADMIN_KEY_1', + 'ALGOLIA_APPLICATION_ID_2', + 'ALGOLIA_ADMIN_KEY_2', ] for credential in credentials: @@ -25,12 +31,14 @@ def check_credentials(): def index_name(): - name = 'algolia-python{}'.format(randint(1, 100000)) + date = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") if 'TRAVIS' in os.environ: - name = 'TRAVIS_PYTHON_{}_id-{}'.format(name, os.environ['TRAVIS_JOB_NUMBER']) + instance = os.environ['TRAVIS_JOB_NUMBER'] + else: + instance = 'unknown' - return name + return 'python_%s_%s' % (date,instance) class Factory: diff --git a/tests/test_account_client.py b/tests/test_account_client.py new file mode 100644 index 000000000..1e20bea1c --- /dev/null +++ b/tests/test_account_client.py @@ -0,0 +1,50 @@ +import pytest + +from algoliasearch.account_client import AccountClient +from algoliasearch.helpers import AlgoliaException +from .helpers import rule_stub, synonym_stub +from .helpers import is_community + +def test_copy_index_applications_must_be_different(client_1): + + index_1 = client_1.init_index('copy_index') + index_2 = client_1.init_index('copy_index_2') + + with pytest.raises(AlgoliaException): + AccountClient.copy_index(index_1, index_2) + +def test_copy_index_copy_the_index_and_destination_must_not_exist(index_1, index_2): + responses = [ + index_1.save_object({'objectID': 'one'}), + index_1.batch_rules([rule_stub('one')]), + index_1.batch_synonyms([synonym_stub('one')]), + index_1.set_settings({'searchableAttributes': ['objectID']}) + ] + + for response in responses: + index_1.wait_task(response['taskID']) + + responses = AccountClient.copy_index(index_1, index_2) + for response in responses: + index_2.wait_task(response['taskID']) + + # Assert objects got copied + res = index_2.search('') + assert len(res['hits']) == 1 + assert res['hits'][0] == {'objectID': 'one'} + + # Assert settings got copied + settings = index_2.get_settings() + assert settings['searchableAttributes'] == ['objectID'] + + # Assert synonyms got copied + rule = index_2.read_rule('one') + assert rule == rule_stub('one') + + # Assert synonyms got copied + synonym = index_2.get_synonym('one') + assert synonym == synonym_stub('one') + + # Assert that copying again fails because index already exists + with pytest.raises(AlgoliaException): + AccountClient.copy_index(index_1, index_2)