diff --git a/.gitignore b/.gitignore index e02f782..badc928 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ +.pytest_cache/ *.py[cod] *$py.class .venv/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 56d7dae..6e8b7ac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,4 +1,4 @@ -Changelog +1Changelog ========= This document describes changes between each past release. @@ -8,6 +8,8 @@ This document describes changes between each past release. - Add a ``--dry-run`` for the load command to see how many records would be deleted. (#46) +- Add a ``--delete-record`` to delete the existing records that are + not in the YAML file. (#47) 2.3.0 (2017-10-04) diff --git a/kinto_wizard/__main__.py b/kinto_wizard/__main__.py index ff78574..f8958cd 100644 --- a/kinto_wizard/__main__.py +++ b/kinto_wizard/__main__.py @@ -32,6 +32,9 @@ def main(): subparser.add_argument('--dry-run', help="Do not apply write call to the server", action='store_true') + subparser.add_argument('--delete-records', + help='Delete records that are not in the file.', + action='store_true') # dump sub-command. subparser = subparsers.add_parser('dump') @@ -91,6 +94,7 @@ def main(): config, bucket=args.bucket, collection=args.collection, - force=args.force + force=args.force, + delete_missing_records=args.delete_records ) ) diff --git a/kinto_wizard/yaml2kinto.py b/kinto_wizard/yaml2kinto.py index 2b93d05..6f9c5f4 100644 --- a/kinto_wizard/yaml2kinto.py +++ b/kinto_wizard/yaml2kinto.py @@ -3,16 +3,18 @@ from .kinto2yaml import introspect_server -async def initialize_server(async_client, config, bucket=None, collection=None, force=False): +async def initialize_server(async_client, config, bucket=None, collection=None, + force=False, delete_missing_records=False): logger.debug("Converting YAML config into a server batch.") bid = bucket cid = collection # 1. Introspect current server state. - if not force: + if not force or delete_missing_records: current_server_status = await introspect_server( async_client, bucket=bucket, - collection=collection + collection=collection, + records=delete_missing_records ) else: # We don't need to load it because we will override it nevertheless. @@ -143,5 +145,23 @@ async def initialize_server(async_client, config, bucket=None, collection=None, data=record_data, permissions=record_permissions) + if delete_missing_records and collection_records: + # Fetch all records IDs + file_records_ids = set(collection_records.keys()) + server_records_ids = set(current_collection['records'].keys()) + + to_delete = server_records_ids - file_records_ids + if not force: + message = ("Are you sure that you want to delete the " + "following {} records?".format(len(list(to_delete)))) + value = input(message) + if value.lower() not in ['y', 'yes']: + print("Exiting") + exit(1) + for record_id in to_delete: + batch.delete_record(id=record_id, + bucket=bucket_id, + collection=collection_id) + logger.debug('Sending batch:\n\n%s' % batch.session.requests) logger.info("Batch uploaded") diff --git a/tests/test_functional.py b/tests/test_functional.py index de5cc0e..7812915 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -1,9 +1,10 @@ +import builtins import io import os import pytest import unittest import sys -from contextlib import redirect_stdout +from contextlib import contextmanager, redirect_stdout import requests @@ -29,6 +30,14 @@ def test_dry_round_trip(self): client.get_bucket(id="staging") +@contextmanager +def mockInput(mock): + original_input = builtins.input + builtins.input = lambda _: mock + yield + builtins.input = original_input + + class SimpleDump(unittest.TestCase): def setUp(self): self.server = os.getenv("SERVER_URL", "http://localhost:8888/v1") @@ -108,6 +117,74 @@ def test_round_trip_with_client_wins(self): id='0831d549-0a69-48dd-b240-feef94688d47') assert set(record['data'].keys()) != {'id', 'last_modified'} + def test_round_trip_with_client_wins_and_delete_missing_records(self): + # Load some data + cmd = 'kinto-wizard {} --server={} --auth={}' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + main() + + # Change something that could make the server to fail. + client = Client(server_url=self.server, auth=tuple(self.auth.split(':'))) + client.create_record(bucket='build-hub', collection='archives', + id='8031d549-0a69-48dd-b240-feef94688d47', data={}) + cmd = 'kinto-wizard {} --server={} -D --auth={} --force --delete-records' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + main() + with pytest.raises(exceptions.KintoException) as exc: + client.get_record(bucket='build-hub', collection='archives', + id='8031d549-0a69-48dd-b240-feef94688d47') + assert "'Not Found'" in str(exc.value) + + def test_round_trip_with_delete_missing_records_ask_for_confirmation(self): + # Load some data + cmd = 'kinto-wizard {} --server={} --auth={}' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + main() + + # Change something that could make the server to fail. + client = Client(server_url=self.server, auth=tuple(self.auth.split(':'))) + client.create_record(bucket='build-hub', collection='archives', + id='8031d549-0a69-48dd-b240-feef94688d47', data={}) + cmd = 'kinto-wizard {} --server={} -D --auth={} --delete-records' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + + with mockInput('yes'): + main() + + with pytest.raises(exceptions.KintoException) as exc: + client.get_record(bucket='build-hub', collection='archives', + id='8031d549-0a69-48dd-b240-feef94688d47') + assert "'Not Found'" in str(exc.value) + + def test_round_trip_with_delete_missing_records_handle_misconfirmation(self): + # Load some data + cmd = 'kinto-wizard {} --server={} --auth={}' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + main() + + # Change something that could make the server to fail. + client = Client(server_url=self.server, auth=tuple(self.auth.split(':'))) + client.create_record(bucket='build-hub', collection='archives', + id='8031d549-0a69-48dd-b240-feef94688d47', data={}) + cmd = 'kinto-wizard {} --server={} -D --auth={} --delete-records' + load_cmd = cmd.format("load {}".format(self.file), + self.server, self.auth) + sys.argv = load_cmd.split(" ") + + with mockInput('no'): + with pytest.raises(SystemExit): + main() + class DataRecordsDump(unittest.TestCase): def setUp(self):