From 1188a93271d6a9731145987243da057a64f2b61d Mon Sep 17 00:00:00 2001 From: Addison Schiller Date: Wed, 18 Oct 2017 11:03:48 -0400 Subject: [PATCH] Remove unneeded API calls in folder_file_ops Added construct_path and construct_empty_path to base- provider for individual providers to make paths for folder-file-ops without needing to make API calls --- tests/core/test_provider.py | 9 +++ tests/providers/bitbucket/test_provider.py | 55 +++++++++++++------ tests/providers/box/test_provider.py | 28 ++++++---- tests/providers/cloudfiles/test_provider.py | 16 +++++- tests/providers/dataverse/test_provider.py | 32 ++++++++++- tests/providers/dropbox/test_provider.py | 14 ++++- tests/providers/figshare/test_provider.py | 30 ++++++++++ tests/providers/filesystem/test_provider.py | 19 +++++++ tests/providers/github/test_provider.py | 12 ++++ tests/providers/googledrive/test_provider.py | 46 +++++++++++----- tests/providers/osfstorage/test_provider.py | 17 ++++++ tests/providers/owncloud/test_provider.py | 8 +++ tests/providers/s3/test_provider.py | 7 +++ waterbutler/core/provider.py | 37 ++++++++++++- waterbutler/providers/bitbucket/provider.py | 10 +++- waterbutler/providers/box/provider.py | 11 +++- waterbutler/providers/cloudfiles/provider.py | 12 +++- waterbutler/providers/dataverse/provider.py | 9 ++- waterbutler/providers/dropbox/provider.py | 5 ++ waterbutler/providers/figshare/provider.py | 5 ++ waterbutler/providers/filesystem/provider.py | 10 +++- waterbutler/providers/github/provider.py | 16 ++++-- waterbutler/providers/googledrive/provider.py | 7 ++- waterbutler/providers/osfstorage/provider.py | 12 +++- waterbutler/providers/owncloud/provider.py | 8 ++- waterbutler/providers/s3/provider.py | 16 ++++-- 26 files changed, 377 insertions(+), 74 deletions(-) diff --git a/tests/core/test_provider.py b/tests/core/test_provider.py index 123888ca0..6d2a18161 100644 --- a/tests/core/test_provider.py +++ b/tests/core/test_provider.py @@ -104,6 +104,15 @@ async def test_revalidate_path_is_child(self, provider1): assert str(path) == str(new_path.parent) assert new_path.name == 'text_file.txt' + @pytest.mark.asyncio + async def test_construct_empty_path(self, provider1): + path = await provider1.validate_path('/folder/') + data = utils.MockFileMetadata() + empty_path = await provider1.construct_empty_path(path, data) + + # Make sure its actually not a real path + assert empty_path.identifier is None + class TestHandleNameConflict: diff --git a/tests/providers/bitbucket/test_provider.py b/tests/providers/bitbucket/test_provider.py index ea9ad8248..4928a7940 100644 --- a/tests/providers/bitbucket/test_provider.py +++ b/tests/providers/bitbucket/test_provider.py @@ -61,7 +61,7 @@ async def test_validate_v1_path_root(self, provider): assert wb_path_v1 == wb_path_v0 assert wb_path_v1.branch_name == default_branch_body['name'] - assert wb_path_v1.commit_sha == None + assert wb_path_v1.commit_sha is None @pytest.mark.asyncio @pytest.mark.aiohttpretty @@ -77,8 +77,8 @@ async def test_validate_v1_path(self, provider, path, kind): default_branch_url = provider._build_v1_repo_url('main-branch') aiohttpretty.register_json_uri('GET', default_branch_url, body=default_branch_body) - dir_listing_body = test_fixtures['root_dir_listing'] - dir_listing_url = provider._build_v1_repo_url('src', default_branch) + '/' + dir_listing_body = test_fixtures['root_dir_listing'] + dir_listing_url = provider._build_v1_repo_url('src', default_branch) + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) try: @@ -107,9 +107,9 @@ async def test_validate_v1_path(self, provider, path, kind): async def test_validate_v1_path_commit_sha(self, provider, arg_name, arg_val, attr_name): test_fixtures = fixtures.validate_path - dir_listing_body = test_fixtures['root_dir_listing'] + dir_listing_body = test_fixtures['root_dir_listing'] base_commit = dir_listing_body['node'] - dir_listing_url = provider._build_v1_repo_url('src', arg_val) + '/' + dir_listing_url = provider._build_v1_repo_url('src', arg_val) + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) path = '/foo-file.txt' @@ -145,9 +145,9 @@ async def test_validate_v1_path_commit_sha(self, provider, arg_name, arg_val, at async def test_validate_v1_path_subfolder(self, provider): test_fixtures = fixtures.validate_path - dir_listing_body = test_fixtures['subfolder_dir_listing'] + dir_listing_body = test_fixtures['subfolder_dir_listing'] base_commit = dir_listing_body['node'] - dir_listing_url = provider._build_v1_repo_url('src', 'main-branch', 'subfolder') + '/' + dir_listing_url = provider._build_v1_repo_url('src', 'main-branch', 'subfolder') + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) path = '/subfolder/.gitkeep' @@ -186,8 +186,8 @@ async def test_get_metadata_for_file(self, provider): path = BitbucketPath('/foo-file.txt', _ids=[(base_ref, 'develop'), (base_ref, 'develop')]) test_fixtures = fixtures.validate_path - dir_listing_body = test_fixtures['root_dir_listing'] - dir_listing_url = provider._build_v1_repo_url('src', base_ref) + '/' + dir_listing_body = test_fixtures['root_dir_listing'] + dir_listing_url = provider._build_v1_repo_url('src', base_ref) + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) result = await provider.metadata(path) @@ -211,8 +211,8 @@ async def test_get_metadata_for_folder(self, provider): path = BitbucketPath('/', _ids=[(None, 'develop')], folder=True) test_fixtures = fixtures.validate_path - dir_listing_body = test_fixtures['root_dir_listing'] - dir_listing_url = provider._build_v1_repo_url('src', 'develop') + '/' + dir_listing_body = test_fixtures['root_dir_listing'] + dir_listing_url = provider._build_v1_repo_url('src', 'develop') + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) result = await provider.metadata(path) @@ -229,8 +229,8 @@ async def test_get_metadata_for_file(self, provider): path = BitbucketPath('/foo-file.txt', _ids=[(base_ref, 'develop'), (base_ref, 'develop')]) test_fixtures = fixtures.validate_path - dir_listing_body = test_fixtures['root_dir_listing'] - dir_listing_url = provider._build_v1_repo_url('src', base_ref) + '/' + dir_listing_body = test_fixtures['root_dir_listing'] + dir_listing_url = provider._build_v1_repo_url('src', base_ref) + '/' aiohttpretty.register_json_uri('GET', dir_listing_url, body=dir_listing_body) download_url = provider._build_v1_repo_url('raw', path.commit_sha, *path.path_tuple()) @@ -268,17 +268,38 @@ async def test_copy_to(self, provider): assert e.value.code == 501 def test_can_intra_move(self, provider): - assert provider.can_intra_move(provider) == False + assert provider.can_intra_move(provider) is False def test_can_intra_copy(self, provider): - assert provider.can_intra_copy(provider) == False + assert provider.can_intra_copy(provider) is False # leftover bits class TestMisc: + @pytest.mark.asyncio + async def test_construct_path(self, provider): + name = 'aaa-01-2.txt' + subdir = 'plaster' + full_path = '/{}/{}'.format(subdir, name) + branch = 'master' + commit_sha = '123abc456def' + + path = BitbucketPath(full_path, _ids=[ + (commit_sha, branch), (commit_sha, branch), (commit_sha, branch) + ]) + + metadata = BitbucketFileMetadata(fixtures.file_metadata, path, owner=fixtures.owner, repo=fixtures.repo) + child_path = await provider.construct_path(path.parent, metadata) + rev_metadata = await provider.revalidate_path(path.parent, + metadata.name, folder=metadata.is_folder) + + assert child_path.full_path == path.full_path + assert child_path == path + assert child_path == rev_metadata + def test_can_duplicate_name(self, provider): - assert provider.can_duplicate_names() == False + assert provider.can_duplicate_names() is False def test_path_from_metadata(self, provider): name = 'aaa-01-2.txt' @@ -292,7 +313,7 @@ def test_path_from_metadata(self, provider): ]) metadata = BitbucketFileMetadata(fixtures.file_metadata, path, owner=fixtures.owner, repo=fixtures.repo) - child_path = provider.path_from_metadata(path.parent, metadata) + child_path = provider.path_from_metadata(path.parent, metadata) assert child_path.full_path == path.full_path assert child_path == path diff --git a/tests/providers/box/test_provider.py b/tests/providers/box/test_provider.py index 47e8d55ab..cb95f7583 100644 --- a/tests/providers/box/test_provider.py +++ b/tests/providers/box/test_provider.py @@ -834,23 +834,31 @@ class TestOperations: @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_can_duplicate_names(self, provider): + async def test_construct_path(self, provider, root_provider_fixtures): + path = WaterButlerPath('/50 shades of nope/', _ids=(provider.folder, '0')) + item = root_provider_fixtures['revalidate_metadata']['entries'][0] + data = BoxFileMetadata(item, path) + + url = provider.build_url('folders', path.identifier, 'items', + fields='id,name,type', limit=1000) + aiohttpretty.register_json_uri('GET', url, body=root_provider_fixtures['revalidate_metadata']) + + con_metadata = await provider.construct_path(path, data) + + path_metadata = await provider.revalidate_path(path, data.name) + assert con_metadata == path_metadata + + def test_can_duplicate_names(self, provider): assert provider.can_duplicate_names() is False - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_shares_storage_root(self, provider, other_provider): + def test_shares_storage_root(self, provider, other_provider): assert provider.shares_storage_root(other_provider) is False assert provider.shares_storage_root(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_move(self, provider, other_provider): + def test_can_intra_move(self, provider, other_provider): assert provider.can_intra_move(other_provider) is False assert provider.can_intra_move(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_copy(self, provider, other_provider): + def test_can_intra_copy(self, provider, other_provider): assert provider.can_intra_copy(other_provider) is False assert provider.can_intra_copy(provider) is True diff --git a/tests/providers/cloudfiles/test_provider.py b/tests/providers/cloudfiles/test_provider.py index da8bb55a5..9436b9a70 100644 --- a/tests/providers/cloudfiles/test_provider.py +++ b/tests/providers/cloudfiles/test_provider.py @@ -3,7 +3,6 @@ import json import time import hashlib -import functools from unittest import mock import furl @@ -17,6 +16,7 @@ from waterbutler.core.path import WaterButlerPath from waterbutler.providers.cloudfiles import CloudFilesProvider from waterbutler.providers.cloudfiles import settings as cloud_settings +from waterbutler.providers.cloudfiles.metadata import CloudFilesFileMetadata @pytest.fixture @@ -180,7 +180,8 @@ def file_metadata(): ('ETAG', 'edfa12d00b779b4b37b81fe5b61b2b3f'), ('CONTENT-TYPE', 'text/html; charset=UTF-8'), ('X-TRANS-ID', 'txf876a4b088e3451d94442-00549b7c6aiad3'), - ('DATE', 'Thu, 25 Dec 2014 02:54:34 GMT') + ('DATE', 'Thu, 25 Dec 2014 02:54:34 GMT'), + ('NAME', 'file.txt') ]) @@ -664,6 +665,17 @@ async def test_v1_validate_path(self, connected_provider): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, file_metadata): + # Construct replaces revalidate_path in some instances, so it should always + # be tested against it, even if calls revalidate_path + path = WaterButlerPath('/file.txt') + + data = CloudFilesFileMetadata(file_metadata) + rev_path = await provider.revalidate_path(path.parent, data.name) + con_path = await provider.construct_path(path.parent, data) + assert rev_path == con_path + @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_ensure_connection(self, provider, auth_json, mock_temp_key): diff --git a/tests/providers/dataverse/test_provider.py b/tests/providers/dataverse/test_provider.py index 141abe45c..f8eccab8e 100644 --- a/tests/providers/dataverse/test_provider.py +++ b/tests/providers/dataverse/test_provider.py @@ -110,7 +110,6 @@ async def test_revalidate_path(self, provider, native_dataset_metadata): status=200, body=native_dataset_metadata) - base = await provider.validate_v1_path('/') wb_path = await provider.revalidate_path(base, '/thefile.txt') @@ -301,7 +300,7 @@ async def test_upload_checksum_mismatch(self, provider, file_stream, ]) path = await provider.validate_path(path) - with pytest.raises(exceptions.UploadChecksumMismatchError) as exc: + with pytest.raises(exceptions.UploadChecksumMismatchError): await provider.upload(file_stream, path) assert aiohttpretty.has_call(method='POST', uri=url) @@ -511,12 +510,39 @@ async def test_metadata_never_published_raises_errors(self, provider): path = await provider.validate_path('/') with pytest.raises(exceptions.MetadataError) as e: - result = await provider.metadata(path) + await provider.metadata(path) assert e.value.code == 400 class TestUtils: + @pytest.mark.asyncio + @pytest.mark.aiohttpretty + async def test_construct_path(self, provider, native_dataset_metadata): + entry = native_dataset_metadata['data']['files'][0]['datafile'] + metadata = DataverseFileMetadata(entry, 'latest') + + draft_url = provider.build_url(dvs.JSON_BASE_URL.format(provider._id, 'latest'), + key=provider.token) + published_url = provider.build_url(dvs.JSON_BASE_URL.format(provider._id, + 'latest-published'), + key=provider.token) + + aiohttpretty.register_json_uri('GET', + draft_url, + status=200, + body=native_dataset_metadata) + aiohttpretty.register_json_uri('GET', + published_url, + status=200, + body=native_dataset_metadata) + + base = await provider.validate_v1_path('/') + + rev_path = await provider.revalidate_path(base, metadata.name) + con_path = await provider.construct_path(base, metadata) + assert rev_path == con_path + def test_utils(self, provider): assert not provider.can_duplicate_names() diff --git a/tests/providers/dropbox/test_provider.py b/tests/providers/dropbox/test_provider.py index 01f1b6c00..b8f539390 100644 --- a/tests/providers/dropbox/test_provider.py +++ b/tests/providers/dropbox/test_provider.py @@ -722,6 +722,18 @@ async def test_intra_move_casing_change(self, provider): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, root_provider_fixtures): + # Construct replaces revalidate_path in some instances, so it should always + # be tested against it, even if calls revalidate_path + path = WaterButlerPath('/file.txt') + + data = DropboxFileMetadata(root_provider_fixtures['file_metadata'], folder=False) + + rev_path = await provider.revalidate_path(path.parent, data.name) + con_path = await provider.construct_path(path.parent, data) + assert rev_path == con_path + def test_can_intra_copy(self, provider): assert provider.can_intra_copy(provider) @@ -732,7 +744,7 @@ def test_can_intra_move(self, provider): assert provider.can_intra_move(provider) def test_cannot_intra_move_other(self, provider, other_provider): - assert provider.can_intra_move(other_provider) == False + assert provider.can_intra_move(other_provider) is False def test_conflict_error_handler_not_found(self, provider, error_fixtures): error_path = '/Photos/folder/file' diff --git a/tests/providers/figshare/test_provider.py b/tests/providers/figshare/test_provider.py index 55b37c8d8..4b1362e84 100644 --- a/tests/providers/figshare/test_provider.py +++ b/tests/providers/figshare/test_provider.py @@ -1387,6 +1387,36 @@ async def test_article_revalidate_path_file(self, article_provider, crud_fixture class TestMisc: + @pytest.mark.asyncio + @pytest.mark.aiohttpretty + async def test_construct_path(self, project_provider, root_provider_fixtures): + file_article_id = str(root_provider_fixtures['list_project_articles'][0]['id']) + folder_article_id = str(root_provider_fixtures['list_project_articles'][1]['id']) + + root_parts = project_provider.root_path_parts + list_articles_url = project_provider.build_url(False, *root_parts, 'articles') + file_article_url = project_provider.build_url(False, *root_parts, 'articles', + file_article_id) + folder_article_url = project_provider.build_url(False, *root_parts, 'articles', + folder_article_id) + + aiohttpretty.register_json_uri('GET', list_articles_url, + body=root_provider_fixtures['list_project_articles'], + params={'page': '1', 'page_size': str(MAX_PAGE_SIZE)}) + aiohttpretty.register_json_uri('GET', list_articles_url, body=[], + params={'page': '2', 'page_size': str(MAX_PAGE_SIZE)}) + aiohttpretty.register_json_uri('GET', file_article_url, + body=root_provider_fixtures['file_article_metadata']) + aiohttpretty.register_json_uri('GET', folder_article_url, + body=root_provider_fixtures['folder_article_metadata']) + + path = FigsharePath('/test/', _ids=('0', folder_article_id), folder=True, parent_is_folder=False) + base_meta = root_provider_fixtures['file_article_metadata'] + data = metadata.FigshareFileMetadata(base_meta, base_meta['files'][0]) + rev_path = await project_provider.revalidate_path(path, data.name, folder=False) + con_path = await project_provider.construct_path(path, data) + assert con_path == rev_path + def test_path_from_metadata(self, project_provider, root_provider_fixtures): file_article_metadata = root_provider_fixtures['file_article_metadata'] fig_metadata = metadata.FigshareFileMetadata(file_article_metadata) diff --git a/tests/providers/filesystem/test_provider.py b/tests/providers/filesystem/test_provider.py index c15c67981..98dcb26b5 100644 --- a/tests/providers/filesystem/test_provider.py +++ b/tests/providers/filesystem/test_provider.py @@ -24,6 +24,17 @@ def credentials(): return {} +@pytest.fixture +def file_metadata(): + return { + 'path': '/code/website/osfstoragecache/77094244-aa24-48da-9437-d8ce6f7a94e9', + 'modified_utc': '2017-09-20T15:16:02.601916+00:00', + 'mime_type': None, + 'size': 35981, + 'modified': 'Wed, 20 Sep 2017 15:16:02 +0000' + } + + @pytest.fixture def settings(tmpdir): return {'folder': str(tmpdir)} @@ -271,6 +282,14 @@ async def test_intra_move_file(self, provider): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, file_metadata): + data = FileSystemFileMetadata(file_metadata, '/') + path = await provider.validate_path('/folder/') + con_path = await provider.construct_path(path, data) + rev_path = await provider.revalidate_path(path, data.name) + assert con_path == rev_path + def test_can_duplicate_names(self, provider): assert provider.can_duplicate_names() is False diff --git a/tests/providers/github/test_provider.py b/tests/providers/github/test_provider.py index 9226f8dbe..d2113dd8c 100644 --- a/tests/providers/github/test_provider.py +++ b/tests/providers/github/test_provider.py @@ -1349,6 +1349,18 @@ async def test_returns_metadata(self, provider, root_provider_fixtures): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, root_provider_fixtures): + path = GitHubPath('/folder/', _ids=[("master", ''), ('whatever', '')]) + item = root_provider_fixtures['content_repo_metadata_root'][0] + data = GitHubFileContentMetadata( + item, web_view=item['html_url'], ref=provider.default_branch + ) + + con_path = await provider.construct_path(path, data) + rev_path = await provider.revalidate_path(path, data.name) + assert con_path == rev_path + def test_can_duplicate_names(self, provider): assert provider.can_duplicate_names() is False diff --git a/tests/providers/googledrive/test_provider.py b/tests/providers/googledrive/test_provider.py index 81624f5a5..45f5f625a 100644 --- a/tests/providers/googledrive/test_provider.py +++ b/tests/providers/googledrive/test_provider.py @@ -1579,31 +1579,51 @@ class TestOperationsOrMisc: @pytest.mark.asyncio @pytest.mark.aiohttpretty - async def test_can_duplicate_names(self, provider): + async def test_construct_path(self, provider, root_provider_fixtures): + file_name = '/Gear1.stl' + revalidate_path_metadata = root_provider_fixtures['revalidate_path_file_metadata_1'] + file_id = revalidate_path_metadata['items'][0]['id'] + path = GoogleDrivePath(file_name, _ids=['0', file_id]) + + parts = [[parse.unquote(x), True] for x in file_name.strip('/').split('/')] + parts[-1][1] = False + + current_part = parts.pop(0) + part_name = current_part[0] + name, ext = os.path.splitext(part_name) + query = _build_title_search_query(provider, file_name.strip('/'), False) + + url = provider.build_url('files', file_id, 'children', q=query, fields='items(id)') + aiohttpretty.register_json_uri('GET', url, body=revalidate_path_metadata) + + url = provider.build_url('files', file_id, fields='id,title,mimeType') + aiohttpretty.register_json_uri('GET', url, + body=root_provider_fixtures['revalidate_path_file_metadata_2']) + + rev_path = await provider.revalidate_path(path, file_name) + + data = GoogleDriveFileMetadata(root_provider_fixtures['revalidate_path_file_metadata_2'], '/') + con_path = await provider.construct_path(path, data) + rev_path = await provider.revalidate_path(path, data.name) + assert con_path == rev_path + + def test_can_duplicate_names(self, provider): assert provider.can_duplicate_names() is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_shares_storage_root(self, provider, other_provider): + def test_shares_storage_root(self, provider, other_provider): assert provider.shares_storage_root(other_provider) is True assert provider.shares_storage_root(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_move(self, provider, other_provider): + def test_can_intra_move(self, provider, other_provider): assert provider.can_intra_move(other_provider) is False assert provider.can_intra_move(provider) is True - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test__serialize_item_raw(self, provider, root_provider_fixtures): + def test__serialize_item_raw(self, provider, root_provider_fixtures): item = root_provider_fixtures['docs_file_metadata'] assert provider._serialize_item(None, item, True) == item - @pytest.mark.asyncio - @pytest.mark.aiohttpretty - async def test_can_intra_copy(self, provider, other_provider, root_provider_fixtures): + def test_can_intra_copy(self, provider, other_provider, root_provider_fixtures): item = root_provider_fixtures['list_file']['items'][0] path = WaterButlerPath('/birdie.jpg', _ids=(provider.folder['id'], item['id'])) diff --git a/tests/providers/osfstorage/test_provider.py b/tests/providers/osfstorage/test_provider.py index e0e6d52da..a914c4f26 100644 --- a/tests/providers/osfstorage/test_provider.py +++ b/tests/providers/osfstorage/test_provider.py @@ -495,6 +495,23 @@ async def test_intra_move_file(self, provider_and_mock, provider_and_mock2, mock class TestUtils: + @pytest.mark.asyncio + @pytest.mark.aiohttpretty + async def test_construct_path(self, provider, folder_path, folder_children_metadata, + mock_time): + url, params = build_signed_url_without_auth(provider, 'GET', folder_path.identifier, + 'children') + aiohttpretty.register_json_uri('GET', url, params=params, status=200, + body=folder_children_metadata) + + rev_path = await provider.revalidate_path(folder_path, + folder_children_metadata[1]['name'], + folder=False) + + data = OsfStorageFileMetadata(folder_children_metadata[1], '/') + con_path = await provider.construct_path(folder_path, data) + assert rev_path == con_path + def test_intra_move_copy_utils(self, provider): assert provider.can_duplicate_names() diff --git a/tests/providers/owncloud/test_provider.py b/tests/providers/owncloud/test_provider.py index 3ef3cb5d9..28815fad4 100644 --- a/tests/providers/owncloud/test_provider.py +++ b/tests/providers/owncloud/test_provider.py @@ -360,6 +360,14 @@ async def test_revisions(self, provider, file_metadata): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, file_metadata): + data = OwnCloudFileMetadata(file_metadata, '/') + path = WaterButlerPath('/dissertation/', prepend=provider.folder) + con_path = await provider.construct_path(path, data) + rev_path = await provider.revalidate_path(path, data.name) + assert con_path == rev_path + def test_can_intra_copy(self, provider, provider_different_credentials): assert provider.can_intra_copy(provider) assert not provider.can_intra_copy(provider_different_credentials) diff --git a/tests/providers/s3/test_provider.py b/tests/providers/s3/test_provider.py index 774c4bdd9..2f446a95d 100644 --- a/tests/providers/s3/test_provider.py +++ b/tests/providers/s3/test_provider.py @@ -807,6 +807,13 @@ async def test_creates(self, provider, mock_time): class TestOperations: + @pytest.mark.asyncio + async def test_construct_path(self, provider, file_metadata_object): + path = WaterButlerPath('/dissertation/') + con_path = await provider.construct_path(path, file_metadata_object) + rev_path = await provider.revalidate_path(path, file_metadata_object.name) + assert con_path == rev_path + @pytest.mark.asyncio @pytest.mark.aiohttpretty async def test_intra_copy(self, provider, file_header_metadata, mock_time): diff --git a/waterbutler/core/provider.py b/waterbutler/core/provider.py index ba443e33f..cf52e29b2 100644 --- a/waterbutler/core/provider.py +++ b/waterbutler/core/provider.py @@ -349,8 +349,8 @@ async def _folder_file_op(self, func( dest_provider, # TODO figure out a way to cut down on all the requests made here - (await self.revalidate_path(src_path, item.name, folder=item.is_folder)), - (await dest_provider.revalidate_path(dest_path, item.name, folder=item.is_folder)), + (await self.construct_path(src_path, item)), + (await dest_provider.construct_empty_path(dest_path, item)), handle_naming=False, ) )) @@ -670,6 +670,39 @@ async def validate_path(self, path: str, **kwargs) -> wb_path.WaterButlerPath: """ raise NotImplementedError + async def construct_path(self, + parent_path: wb_path.WaterButlerPath, + meta_data: wb_metadata.BaseMetadata) -> wb_path.WaterButlerPath: + """Given a path and metadata, figure out how to return a child path with as few calls as possible. + For name based providers, this will usually just return revalidate_path. for ID based providers + it will usually return path_from_metadata or something similar. + + Used by folder-file-op to cut down on number of API calls made. + + :param parent_path: ( :class:`.WaterButlerPath` ) The base parent to create the child from + :param meta_data: ( :class:`.BaseMetadata`) the path of a child of `base`, relative to `base` + :rtype: :class:`.WaterButlerPath` + """ + # Went with a `NotImplementedError` since a provider should have to determine how to construct path + # It can call revalidate path, path_from_metadata, or something else. This is different from + # `construct_empty_path` since that one almost always returns the same thing + raise NotImplementedError + + async def construct_empty_path(self, + parent_path: wb_path.WaterButlerPath, + meta_data: wb_metadata.BaseMetadata) -> wb_path.WaterButlerPath: + """Given a path and metadata, figure out how to return an empty child path with as few calls as possible. + This is called when we need a path but usually the file does not exist yet. We need identifier to be `None` + here so other similar functions will not work correctly. + + :param parent_path: ( :class:`.WaterButlerPath` ) The base parent to create the child from + :param meta_data: ( :class:`.BaseMetadata`) the path of a child of `base`, relative to `base` + :rtype: :class:`.WaterButlerPath` + """ + + # Read only providers should never call this function since they can't be a `dest_provider` + return parent_path.child(meta_data.name, folder=meta_data.is_folder) + def path_from_metadata(self, parent_path: wb_path.WaterButlerPath, meta_data: wb_metadata.BaseMetadata) -> wb_path.WaterButlerPath: diff --git a/waterbutler/providers/bitbucket/provider.py b/waterbutler/providers/bitbucket/provider.py index 1cc7a5130..71d7a6175 100644 --- a/waterbutler/providers/bitbucket/provider.py +++ b/waterbutler/providers/bitbucket/provider.py @@ -127,10 +127,16 @@ async def validate_path(self, path: str, **kwargs) -> BitbucketPath: return path_obj + async def construct_path(self, # type: ignore + parent_path: BitbucketPath, + meta_data) -> BitbucketPath: + + return self.path_from_metadata(parent_path, meta_data) + def path_from_metadata(self, # type: ignore parent_path: BitbucketPath, - metadata) -> BitbucketPath: - return parent_path.child(metadata.name, folder=metadata.is_folder) + meta_data) -> BitbucketPath: + return parent_path.child(meta_data.name, folder=meta_data.is_folder) async def metadata(self, path: BitbucketPath, **kwargs): # type: ignore """Get metadata about the requested file or folder. diff --git a/waterbutler/providers/box/provider.py b/waterbutler/providers/box/provider.py index a8b8f8096..b655e4963 100644 --- a/waterbutler/providers/box/provider.py +++ b/waterbutler/providers/box/provider.py @@ -9,10 +9,10 @@ from waterbutler.core import exceptions from waterbutler.core import path as wb_path from waterbutler.providers.box import settings -from waterbutler.providers.box.metadata import (BaseBoxMetadata, +from waterbutler.providers.box.metadata import (BoxRevision, + BaseBoxMetadata, BoxFileMetadata, - BoxFolderMetadata, - BoxRevision) + BoxFolderMetadata) class BoxProvider(provider.BaseProvider): @@ -142,6 +142,11 @@ async def validate_path(self, path: str, **kwargs) -> wb_path.WaterButlerPath: return ret + async def construct_path(self, + parent_path: wb_path.WaterButlerPath, + meta_data: BaseBoxMetadata) -> wb_path.WaterButlerPath: + return self.path_from_metadata(parent_path, meta_data) + async def revalidate_path(self, base: wb_path.WaterButlerPath, path: str, folder: bool=None) -> wb_path.WaterButlerPath: # TODO Research the search api endpoint diff --git a/waterbutler/providers/cloudfiles/provider.py b/waterbutler/providers/cloudfiles/provider.py index fa0248571..57f3d6787 100644 --- a/waterbutler/providers/cloudfiles/provider.py +++ b/waterbutler/providers/cloudfiles/provider.py @@ -14,9 +14,10 @@ from waterbutler.core.path import WaterButlerPath from waterbutler.providers.cloudfiles import settings -from waterbutler.providers.cloudfiles.metadata import CloudFilesFileMetadata -from waterbutler.providers.cloudfiles.metadata import CloudFilesFolderMetadata -from waterbutler.providers.cloudfiles.metadata import CloudFilesHeaderMetadata +from waterbutler.providers.cloudfiles.metadata import (BaseCloudFilesMetadata, + CloudFilesFileMetadata, + CloudFilesFolderMetadata, + CloudFilesHeaderMetadata) def ensure_connection(func): @@ -173,6 +174,11 @@ async def delete(self, path, **kwargs): ) await resp.release() + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: BaseCloudFilesMetadata) -> WaterButlerPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + @ensure_connection async def metadata(self, path, recursive=False, **kwargs): """Get Metadata about the requested file or folder diff --git a/waterbutler/providers/dataverse/provider.py b/waterbutler/providers/dataverse/provider.py index eddaed0b9..efb68d00a 100644 --- a/waterbutler/providers/dataverse/provider.py +++ b/waterbutler/providers/dataverse/provider.py @@ -5,8 +5,8 @@ from waterbutler.core import streams from waterbutler.core import provider from waterbutler.core import exceptions -from waterbutler.core.path import WaterButlerPath from waterbutler.core.utils import AsyncIterator +from waterbutler.core.path import WaterButlerPath from waterbutler.providers.dataverse import settings from waterbutler.providers.dataverse.metadata import DataverseRevision @@ -228,6 +228,13 @@ async def metadata(self, path, version=None, **kwargs): code=HTTPStatus.NOT_FOUND, ) + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data) -> WaterButlerPath: + # This still makes API calls. Was unsure if Dataverse even can copy/move folders + # Since I doubt it will hit this ever or often, it should be fine + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def revisions(self, path, **kwargs): """Get past versions of the request file. Orders versions based on `_get_all_data()` diff --git a/waterbutler/providers/dropbox/provider.py b/waterbutler/providers/dropbox/provider.py index 3353770f5..5f8b31dd0 100644 --- a/waterbutler/providers/dropbox/provider.py +++ b/waterbutler/providers/dropbox/provider.py @@ -290,6 +290,11 @@ async def delete(self, path: WaterButlerPath, confirm_delete: int=0, # type: ig throws=exceptions.DeleteError, ) + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: BaseDropboxMetadata) -> WaterButlerPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def metadata(self, # type: ignore path: WaterButlerPath, revision: str=None, diff --git a/waterbutler/providers/figshare/provider.py b/waterbutler/providers/figshare/provider.py index 67f16851b..4de169b21 100644 --- a/waterbutler/providers/figshare/provider.py +++ b/waterbutler/providers/figshare/provider.py @@ -456,6 +456,11 @@ async def validate_path(self, path, **kwargs): # Return for v0 folder creation return FigsharePath(path, _ids=('', ''), folder=True, is_public=False) + async def construct_path(self, + parent_path: FigsharePath, + meta_data: metadata.BaseFigshareMetadata) -> FigsharePath: + return self.path_from_metadata(parent_path, meta_data) + async def revalidate_path(self, parent_path, child_name, folder): """Look for file or folder named ``child_name`` under ``parent_path``. If it finds a match, it returns a FigsharePath object with the appropriate ids set. Otherwise, it returns a diff --git a/waterbutler/providers/filesystem/provider.py b/waterbutler/providers/filesystem/provider.py index c4139dedb..26a593465 100644 --- a/waterbutler/providers/filesystem/provider.py +++ b/waterbutler/providers/filesystem/provider.py @@ -9,8 +9,9 @@ from waterbutler.core.path import WaterButlerPath from waterbutler.providers.filesystem import settings -from waterbutler.providers.filesystem.metadata import FileSystemFileMetadata -from waterbutler.providers.filesystem.metadata import FileSystemFolderMetadata +from waterbutler.providers.filesystem.metadata import (FileSystemFileMetadata, + BaseFileSystemMetadata, + FileSystemFolderMetadata) class FileSystemProvider(provider.BaseProvider): @@ -86,6 +87,11 @@ async def delete(self, path, **kwargs): if path.is_root: os.makedirs(self.folder, exist_ok=True) + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: BaseFileSystemMetadata) -> WaterButlerPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def metadata(self, path, **kwargs): if path.is_dir: if not os.path.exists(path.full_path) or not os.path.isdir(path.full_path): diff --git a/waterbutler/providers/github/provider.py b/waterbutler/providers/github/provider.py index 0071bd7cc..c93b23380 100644 --- a/waterbutler/providers/github/provider.py +++ b/waterbutler/providers/github/provider.py @@ -10,12 +10,13 @@ from waterbutler.providers.github import settings from waterbutler.providers.github.path import GitHubPath -from waterbutler.providers.github.metadata import GitHubRevision -from waterbutler.providers.github.metadata import GitHubFileContentMetadata -from waterbutler.providers.github.metadata import GitHubFolderContentMetadata -from waterbutler.providers.github.metadata import GitHubFileTreeMetadata -from waterbutler.providers.github.metadata import GitHubFolderTreeMetadata from waterbutler.providers.github.exceptions import GitHubUnsupportedRepoError +from waterbutler.providers.github.metadata import (GitHubRevision, + BaseGitHubMetadata, + GitHubFileTreeMetadata, + GitHubFolderTreeMetadata, + GitHubFileContentMetadata, + GitHubFolderContentMetadata) GIT_EMPTY_SHA = '4b825dc642cb6eb9a060e54bf8d69288fbee4904' @@ -132,6 +133,11 @@ async def validate_path(self, path, **kwargs): return gh_path + async def construct_path(self, + parent_path: GitHubPath, + meta_data: BaseGitHubMetadata) -> GitHubPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def revalidate_path(self, base, path, folder=False): return base.child(path, _id=((base.branch_ref, None)), folder=folder) diff --git a/waterbutler/providers/googledrive/provider.py b/waterbutler/providers/googledrive/provider.py index 73712d601..ca94dada3 100644 --- a/waterbutler/providers/googledrive/provider.py +++ b/waterbutler/providers/googledrive/provider.py @@ -106,10 +106,15 @@ async def validate_path(self, path: str, **kwargs) -> GoogleDrivePath: names, ids = zip(*[(parse.quote(x['title'], safe=''), x['id']) for x in parts]) return GoogleDrivePath('/'.join(names), _ids=ids, folder='folder' in parts[-1]['mimeType']) + async def construct_path(self, + parent_path: wb_path.WaterButlerPath, + meta_data: BaseGoogleDriveMetadata) -> wb_path.WaterButlerPath: + return self.path_from_metadata(parent_path, meta_data) + async def revalidate_path(self, base: wb_path.WaterButlerPath, name: str, - folder: bool = None) -> wb_path.WaterButlerPath: + folder: bool=None) -> wb_path.WaterButlerPath: # TODO Redo the logic here folders names ending in /s # Will probably break if '/' in name.lstrip('/') and '%' not in name: diff --git a/waterbutler/providers/osfstorage/provider.py b/waterbutler/providers/osfstorage/provider.py index 3ec6f2a07..053f6e1a7 100644 --- a/waterbutler/providers/osfstorage/provider.py +++ b/waterbutler/providers/osfstorage/provider.py @@ -15,9 +15,10 @@ from waterbutler.providers.osfstorage import settings from waterbutler.providers.osfstorage.tasks import backup from waterbutler.providers.osfstorage.tasks import parity -from waterbutler.providers.osfstorage.metadata import OsfStorageFileMetadata -from waterbutler.providers.osfstorage.metadata import OsfStorageFolderMetadata -from waterbutler.providers.osfstorage.metadata import OsfStorageRevisionMetadata +from waterbutler.providers.osfstorage.metadata import (OsfStorageFileMetadata, + BaseOsfStorageMetadata, + OsfStorageFolderMetadata, + OsfStorageRevisionMetadata) QUERY_METHODS = ('GET', 'DELETE') @@ -432,6 +433,11 @@ async def metadata(self, path, **kwargs): return await self._item_metadata(path) return await self._children_metadata(path) + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: BaseOsfStorageMetadata) -> WaterButlerPath: + return self.path_from_metadata(parent_path, meta_data) + async def revisions(self, path, view_only=None, **kwargs): if path.identifier is None: raise exceptions.MetadataError('File not found', code=404) diff --git a/waterbutler/providers/owncloud/provider.py b/waterbutler/providers/owncloud/provider.py index 29cc2b10e..2bc4ab83f 100644 --- a/waterbutler/providers/owncloud/provider.py +++ b/waterbutler/providers/owncloud/provider.py @@ -6,7 +6,8 @@ from waterbutler.core.path import WaterButlerPath from waterbutler.providers.owncloud import utils -from waterbutler.providers.owncloud.metadata import OwnCloudFileRevisionMetadata +from waterbutler.providers.owncloud.metadata import (BaseOwnCloudMetadata, + OwnCloudFileRevisionMetadata) class OwnCloudProvider(provider.BaseProvider): @@ -201,6 +202,11 @@ async def delete(self, path, **kwargs): await delete_resp.release() return + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: BaseOwnCloudMetadata) -> WaterButlerPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def metadata(self, path, **kwargs): """Queries the remote host for metadata and returns metadata objects based on the return value. diff --git a/waterbutler/providers/s3/provider.py b/waterbutler/providers/s3/provider.py index 64463ac49..2b0d0f208 100644 --- a/waterbutler/providers/s3/provider.py +++ b/waterbutler/providers/s3/provider.py @@ -20,11 +20,12 @@ from waterbutler.core.path import WaterButlerPath from waterbutler.providers.s3 import settings -from waterbutler.providers.s3.metadata import S3Revision -from waterbutler.providers.s3.metadata import S3FileMetadata -from waterbutler.providers.s3.metadata import S3FolderMetadata -from waterbutler.providers.s3.metadata import S3FolderKeyMetadata -from waterbutler.providers.s3.metadata import S3FileMetadataHeaders +from waterbutler.providers.s3.metadata import (S3Metadata, + S3Revision, + S3FileMetadata, + S3FolderMetadata, + S3FolderKeyMetadata, + S3FileMetadataHeaders) class S3Provider(provider.BaseProvider): @@ -369,6 +370,11 @@ async def revisions(self, path, **kwargs): if item['Key'] == path.path ] + async def construct_path(self, + parent_path: WaterButlerPath, + meta_data: S3Metadata) -> WaterButlerPath: + return await self.revalidate_path(parent_path, meta_data.name, folder=meta_data.is_folder) + async def metadata(self, path, revision=None, **kwargs): """Get Metadata about the requested file or folder