Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pilot 4414: add a new logic to allow move/rename action to create non-existing parent folders #124

Merged
merged 5 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/commands/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,16 +521,26 @@ def file_metadata_download(**kwargs):
help=file_help.file_help_page(file_help.FileHELP.FILE_MOVE_Z),
show_default=False,
)
@click.option(
'-y',
'--yes',
default=False,
required=False,
is_flag=True,
help=file_help.file_help_page(file_help.FileHELP.FILE_MOVE_Y),
show_default=True,
)
@require_valid_token()
@doc(file_help.file_help_page(file_help.FileHELP.FILE_MOVE))
def file_move(**kwargs):
project_code = kwargs.get('project_code')
src_item_path = kwargs.get('src_item_path')
dest_item_path = kwargs.get('dest_item_path')
zone = kwargs.get('zone')
skip_confirm = kwargs.get('yes')

zone = get_zone(zone) if zone else AppConfig.Env.green_zone.lower()
file_meta_client = FileMoveClient(zone, project_code, src_item_path, dest_item_path)
file_meta_client = FileMoveClient(zone, project_code, src_item_path, dest_item_path, skip_confirm=skip_confirm)
file_meta_client.move_file()

message_handler.SrvOutPutHandler.move_action_success(src_item_path, dest_item_path)
1 change: 1 addition & 0 deletions app/resources/custom_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class HelpPage:
'FILE_META_T': 'The location of tag metadata file',
'FILE_MOVE': 'Move/Rename files/folders to a given Project path.',
'FILE_MOVE_Z': 'Target Zone (i.e., core/greenroom).',
'FILE_MOVE_Y': 'Skip the prompt confirmation and create non-existing folders.',
},
'config': {
'SET_CONFIG': 'Chose config file and set for cli.',
Expand Down
76 changes: 75 additions & 1 deletion app/services/file_manager/file_move/file_move_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@
#
# Contact Indoc Systems for any questions regarding the use of this source code.

import uuid
from sys import exit

import click
from click import Abort

import app.services.output_manager.message_handler as message_handler
from app.configs.app_config import AppConfig
from app.configs.user_config import UserConfig
from app.services.output_manager.error_handler import ECustomizedError
from app.services.output_manager.error_handler import SrvErrorHandler
from app.services.output_manager.error_handler import customized_error_msg
from app.services.user_authentication.decorator import require_valid_token
from app.utils.aggregated import check_item_duplication
from app.utils.aggregated import resilient_session
from app.utils.aggregated import search_item


class FileMoveClient:
Expand All @@ -21,6 +33,7 @@ def __init__(
project_code: str,
src_item_path: str,
dest_item_path: str,
skip_confirm: bool = False,
) -> None:
"""
Summary:
Expand All @@ -32,19 +45,80 @@ def __init__(
dest_item_path (str): destination item path.
"""

self.zone = zone
self.zone = {'greenroom': 0, 'core': 1}.get(zone)
self.project_code = project_code
self.src_item_path = src_item_path
self.dest_item_path = dest_item_path
self.skip_confirm = skip_confirm

self.user = UserConfig()

def create_object_path_if_not_exist(self, folder_path: str) -> dict:
"""Create object path is not on platfrom.

it will create every non-exist folder along path.
"""

path_list = folder_path.split('/')
# first check every folder in path exist or not
# the loop start with index 1 since we assume cli will not
# create any name folder or project folder
check_list = []
for index in range(1, len(path_list) - 1):
check_list.append('/'.join(path_list[: index + 1]))
if len(check_list) == 0:
return

# confirm if user want to create folder or not
exist_path = check_item_duplication(check_list, self.zone, self.project_code)
not_exist_path = sorted(set(check_list) - set(exist_path))
if not_exist_path:
try:
if not self.skip_confirm:
click.confirm(customized_error_msg(ECustomizedError.CREATE_FOLDER_IF_NOT_EXIST), abort=True)
except Abort:
message_handler.SrvOutPutHandler.move_cancelled()
exit(1)
else:
return

# get the current exist parent folder for reference
exist_parent = not_exist_path[0].rsplit('/', 1)[0]
exist_parent_item = search_item(self.project_code, self.zone, exist_parent).get('result')
exist_parent_id = exist_parent_item.get('id')
to_create = {'folders': [], 'parent_id': exist_parent_id}
for path in not_exist_path:
parent_path, folder_name = path.rsplit('/', 1)
current_item_id = str(uuid.uuid4())
to_create['folders'].append(
{
'name': folder_name,
'parent': exist_parent_id,
'parent_path': parent_path,
'container_code': self.project_code,
'container_type': 'project',
'zone': self.zone,
'item_id': current_item_id,
}
)
exist_parent_id = current_item_id

url = AppConfig.Connections.url_bff + '/v1/folders/batch'
headers = {'Authorization': 'Bearer ' + UserConfig().access_token}
response = resilient_session().post(url, json=to_create, headers=headers)
if response.status_code != 200:
SrvErrorHandler.default_handle(response.text, True)
return response.json().get('result')

@require_valid_token()
def move_file(self) -> None:
"""
Summary:
Move file.
"""

self.create_object_path_if_not_exist(self.dest_item_path)

try:
url = AppConfig.Connections.url_bff + f'/v1/{self.project_code}/files'
payload = {
Expand Down
1 change: 1 addition & 0 deletions app/services/output_manager/help_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ class FileHELP(enum.Enum):

FILE_MOVE = 'FILE_MOVE'
FILE_MOVE_Z = 'FILE_MOVE_Z'
FILE_MOVE_Y = 'FILE_MOVE_Y'


def file_help_page(FileHELP: FileHELP):
Expand Down
5 changes: 5 additions & 0 deletions app/services/output_manager/message_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,11 @@ def move_action_success(src, dest):
"""e.g. Move action succeed."""
return logger.succeed(f'Successfully moved {src} to {dest}')

@staticmethod
def move_cancelled():
"""e.g. Move action cancelled."""
return logger.warning('Move cancelled.')

@staticmethod
def move_action_failed(src, dest, error):
"""e.g. Move action failed."""
Expand Down
28 changes: 28 additions & 0 deletions app/utils/aggregated.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@ def get_file_info_by_geid(geid: list):
return res.json()['result']


@require_valid_token()
def check_item_duplication(item_list: List[str], zone: int, project_code: str) -> List[str]:
'''
Summary:
Check if the item already exists in the project in batch.
Parameters:
- item_list: list of item path to check
- zone: zone of the project
- project_code: project code
Returns:
- list of item path that already exists in the project
'''

url = AppConfig.Connections.url_base + '/portal/v1/files/exists'
headers = {'Authorization': 'Bearer ' + UserConfig().access_token}
payload = {
'locations': item_list,
'container_code': project_code,
'container_type': 'project',
'zone': zone,
}
response = resilient_session().post(url, json=payload, headers=headers)
if response.status_code != 200:
SrvErrorHandler.default_handle(response.text, True)

return response.json().get('result')


def fit_terminal_width(string_to_format):
string_to_format = string_to_format.rsplit('...')
current_len = 0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "app"
version = "2.9.7"
version = "2.9.8"
description = "This service is designed to support pilot platform"
authors = ["Indoc Systems"]

Expand Down
45 changes: 45 additions & 0 deletions tests/app/services/file_manager/file_move/test_file_move_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#
# Contact Indoc Systems for any questions regarding the use of this source code.

import pytest

from app.configs.app_config import AppConfig
from app.services.file_manager.file_move.file_move_client import FileMoveClient
from tests.conftest import decoded_token
Expand Down Expand Up @@ -71,3 +73,46 @@ def test_file_move_error_with_wrong_input_422(mocker, httpx_mock, capfd):
except SystemExit:
out, _ = capfd.readouterr()
assert out == 'Failed to move src_item_path to dest_item_path: \nerror_msg\n'


@pytest.mark.parametrize('skip_confirmation', [True, False])
def test_move_file_dest_parent_not_exist_success(mocker, httpx_mock, skip_confirmation):
project_code = 'test_code'
item_info = {'result': {'id': 'test_id', 'name': 'test_name'}}

mocker.patch(
'app.services.user_authentication.token_manager.SrvTokenManager.decode_access_token',
return_value=decoded_token(),
)

# mock duplicated item
mocker.patch(
'app.services.file_manager.file_move.file_move_client.check_item_duplication',
return_value=[],
)
mocker.patch(
'app.services.file_manager.file_move.file_move_client.search_item',
return_value={'result': {'id': 'test_id', 'name': 'test_name'}},
)
click_mocker = mocker.patch('app.services.file_manager.file_move.file_move_client.click.confirm', return_value=None)
httpx_mock.add_response(
url=AppConfig.Connections.url_bff + '/v1/folders/batch',
method='POST',
json={'result': []},
)

httpx_mock.add_response(
url=AppConfig.Connections.url_bff + f'/v1/{project_code}/files',
method='PATCH',
json={'result': item_info},
)

file_move_client = FileMoveClient(
'zone', project_code, 'src_item_path', 'dest_item_path/test_folder/test_file', skip_confirm=skip_confirmation
)
res = file_move_client.move_file()
assert res == item_info
if not skip_confirmation:
click_mocker.assert_called_once()
else:
click_mocker.assert_not_called()
22 changes: 22 additions & 0 deletions tests/app/utils/test_aggregated.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

import pytest

from app.configs.app_config import AppConfig
from app.utils.aggregated import check_item_duplication
from app.utils.aggregated import search_item
from tests.conftest import decoded_token

test_project_code = 'testproject'

Expand Down Expand Up @@ -100,3 +103,22 @@ def test_search_file_error_handling_with_401(requests_mock, mocker, capsys):
search_item(test_project_code, 'zone', 'folder_relative_path', 'project')
out, _ = capsys.readouterr()
assert out.rstrip() == 'Your login session has expired. Please try again or log in again.'


def test_check_duplicate_fail_with_error_code(httpx_mock, mocker, capsys):
mocker.patch(
'app.services.user_authentication.token_manager.SrvTokenManager.decode_access_token',
return_value=decoded_token(),
)

httpx_mock.add_response(
url=AppConfig.Connections.url_base + '/portal/v1/files/exists',
method='POST',
json={'error': 'internal server error'},
status_code=500,
)

with pytest.raises(SystemExit):
check_item_duplication(['test_path'], 0, 'test_project_code')
out, _ = capsys.readouterr()
assert out.rstrip() == '{"error": "internal server error"}'
Loading