idmtools-cli 0.0.0.dev0__py3-none-any.whl → 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
idmtools_cli/__init__.py CHANGED
@@ -1,8 +1,12 @@
1
- """
2
- idmtools-cli - Placeholder Package
3
-
4
- This is a placeholder package to reserve the name on PyPI.
5
- The actual package will be published later.
6
- """
7
-
8
- __version__ = "0.0.0.dev0"
1
+ """iidmtools_cli version definition file."""
2
+ try:
3
+ from importlib.metadata import version, PackageNotFoundError
4
+ except ImportError:
5
+ # Python < 3.8
6
+ from importlib_metadata import version, PackageNotFoundError
7
+
8
+ try:
9
+ __version__ = version("idmtools-cli") # Use your actual package name
10
+ except PackageNotFoundError:
11
+ # Package not installed, use fallback
12
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1 @@
1
+ """Package init file."""
@@ -0,0 +1,19 @@
1
+ [{
2
+ "name": "reproducible-science",
3
+ "url": "gh:mkrapp/cookiecutter-reproducible-science",
4
+ "description": "A boilerplate for reproducible and transparent science with close resemblances to the philosophy of Cookiecutter Data Science: A logical, reasonably standardized, but flexible project structure for doing and sharing data science work.",
5
+ "info": "https://github.com/mkrapp/cookiecutter-reproducible-science"
6
+ },
7
+ {
8
+ "name": "docker-science",
9
+ "url": "git@github.com:docker-science/cookiecutter-docker-science.git",
10
+ "description": "This project is a tiny template for machine learning projects developed in Docker environments. In machine learning tasks, projects glow uniquely to fit target tasks, but in the initial state, most directory structure and targets in Makefile are common. Cookiecutter Docker Science generates initial directories which fits simple machine learning tasks.",
11
+ "info": "https://docker-science.github.io/"
12
+ },
13
+ {
14
+ "name": "data-science",
15
+ "url": "https://github.com/drivendata/cookiecutter-data-science",
16
+ "description": "A logical, reasonably standardized, but flexible project structure for doing and sharing data science work.",
17
+ "info": "https://docker-science.github.io/"
18
+ }
19
+ ]
@@ -0,0 +1,199 @@
1
+ """Defines the config cli group and commands."""
2
+ import configparser
3
+ import dataclasses
4
+ import os
5
+ import re
6
+ import click
7
+ from click import secho
8
+ from colorama import Fore, Style
9
+ from idmtools.registry.platform_specification import PlatformPlugins
10
+ from idmtools_cli.cli.entrypoint import cli
11
+
12
+ IGNORED_PLATFORMS = ["Test", "Slurm", "File", "Container", "SSMT", "TestExecute", "Process"]
13
+ AVAILABLE_PLATFORMS = PlatformPlugins().get_plugin_map()
14
+ for platform in IGNORED_PLATFORMS:
15
+ if platform in AVAILABLE_PLATFORMS:
16
+ del AVAILABLE_PLATFORMS[platform]
17
+ HIDDEN_FIELD_REGEX = re.compile('^_.+$')
18
+ FIELD_BLACKLIST = ['platform_type_map', 'supported_types', 'plugin_key', 'docker_image']
19
+
20
+
21
+ @cli.group()
22
+ @click.option("--config_path", prompt="Path to the idmtools.ini file",
23
+ help="Path to the idmtools.ini file",
24
+ default=os.path.join(os.getcwd(), "idmtools.ini"),
25
+ type=click.Path(dir_okay=False, file_okay=True, exists=False, writable=True, resolve_path=True))
26
+ @click.option("--global-config/--no-global-config", default=False, help="Allow generating config in the platform default global location")
27
+ @click.pass_context
28
+ def config(ctx, config_path, global_config):
29
+ """
30
+ Contains commands related to the creation of idmtools.ini.
31
+
32
+ With the config command, you can :
33
+ - Generate an idmtools.ini file in the current directory
34
+ - Add a configuration block
35
+ """
36
+ ctx.ensure_object(dict)
37
+ if global_config:
38
+ from idmtools import IdmConfigParser
39
+ config_path = IdmConfigParser.get_global_configuration_name()
40
+
41
+ # Create a config parser and read the file if it exist
42
+ # The comment prefixes and allow_no_value is a truck to keep the comments even while editing
43
+ cp = configparser.ConfigParser(comment_prefixes='/', allow_no_value=True)
44
+ if os.path.exists(config_path):
45
+ cp.read_file(open(config_path))
46
+
47
+ # Store the config parser in the context
48
+ ctx.obj["cp"] = cp
49
+ ctx.obj["path"] = config_path
50
+
51
+
52
+ def slugify(value):
53
+ """
54
+ Slugify the option.
55
+
56
+ This means upper-casing and replacing spaces with "-"
57
+
58
+ Args:
59
+ value: Item to slugify
60
+
61
+ Returns:
62
+ Slugified string
63
+ """
64
+ value = value.upper()
65
+ value = value.replace(" ", "_")
66
+ return value
67
+
68
+
69
+ def validate_block_name(context, value):
70
+ """
71
+ Validate if a block name exists, and if so, should we overwrite it.
72
+
73
+ Args:
74
+ context: Context object
75
+ value: Value to check
76
+
77
+ Returns:
78
+ Slugified value name
79
+ """
80
+ cp = context.obj["cp"]
81
+ value = slugify(value)
82
+ if value in cp.sections():
83
+ secho(f"The {value} block already exists in the selected ini file.", fg="bright_yellow")
84
+ click.confirm(click.style("Do you want to continue and overwrite the existing block?", fg="bright_yellow"), default=False, abort=True)
85
+
86
+ # Remove the block from the config parser
87
+ del cp[value]
88
+
89
+ context.obj['cp'] = cp
90
+
91
+ return value
92
+
93
+
94
+ @config.command()
95
+ @click.option("--block_name", prompt="New block name",
96
+ help="Name of the new block in the file",
97
+ callback=lambda c, p, v: validate_block_name(c, v),
98
+ type=click.STRING)
99
+ @click.option('--platform', default='COMPS', type=click.Choice(AVAILABLE_PLATFORMS.keys()), prompt="Platform type")
100
+ @click.pass_context
101
+ def block(ctx, block_name, platform):
102
+ """
103
+ Command to create/replace a block in the selected idmtools.ini.
104
+
105
+ Args:
106
+ ctx: Context containing the path of idmtools.ini and the associated configparser
107
+ block_name: Name of the block to create/replace
108
+ platform: Selected platform
109
+ """
110
+ config_path = ctx.obj['path']
111
+ print("\n" + Style.BRIGHT + "-" * 50)
112
+ print("idmtools.ini Utility")
113
+ print(f"- INI Location: {config_path}")
114
+ print(f"- Selected block: {block_name}")
115
+ print(f"- Selected platform: {platform}")
116
+ print("-" * 50 + Style.NORMAL + "\n")
117
+
118
+ # Retrieve the platform and its associated fields
119
+ platform_obj = AVAILABLE_PLATFORMS[platform]
120
+ fields = dataclasses.fields(platform_obj.get_type())
121
+
122
+ # Dictionary to store user choices and field defaults
123
+ # Store both to allow the fields callback functions to access the previous user choices regardless of defaults
124
+ values = {"type": platform}
125
+ defaults = {}
126
+
127
+ # Ask about each field
128
+ # The field needs to contain a `help` section in the metadata to be considered
129
+ for field in filter(lambda f: "help" in f.metadata, fields):
130
+
131
+ # Display the help message
132
+ print(f"{Fore.CYAN}{field.metadata['help']}{Fore.RESET}")
133
+
134
+ # Retrieve the metadata
135
+ md = dict(field.metadata)
136
+
137
+ # If a callback exists -> execute it
138
+ if "callback" in md:
139
+ md.update(md["callback"](values, field))
140
+
141
+ # Create the default
142
+ field_default = md.get("default", field.default if field.default is not None else '')
143
+ defaults[field.name] = field.default
144
+
145
+ # Handle the choices if any
146
+ prompt_type = click.Choice(md["choices"]) if "choices" in md else field.type
147
+
148
+ # Retrieve the validation function if any
149
+ if "validate" in md:
150
+ validation = md["validate"]
151
+ else:
152
+ validation = lambda v: (True, None) # noqa: E731
153
+
154
+ # Prompt the user
155
+ while True:
156
+ user_input = click.prompt(field.name, type=prompt_type, default=field_default, prompt_suffix=f": {Fore.GREEN}")
157
+
158
+ # Call the validation
159
+ result, msg = validation(user_input)
160
+
161
+ # If positive, get out
162
+ if result:
163
+ break
164
+
165
+ # Else display the error message
166
+ secho(msg, fg="bright_red")
167
+
168
+ # Store the value
169
+ values[field.name] = user_input if user_input != "" else None
170
+ print(Fore.RESET)
171
+
172
+ # Remove the default values from the values
173
+ for k, d in defaults.items():
174
+ if values[k] == d:
175
+ del values[k]
176
+
177
+ # Display a validation prompt
178
+ print("The following block will be added to the file:\n")
179
+ longest_param = max(len(p) for p in values)
180
+ block_parameters = "\n".join(f"{param.ljust(longest_param)} = {value}" for param, value in values.items())
181
+ block_headers = f"[{block_name}]"
182
+ block = block_headers + "\n" + block_parameters
183
+ secho(f"{block}\n", fg="bright_blue")
184
+
185
+ # If we decide to go ahead -> write to file
186
+ if click.confirm("Do you want to write this block to the file?", default=True):
187
+ # First re-write the content of the config parser
188
+ cp = ctx.obj["cp"]
189
+ with open(config_path, 'w') as fp:
190
+ cp.write(fp)
191
+ fp.writelines("\n" + block)
192
+
193
+ secho("Block written successfully!", fg="bright_green")
194
+ else:
195
+ secho("Aborted...", fg="bright_red")
196
+
197
+
198
+ if __name__ == '__main__':
199
+ config(["block", '--block_name', 'test'])
@@ -0,0 +1,45 @@
1
+ """Base click group definition."""
2
+ import logging
3
+ from idmtools import IdmConfigParser
4
+ from idmtools.core.logging import setup_logging, IdmToolsLoggingConfig
5
+ import click
6
+ from click_plugins import with_plugins
7
+
8
+ try:
9
+ from importlib.metadata import entry_points
10
+ except ImportError:
11
+ from importlib_metadata import entry_points # for python 3.7
12
+ from idmtools_cli.iplatform_cli import IPlatformCLI
13
+
14
+ # Decorator for CLI functions that will require a platform object passed down to them
15
+ pass_platform_cli = click.make_pass_decorator(IPlatformCLI)
16
+
17
+
18
+ def get_filtered_entry_points(group):
19
+ """
20
+ Get entry points for a specific group, compatible across Python versions.
21
+
22
+ Args:
23
+ group (str): The entry point group to filter by.
24
+
25
+ Returns:
26
+ An iterable of entry point objects for the specified group.
27
+ """
28
+ user_entry_points = entry_points()
29
+ # For Python 3.10 and newer, use the select method if available
30
+ if hasattr(user_entry_points, 'select'):
31
+ return user_entry_points.select(group=group)
32
+ else:
33
+ # For Python 3.9 and earlier, manually filter the entry points
34
+ return (ep for ep in user_entry_points.get(group, []))
35
+
36
+
37
+ @with_plugins(get_filtered_entry_points('idmtools_cli.cli_plugins'))
38
+ @click.group()
39
+ @click.option('--debug/--no-debug', default=False, help="When selected, enables console level logging")
40
+ def cli(debug):
41
+ """Allows you to perform multiple idmtools commands."""
42
+ IdmConfigParser()
43
+ # init config by just calling config parser
44
+ if debug:
45
+ setup_logging(IdmToolsLoggingConfig(console=True, level=logging.DEBUG, force=True))
@@ -0,0 +1,393 @@
1
+ """Define the gitrepo group cli command."""
2
+ import os
3
+ import json
4
+ from logging import getLogger
5
+ import click
6
+ from click import secho
7
+ from colorama import Fore
8
+ from typing import Optional, List
9
+
10
+ from idmtools.core.logging import SUCCESS
11
+ from idmtools_cli.cli.entrypoint import cli
12
+ from idmtools.utils.gitrepo import GitRepo, REPO_OWNER, GITHUB_HOME, REPO_NAME
13
+ from idmtools.registry.master_plugin_registry import MasterPluginRegistry
14
+
15
+ user_logger = getLogger('user')
16
+
17
+
18
+ @cli.group(short_help="Contains commands related to examples download.")
19
+ def gitrepo():
20
+ """Contains command to download examples."""
21
+ pass
22
+
23
+
24
+ @gitrepo.command()
25
+ @click.option('--raw', default=False, type=bool, help="Files in detail")
26
+ def view(raw: Optional[bool]):
27
+ """
28
+ Display all idmtools available examples.
29
+
30
+ Args:
31
+ raw: True/False - display results in details or simplified format
32
+ """
33
+ list_examples(raw)
34
+
35
+
36
+ def list_examples(raw: bool):
37
+ """
38
+ Display all idmtools available examples.
39
+
40
+ Args:
41
+ raw: True/False - display results in details or simplified format
42
+ """
43
+ examples = get_plugins_examples()
44
+ if raw:
45
+ user_logger.info(json.dumps(examples, indent=3))
46
+ exit(0)
47
+ for plugin, urls in examples.items():
48
+ user_logger.info(f'\n{plugin}')
49
+ urls = [urls] if isinstance(urls, str) else urls
50
+ url_list = [f' - {url}' for url in urls]
51
+ user_logger.info('\n'.join(url_list))
52
+
53
+
54
+ # alias under examples
55
+ @cli.group(help="Display a list of examples organized by plugin type.")
56
+ def examples():
57
+ """Examples group command. alias for gitrepo command."""
58
+ pass
59
+
60
+
61
+ @examples.command(name='list', help="List examples available")
62
+ @click.option('--raw', default=False, type=bool, help="Files in detail")
63
+ def list_m(raw: Optional[bool]):
64
+ """Alternate path for gitrepo."""
65
+ list_examples(raw)
66
+
67
+
68
+ @gitrepo.command()
69
+ @click.option('--owner', default=REPO_OWNER, help="Repo owner")
70
+ @click.option('--page', default=1, help="Pagination")
71
+ def repos(owner: Optional[str], page: Optional[int]):
72
+ """
73
+ Display all public repos of the owner.
74
+
75
+ Args:
76
+ owner: Repo owner
77
+ page: Result pagination
78
+ """
79
+ gr = GitRepo(owner)
80
+ try:
81
+ repos = gr.list_public_repos(page=page)
82
+ except Exception as ex:
83
+ secho(str(ex), fg="yellow")
84
+ exit(1)
85
+ repos_full = [f' - {GITHUB_HOME}/{r}' for r in repos]
86
+ user_logger.log(SUCCESS, f"GitHub Owner: {gr.repo_owner}")
87
+ user_logger.info('\n'.join(repos_full))
88
+
89
+
90
+ @gitrepo.command()
91
+ @click.option('--owner', default=REPO_OWNER, help="Repo owner")
92
+ @click.option('--repo', default=REPO_NAME, help="Repo name")
93
+ def releases(owner: Optional[str], repo: Optional[str]):
94
+ """
95
+ Display all the releases of the repo.
96
+
97
+ Args:
98
+ owner: Repo owner
99
+ repo: Repo name
100
+ """
101
+ gr = GitRepo(owner, repo)
102
+ try:
103
+ rels = gr.list_repo_releases()
104
+ except Exception as ex:
105
+ secho(str(ex), fg="yellow")
106
+ exit(1)
107
+ rels_list = [f' - {r}' for r in rels]
108
+ secho(f'The Repo: {gr.repo_home_url}', fg="green")
109
+ user_logger.info('\n'.join(rels_list))
110
+
111
+
112
+ @gitrepo.command()
113
+ @click.option('--url', required=True, help="Repo files url")
114
+ @click.option('--raw', default=False, type=bool, help="Display files in detail")
115
+ def peep(url: Optional[str], raw: Optional[bool]):
116
+ """
117
+ Display all current files/dirs of the repo folder (not recursive).
118
+
119
+ Args:
120
+ url: GitHub repo files url (required)
121
+ raw: Display details or not
122
+ """
123
+ user_logger.info(f'Peep: {url}')
124
+ user_logger.info('Processing...')
125
+ try:
126
+ result = GitRepo().peep(url)
127
+ except Exception as ex:
128
+ secho(f'Failed to access: {url}', fg="yellow")
129
+ user_logger.error(ex)
130
+ exit(1)
131
+
132
+ secho(f"Item Count: {len(result)}", fg="green")
133
+ if raw:
134
+ user_logger.info(json.dumps(result, indent=3))
135
+ exit(0)
136
+
137
+ for file in result:
138
+ if file['type'] == 'dir':
139
+ secho(f" - {file['name']}", fg="yellow")
140
+ else:
141
+ secho(f" - {file['name']}")
142
+
143
+
144
+ @gitrepo.command()
145
+ @click.option('--type', default=None, multiple=True, help="Download examples by type (COMPSPlatform, PythonTask, etc)")
146
+ @click.option('--url', default=None, multiple=True, help="Repo files url")
147
+ @click.option('--output', default='./', help="Files download destination")
148
+ def download(type: Optional[str], url: Optional[str], output: Optional[str]):
149
+ """
150
+ Download files from GitHub repo to user location.
151
+
152
+ Args:
153
+ type: Object type (COMPSPlatform, PythonTask, etc)
154
+ url: GitHub repo files url
155
+ output: Local folder
156
+
157
+ Returns: Files download count
158
+ """
159
+ download_github_repo(output, url, example_types=type)
160
+
161
+
162
+ @examples.command(name='download')
163
+ @click.option('--type', default=None, multiple=True, help="Download examples by type (COMPSPlatform, PythonTask, etc)")
164
+ @click.option('--url', default=None, multiple=True, help="Repo files url")
165
+ @click.option('--output', default='./', help="Files download destination")
166
+ def download_alias(type: Optional[str], url: Optional[List[str]], output: Optional[str]):
167
+ """
168
+ Download examples from specified location.
169
+
170
+ Args:
171
+ type: Object type (COMPSPlatform, PythonTask, etc)
172
+ url: GitHub repo files url
173
+ output: Local folder
174
+
175
+ Returns: Files download count
176
+ """
177
+ download_github_repo(output, url, example_types=list(type))
178
+
179
+
180
+ def download_github_repo(output, urls: List[str], example_types: List[str] = None):
181
+ """
182
+ Download github repo.
183
+
184
+ Args:
185
+ output: Output folder
186
+ urls: Urls to download
187
+ example_types: List of example types to download
188
+
189
+ Returns:
190
+ None
191
+ """
192
+ total = 0
193
+ if example_types:
194
+ urls = list(urls)
195
+ urls.extend(get_examples_by_types(list(example_types)))
196
+ urls = list(filter(None, urls)) if urls else None
197
+ option, file_dict = choice(urls)
198
+ secho(f"This is your selection: {option}", fg="bright_blue")
199
+ # If we decide to go ahead -> write to file
200
+ if click.confirm("Do you want to go ahead to download files?", default=True):
201
+ simplified_option, duplicated = remove_duplicated_files(option, file_dict)
202
+ secho(f'Removed duplicated files: {duplicated}', fg="bright_red")
203
+ for i in simplified_option:
204
+ total += download_file(i, file_dict[i], output)
205
+
206
+ secho(f"Total Files: {total}", fg="yellow")
207
+ secho("Download successfully!", fg="bright_green")
208
+ else:
209
+ secho("Aborted...", fg="bright_red")
210
+
211
+
212
+ def get_examples_by_types(example_types: List[str]) -> List[str]:
213
+ """
214
+ Get examples from Plugins.
215
+
216
+ Args:
217
+ example_types: List of types(plugins) to pull examples from
218
+
219
+ Returns:
220
+ List of strings to download
221
+ """
222
+ items = get_plugins_examples()
223
+ result = []
224
+ for example in example_types:
225
+ if example in items:
226
+ result.extend(items[example])
227
+ else:
228
+ user_logger.warning(f"Cannot find example type {example}")
229
+ return result
230
+
231
+
232
+ def download_file(option: int, url: str, output: str):
233
+ """
234
+ Use GitRepo utility to download files.
235
+
236
+ Args:
237
+ option: file index
238
+ url: file url
239
+ output: local folder to save files
240
+
241
+ Returns:
242
+ file count
243
+ """
244
+ # Display file information
245
+ click.echo(f"\nDownloading Files {option if option else ''}: '{url}'")
246
+ click.echo(f'Local Folder: {os.path.abspath(output)}')
247
+ secho('Processing...')
248
+
249
+ # Start to download files
250
+ gr = GitRepo()
251
+ total = gr.download(path=url, output_dir=output)
252
+ return total
253
+
254
+
255
+ def get_plugins_examples():
256
+ """
257
+ Collect all idmtools files.
258
+
259
+ Returns:
260
+ files urls as dict
261
+
262
+ Notes:
263
+ test_examples = {
264
+ 'TestA': 'https://github.com/dustin/py-github/tree/main/github/data',
265
+ 'TestB': 'https://github.com/dustin/py-github/tree/main/github',
266
+ 'TestC': 'https://github.com/dustin/py-github/tree/main/github/__init__.py',
267
+ 'TestD': ['https://github.com/dustin/py-github/tree/main/github',
268
+ 'https://github.com/dustin/py-github/tree/main/github/data']
269
+ }
270
+ """
271
+ # Collect all idmtools examples
272
+ plugin_map = MasterPluginRegistry().get_plugin_map()
273
+
274
+ example_plugins = {}
275
+ for spec_name, plugin in plugin_map.items():
276
+ try:
277
+ plugin_url_list = plugin.get_example_urls()
278
+ if len(plugin_url_list) > 0:
279
+ example_plugins[spec_name] = plugin_url_list
280
+ except Exception as ex:
281
+ user_logger.error(ex)
282
+
283
+ return example_plugins
284
+
285
+
286
+ def choice(urls: list = None):
287
+ """
288
+ Take urls as user selection or prompt user for file selections.
289
+
290
+ Args:
291
+ urls: user provided files
292
+
293
+ Returns: True/False and results (List)
294
+ """
295
+ if urls is None:
296
+ files = get_plugins_examples()
297
+
298
+ # Collect all files and remove duplicates
299
+ url_list = []
300
+ for exp_urls in files.values():
301
+ exp_urls = [exp_urls] if isinstance(exp_urls, str) else exp_urls
302
+ url_list.extend(list(map(str.lower, exp_urls)))
303
+ else:
304
+ url_list = urls
305
+
306
+ # Remove duplicates
307
+ url_list = list(set(url_list))
308
+ # Soring urls
309
+ url_list = sorted(url_list, reverse=False)
310
+
311
+ # Provide index to each file
312
+ file_dict = {}
313
+ for i in range(len(url_list)):
314
+ file_dict[i + 1] = url_list[i]
315
+
316
+ # Pre-view files for user to select
317
+ file_list = [f' {i}. {url}' for i, url in file_dict.items()]
318
+ user_logger.info('File List:')
319
+ user_logger.info('\n'.join(file_list))
320
+
321
+ if urls:
322
+ # Return without user prompt for selection
323
+ return ['all'], file_dict
324
+
325
+ # Make sure user makes correct selection
326
+ choice_set = set(range(1, len(url_list) + 1))
327
+ choice_set.add('all')
328
+ while True:
329
+ user_input = click.prompt(
330
+ f"\nSelect files (multiple) for download (all or 1-{len(url_list)} separated by space)", type=str,
331
+ default='all',
332
+ prompt_suffix=f": {Fore.GREEN}")
333
+ valid, result = validate(user_input, choice_set)
334
+
335
+ if valid:
336
+ user_input = sorted(result, reverse=False)
337
+ break
338
+
339
+ # Else display the error message
340
+ secho(f'This is not correct choice: {result}', fg="bright_red")
341
+
342
+ # Return user selection and indexed files
343
+ return user_input, file_dict
344
+
345
+
346
+ def validate(user_input: object, choice_set: set):
347
+ """
348
+ Validate user_input against num_set.
349
+
350
+ Args:
351
+ user_input: user input
352
+ choice_set: test against this set
353
+
354
+ Returns: True/False and result (List)
355
+ """
356
+ # Normalize user selection
357
+ selection = user_input.lower().strip().split(' ')
358
+ selection = list(filter(None, selection))
359
+ selection = [int(a) if a.isdigit() else a for a in selection]
360
+
361
+ # Find difference
362
+ extra = set(selection) - choice_set
363
+
364
+ # Return True/False along with selection details
365
+ if len(extra) == 0 and len(selection) > 0:
366
+ if 'all' in selection:
367
+ selection = ['all']
368
+ return True, selection
369
+ else:
370
+ return False, list(extra)
371
+
372
+
373
+ def remove_duplicated_files(user_selected: list, file_dict: dict):
374
+ """
375
+ Removed duplicated files.
376
+
377
+ Args:
378
+ user_selected: user selection
379
+ file_dict: all files
380
+
381
+ Returns: simplified selection, duplicated selection
382
+ """
383
+ if 'all' in user_selected:
384
+ user_selected = range(1, len(file_dict) + 1)
385
+
386
+ duplicated_selection = []
387
+ for i in range(len(user_selected)):
388
+ pre = [] if i == 0 else user_selected[0:i]
389
+ if any([file_dict[user_selected[i]].startswith(file_dict[j]) for j in pre]):
390
+ duplicated_selection.append(user_selected[i])
391
+
392
+ simplified_selection = [i for i in user_selected if i not in duplicated_selection]
393
+ return simplified_selection, duplicated_selection