datatrail-cli 0.4.2__tar.gz → 0.4.4__tar.gz
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.
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/PKG-INFO +2 -3
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/clear.py +17 -12
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/cli.py +15 -1
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/config.py +19 -5
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/ls.py +22 -12
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/ps.py +92 -66
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/pull.py +23 -19
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/src/functions.py +45 -64
- datatrail_cli-0.4.4/dtcli/utilities/utilities.py +113 -0
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/pyproject.toml +3 -2
- datatrail_cli-0.4.2/dtcli/utilities/utilities.py +0 -61
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/LICENSE +0 -0
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/README.md +0 -0
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/__init__.py +0 -0
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/config.yaml +0 -0
- {datatrail_cli-0.4.2 → datatrail_cli-0.4.4}/dtcli/utilities/cadcclient.py +0 -0
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datatrail-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: CHIME/FRB Datatrail CLI
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: CHIME FRB Project Office
|
|
7
|
-
Requires-Python: >=3.8,<4.0
|
|
7
|
+
Requires-Python: >=3.8.1,<4.0.0
|
|
8
8
|
Classifier: License :: OSI Approved :: MIT License
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.9
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -9,14 +9,18 @@ from rich.prompt import Confirm
|
|
|
9
9
|
|
|
10
10
|
from dtcli.config import procure
|
|
11
11
|
from dtcli.src.functions import clear_dataset_path, find_dataset_common_path
|
|
12
|
-
from dtcli.utilities.utilities import validate_scope
|
|
12
|
+
from dtcli.utilities.utilities import set_log_level, validate_scope
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger("clear")
|
|
15
15
|
|
|
16
16
|
console = Console()
|
|
17
|
+
error_console = Console(stderr=True, style="bold red")
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@click.command(
|
|
20
|
+
@click.command(
|
|
21
|
+
name="clear",
|
|
22
|
+
help="""Clear a dataset.""",
|
|
23
|
+
)
|
|
20
24
|
@click.argument("scope", type=click.STRING, required=True, nargs=1)
|
|
21
25
|
@click.argument("dataset", type=click.STRING, required=True, nargs=1)
|
|
22
26
|
@click.option(
|
|
@@ -26,7 +30,7 @@ console = Console()
|
|
|
26
30
|
exists=True, file_okay=False, dir_okay=True, writable=True, resolve_path=True
|
|
27
31
|
),
|
|
28
32
|
default=None,
|
|
29
|
-
help="
|
|
33
|
+
help="Root directory to use. Default: None, will use the value set in the config.",
|
|
30
34
|
)
|
|
31
35
|
@click.option(
|
|
32
36
|
"--clear-parents",
|
|
@@ -35,8 +39,10 @@ console = Console()
|
|
|
35
39
|
)
|
|
36
40
|
@click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
|
|
37
41
|
@click.option("-q", "--quiet", is_flag=True, help="Set log level to ERROR.")
|
|
38
|
-
@click.option("--force", "-f", is_flag=True, help="
|
|
42
|
+
@click.option("--force", "-f", is_flag=True, help="Will not prompt for confirmation.")
|
|
43
|
+
@click.pass_context
|
|
39
44
|
def clear(
|
|
45
|
+
ctx: click.Context,
|
|
40
46
|
scope: str,
|
|
41
47
|
dataset: str,
|
|
42
48
|
directory: str,
|
|
@@ -48,6 +54,7 @@ def clear(
|
|
|
48
54
|
"""Clear a dataset.
|
|
49
55
|
|
|
50
56
|
Args:
|
|
57
|
+
ctx (click.Context): Click context.
|
|
51
58
|
scope (str): Scope of dataset.
|
|
52
59
|
dataset (str): Name of dataset.
|
|
53
60
|
directory (str): Directory to clear data from.
|
|
@@ -56,13 +63,8 @@ def clear(
|
|
|
56
63
|
quiet (bool): Minimal logging.
|
|
57
64
|
force (bool): Automatically download files.
|
|
58
65
|
"""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
logger.setLevel("INFO")
|
|
62
|
-
elif verbose > 1:
|
|
63
|
-
logger.setLevel("DEBUG")
|
|
64
|
-
elif quiet:
|
|
65
|
-
logger.setLevel("ERROR")
|
|
66
|
+
# Set logging level.
|
|
67
|
+
set_log_level(logger, verbose, quiet)
|
|
66
68
|
logger.debug("`clear` called with:")
|
|
67
69
|
logger.debug(f"scope: {scope} [{type(scope)}]")
|
|
68
70
|
logger.debug(f"dataset: {dataset} [{type(dataset)}]")
|
|
@@ -90,7 +92,10 @@ def clear(
|
|
|
90
92
|
raise RuntimeError("Clear command not permitted at Chime or Outriggers!")
|
|
91
93
|
|
|
92
94
|
if not validate_scope(scope):
|
|
93
|
-
|
|
95
|
+
error_console.print("Scope does not exist!")
|
|
96
|
+
console.print("Valid scopes are:")
|
|
97
|
+
ctx.invoke(list)
|
|
98
|
+
return None
|
|
94
99
|
|
|
95
100
|
# Find number of files in common directory and size.
|
|
96
101
|
console.print(f"\nSearching for files for {dataset} {scope}...\n")
|
|
@@ -6,6 +6,7 @@ from pkg_resources import get_distribution
|
|
|
6
6
|
from rich import console, pretty
|
|
7
7
|
|
|
8
8
|
from dtcli import clear, config, ls, ps, pull
|
|
9
|
+
from dtcli.utilities import utilities
|
|
9
10
|
|
|
10
11
|
pretty.install()
|
|
11
12
|
terminal = console.Console()
|
|
@@ -15,7 +16,7 @@ terminal = console.Console()
|
|
|
15
16
|
@click.group(cls=ClickAliasedGroup)
|
|
16
17
|
def cli():
|
|
17
18
|
"""Datatrail Command Line Interface."""
|
|
18
|
-
|
|
19
|
+
check_version()
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@cli.command(name="version", help="Show versions.")
|
|
@@ -41,5 +42,18 @@ cli.add_command(pull.pull)
|
|
|
41
42
|
cli.add_command(clear.clear)
|
|
42
43
|
cli.add_command(config.config)
|
|
43
44
|
|
|
45
|
+
|
|
46
|
+
def check_version() -> None:
|
|
47
|
+
"""Check if CLI is latest release."""
|
|
48
|
+
if not utilities.cli_is_latest_release():
|
|
49
|
+
current_version = get_distribution("datatrail-cli").version
|
|
50
|
+
latest_version = utilities.get_latest_released_version()
|
|
51
|
+
terminal.print(
|
|
52
|
+
f"A new release of datatrail-cli is available: {current_version} → {latest_version}", # noqa: E501
|
|
53
|
+
style="bold yellow",
|
|
54
|
+
)
|
|
55
|
+
terminal.print()
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
if __name__ == "__main__":
|
|
45
59
|
cli()
|
|
@@ -17,7 +17,10 @@ CONFIG: Path = Path.home() / ".datatrail" / "config.yaml"
|
|
|
17
17
|
|
|
18
18
|
@click.group(cls=ClickAliasedGroup)
|
|
19
19
|
def config():
|
|
20
|
-
"""Datatrail CLI Configuration.
|
|
20
|
+
"""Datatrail CLI Configuration.
|
|
21
|
+
|
|
22
|
+
For initialising and modifying the Datatrail CLI configuration file.
|
|
23
|
+
"""
|
|
21
24
|
pass
|
|
22
25
|
|
|
23
26
|
|
|
@@ -68,15 +71,26 @@ def list():
|
|
|
68
71
|
print(exc)
|
|
69
72
|
|
|
70
73
|
|
|
71
|
-
@config.command(
|
|
74
|
+
@config.command(
|
|
75
|
+
name="init",
|
|
76
|
+
help="""Initialise configuration.
|
|
77
|
+
|
|
78
|
+
This will create a configuration file in the home directory under `.datatrail`.
|
|
79
|
+
Datatrail MUST be initialised before is can be used.
|
|
80
|
+
""",
|
|
81
|
+
)
|
|
72
82
|
@click.option(
|
|
73
|
-
"--site",
|
|
83
|
+
"--site",
|
|
84
|
+
"-s",
|
|
85
|
+
type=click.STRING,
|
|
86
|
+
help="Site to initialise Datatrail CLI for.",
|
|
87
|
+
required=True,
|
|
74
88
|
)
|
|
75
89
|
def init(site: str):
|
|
76
|
-
"""
|
|
90
|
+
"""Initialise configuration.
|
|
77
91
|
|
|
78
92
|
Args:
|
|
79
|
-
site (str): Site to
|
|
93
|
+
site (str): Site to initialise.
|
|
80
94
|
"""
|
|
81
95
|
# Default configuration.
|
|
82
96
|
defaults: Dict[str, Any] = {
|
|
@@ -9,11 +9,12 @@ from rich.console import Console
|
|
|
9
9
|
from rich.table import Table
|
|
10
10
|
|
|
11
11
|
from dtcli.src import functions
|
|
12
|
-
from dtcli.utilities.utilities import validate_scope
|
|
12
|
+
from dtcli.utilities.utilities import set_log_level, validate_scope
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger("ls")
|
|
15
15
|
|
|
16
16
|
console = Console()
|
|
17
|
+
error_console = Console(stderr=True, style="bold red")
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@click.command(help="List scopes & datasets")
|
|
@@ -32,21 +33,27 @@ console = Console()
|
|
|
32
33
|
@click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
|
|
33
34
|
@click.option("-q", "--quiet", is_flag=True, help="Only errors shown in logs.")
|
|
34
35
|
@click.option("--write", is_flag=True, help="Write the events to file.")
|
|
36
|
+
@click.pass_context
|
|
35
37
|
def list(
|
|
38
|
+
ctx: click.Context,
|
|
36
39
|
scope: Optional[str] = None,
|
|
37
40
|
datasets: Optional[str] = None,
|
|
38
41
|
verbose: int = 0,
|
|
39
42
|
quiet: bool = False,
|
|
40
43
|
write: bool = False,
|
|
41
44
|
):
|
|
42
|
-
"""List Datatrail Scopes & Datasets.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
"""List Datatrail Scopes & Datasets.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ctx (click.Context): Click context.
|
|
49
|
+
scope (str): Scope of dataset.
|
|
50
|
+
datasets (str): Name of dataset.
|
|
51
|
+
verbose (int): Verbosity: v=INFO, vv=DEBUG.
|
|
52
|
+
quiet (bool): Only errors shown in logs.
|
|
53
|
+
write (bool): Write the events to file.
|
|
54
|
+
"""
|
|
55
|
+
# Set logging level.
|
|
56
|
+
set_log_level(logger, verbose, quiet)
|
|
50
57
|
logger.debug("`list` called with:")
|
|
51
58
|
logger.debug(f"scope: {scope} [{type(scope)}]")
|
|
52
59
|
logger.debug(f"datasets: {datasets} [{type(datasets)}]")
|
|
@@ -54,7 +61,10 @@ def list(
|
|
|
54
61
|
logger.debug(f"quiet: {quiet} [{type(quiet)}]")
|
|
55
62
|
if scope:
|
|
56
63
|
if not validate_scope(scope):
|
|
57
|
-
|
|
64
|
+
error_console.print("Scope does not exist!")
|
|
65
|
+
console.print("Valid scopes are:")
|
|
66
|
+
ctx.invoke(list)
|
|
67
|
+
return None
|
|
58
68
|
results = functions.list(scope, datasets, verbose, quiet)
|
|
59
69
|
|
|
60
70
|
# Display scopes.
|
|
@@ -88,7 +98,7 @@ def list(
|
|
|
88
98
|
|
|
89
99
|
# Display datasets in parent dataset for scope.
|
|
90
100
|
if "datasets" in results.keys():
|
|
91
|
-
results["datasets"] = sorted(results["datasets"],
|
|
101
|
+
results["datasets"] = sorted(results["datasets"], reverse=True)
|
|
92
102
|
if write:
|
|
93
103
|
with open(f"./dataset_list_for_{scope}_{datasets}.txt", "w") as file:
|
|
94
104
|
json.dump(results, file)
|
|
@@ -108,4 +118,4 @@ def list(
|
|
|
108
118
|
|
|
109
119
|
# No contact with server.
|
|
110
120
|
if "error" in results.keys():
|
|
111
|
-
|
|
121
|
+
error_console.print(results["error"])
|
|
@@ -9,9 +9,10 @@ from requests.exceptions import SSLError
|
|
|
9
9
|
from rich.console import Console
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
|
+
from dtcli.ls import list
|
|
12
13
|
from dtcli.src import functions
|
|
13
14
|
from dtcli.utilities import cadcclient
|
|
14
|
-
from dtcli.utilities.utilities import validate_scope
|
|
15
|
+
from dtcli.utilities.utilities import set_log_level, validate_scope
|
|
15
16
|
|
|
16
17
|
logger = logging.getLogger("ps")
|
|
17
18
|
|
|
@@ -25,7 +26,9 @@ error_console = Console(stderr=True, style="bold red")
|
|
|
25
26
|
@click.option("-s", "--show-files", is_flag=True, help="Show file names.")
|
|
26
27
|
@click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
|
|
27
28
|
@click.option("-q", "--quiet", is_flag=True, help="Set log level to ERROR.")
|
|
28
|
-
|
|
29
|
+
@click.pass_context
|
|
30
|
+
def ps(
|
|
31
|
+
ctx: click.Context,
|
|
29
32
|
scope: str,
|
|
30
33
|
dataset: str,
|
|
31
34
|
show_files: bool,
|
|
@@ -35,6 +38,7 @@ def ps( # noqa: C901
|
|
|
35
38
|
"""Detailed status of a dataset.
|
|
36
39
|
|
|
37
40
|
Args:
|
|
41
|
+
ctx (click.Context): Click context.
|
|
38
42
|
scope (str): Scope of dataset.
|
|
39
43
|
dataset (str): Name of dataset.
|
|
40
44
|
show_files (bool): Show list of files.
|
|
@@ -45,13 +49,7 @@ def ps( # noqa: C901
|
|
|
45
49
|
None
|
|
46
50
|
"""
|
|
47
51
|
# Set logging level.
|
|
48
|
-
logger
|
|
49
|
-
if verbose == 1:
|
|
50
|
-
logger.setLevel("INFO")
|
|
51
|
-
elif verbose > 1:
|
|
52
|
-
logger.setLevel("DEBUG")
|
|
53
|
-
elif quiet:
|
|
54
|
-
logger.setLevel("ERROR")
|
|
52
|
+
set_log_level(logger, verbose, quiet)
|
|
55
53
|
logger.debug("`ps` called with:")
|
|
56
54
|
logger.debug(f"scope: {scope} [{type(scope)}]")
|
|
57
55
|
logger.debug(f"dataset: {dataset} [{type(dataset)}]")
|
|
@@ -60,72 +58,77 @@ def ps( # noqa: C901
|
|
|
60
58
|
logger.debug(f"quiet: {quiet} [{type(quiet)}]")
|
|
61
59
|
|
|
62
60
|
if not validate_scope(scope):
|
|
63
|
-
|
|
61
|
+
error_console.print("Scope does not exist!")
|
|
62
|
+
console.print("Valid scopes are:")
|
|
63
|
+
ctx.invoke(list)
|
|
64
|
+
return None
|
|
64
65
|
try:
|
|
65
66
|
files, policies = functions.ps(scope, dataset, verbose, quiet)
|
|
66
67
|
except Exception as e:
|
|
67
68
|
logger.error(e)
|
|
68
69
|
return None
|
|
69
70
|
|
|
71
|
+
if show_files and files:
|
|
72
|
+
# Files table
|
|
73
|
+
file_table = create_files_table(dataset, scope, files)
|
|
74
|
+
|
|
75
|
+
with console.pager():
|
|
76
|
+
logger.debug("Showing file table.")
|
|
77
|
+
console.print(file_table)
|
|
78
|
+
return None
|
|
79
|
+
|
|
70
80
|
# Info table
|
|
81
|
+
if files:
|
|
82
|
+
info_table = create_info_table(dataset, scope, files)
|
|
83
|
+
logger.debug("Showing info table.")
|
|
84
|
+
console.print(info_table)
|
|
85
|
+
|
|
86
|
+
# Policy table
|
|
87
|
+
if policies:
|
|
88
|
+
policy_table = create_policy_table(dataset, scope, policies)
|
|
89
|
+
logger.debug("Showing policy table.")
|
|
90
|
+
console.print(policy_table)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_info_table(dataset: str, scope: str, files: dict):
|
|
94
|
+
"""Create info table."""
|
|
71
95
|
logger.debug("Creating info table.")
|
|
72
96
|
info_table = Table(
|
|
73
|
-
title=f"Datatrail: {dataset} {scope} at
|
|
97
|
+
title=f"Datatrail: {dataset} {scope} at SEs",
|
|
74
98
|
header_style="magenta",
|
|
75
99
|
title_style="bold magenta",
|
|
76
100
|
)
|
|
77
|
-
info_table.add_column("Storage Element", style="bold")
|
|
101
|
+
info_table.add_column("Storage Element (SE)", style="bold")
|
|
78
102
|
info_table.add_column("Number of Files", style="green")
|
|
79
103
|
info_table.add_column("Size of Files [GB]", style="green")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
for se in files["file_replica_locations"]:
|
|
105
|
+
logger.debug(f"Creating row for: {se}")
|
|
106
|
+
se_files = files["file_replica_locations"][se]
|
|
107
|
+
if se == "minoc":
|
|
108
|
+
se_files = [f.replace("cadc:CHIMEFRB", "") for f in se_files]
|
|
109
|
+
# Make sure starts with a /
|
|
110
|
+
common_path = os.path.commonpath(
|
|
111
|
+
["/" + f if not f.startswith("/") else f for f in se_files]
|
|
112
|
+
)
|
|
113
|
+
try:
|
|
114
|
+
size = cadcclient.size(common_path)
|
|
115
|
+
except SSLError as error:
|
|
116
|
+
logger.error(error)
|
|
117
|
+
error_console.print(
|
|
118
|
+
"""
|
|
93
119
|
No valid CADC certificate found.
|
|
94
120
|
Create one using 'cadc-get-cert -u <USERNAME>'.
|
|
95
121
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
info_table.add_row(se, f"{len(se_files)}", f"{size:.2f}")
|
|
125
|
+
else:
|
|
126
|
+
info_table.add_row(se, f"{len(se_files)}", "Not available")
|
|
127
|
+
return info_table
|
|
101
128
|
|
|
102
|
-
# Files table
|
|
103
|
-
logger.debug("Creating files table.")
|
|
104
|
-
file_table = Table(
|
|
105
|
-
# header_style="magenta",
|
|
106
|
-
title_style="magenta",
|
|
107
|
-
)
|
|
108
|
-
file_table.add_column(
|
|
109
|
-
f"Datatrail: Files for {dataset} {scope}", style="bold magenta"
|
|
110
|
-
)
|
|
111
129
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
names = [
|
|
115
|
-
Path(_).relative_to(common_path) for _ in files["file_replica_locations"][se]
|
|
116
|
-
]
|
|
117
|
-
for idx, fn in enumerate(names):
|
|
118
|
-
if idx == 0:
|
|
119
|
-
file_table.add_row(f"Storage Element: [magenta]{se}")
|
|
120
|
-
file_table.add_row(f"Common Path: {common_path}/", style="bold green")
|
|
121
|
-
file_table.add_row(f"[green]- {fn}")
|
|
122
|
-
# file_table.add_row(se, common_path, fn)
|
|
123
|
-
else:
|
|
124
|
-
file_table.add_row(f"- {fn}", style="green")
|
|
125
|
-
# file_table.add_row("", "", fn)
|
|
126
|
-
file_table.add_section()
|
|
127
|
-
|
|
128
|
-
# Policy table
|
|
130
|
+
def create_policy_table(dataset: str, scope: str, policies: dict):
|
|
131
|
+
"""Create policy table."""
|
|
129
132
|
logger.debug("Creating policy table.")
|
|
130
133
|
policy_table = Table(
|
|
131
134
|
title=f"Datatrail: Policies for {dataset} {scope}",
|
|
@@ -134,8 +137,11 @@ Create one using 'cadc-get-cert -u <USERNAME>'.
|
|
|
134
137
|
show_footer=True,
|
|
135
138
|
footer_style="bold red",
|
|
136
139
|
)
|
|
140
|
+
belongs_to = (
|
|
141
|
+
policies["belongs_to"][0]["name"] if len(policies["belongs_to"]) > 0 else "None"
|
|
142
|
+
)
|
|
137
143
|
policy_table.add_column("Policy", style="bold", footer="Belongs to")
|
|
138
|
-
policy_table.add_column("Storage Element", footer=
|
|
144
|
+
policy_table.add_column("Storage Element", footer=belongs_to)
|
|
139
145
|
policy_table.add_column("Priority")
|
|
140
146
|
policy_table.add_column("Default")
|
|
141
147
|
policy_table.add_column(r"Delete After \[days]")
|
|
@@ -169,13 +175,33 @@ Create one using 'cadc-get-cert -u <USERNAME>'.
|
|
|
169
175
|
str(dp["delete_after_days"]),
|
|
170
176
|
)
|
|
171
177
|
policy_table.add_section()
|
|
178
|
+
return policy_table
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
|
|
181
|
+
def create_files_table(dataset: str, scope: str, files: dict):
|
|
182
|
+
"""Create files table."""
|
|
183
|
+
logger.debug("Creating files table.")
|
|
184
|
+
file_table = Table(
|
|
185
|
+
# header_style="magenta",
|
|
186
|
+
title_style="magenta",
|
|
187
|
+
)
|
|
188
|
+
file_table.add_column(
|
|
189
|
+
f"Datatrail: Files for {dataset} {scope}", style="bold magenta"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
for se in files["file_replica_locations"]:
|
|
193
|
+
common_path = os.path.commonpath(files["file_replica_locations"][se])
|
|
194
|
+
names = [
|
|
195
|
+
Path(_).relative_to(common_path) for _ in files["file_replica_locations"][se]
|
|
196
|
+
]
|
|
197
|
+
for idx, fn in enumerate(names):
|
|
198
|
+
if idx == 0:
|
|
199
|
+
file_table.add_row(f"Storage Element: [magenta]{se}")
|
|
200
|
+
file_table.add_row(f"Common Path: {common_path}/", style="bold green")
|
|
201
|
+
file_table.add_row(f"[green]- {fn}")
|
|
202
|
+
# file_table.add_row(se, common_path, fn)
|
|
203
|
+
else:
|
|
204
|
+
file_table.add_row(f"- {fn}", style="green")
|
|
205
|
+
# file_table.add_row("", "", fn)
|
|
206
|
+
file_table.add_section()
|
|
207
|
+
return file_table
|
|
@@ -11,7 +11,7 @@ from rich.prompt import Confirm
|
|
|
11
11
|
from dtcli.config import procure
|
|
12
12
|
from dtcli.src.functions import find_missing_dataset_files, get_files
|
|
13
13
|
from dtcli.utilities.cadcclient import size
|
|
14
|
-
from dtcli.utilities.utilities import validate_scope
|
|
14
|
+
from dtcli.utilities.utilities import set_log_level, validate_scope
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger("pull")
|
|
17
17
|
|
|
@@ -41,7 +41,9 @@ error_console = Console(stderr=True, style="bold red")
|
|
|
41
41
|
@click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
|
|
42
42
|
@click.option("-q", "--quiet", is_flag=True, help="Set log level to ERROR.")
|
|
43
43
|
@click.option("--force", "-f", is_flag=True, help="Do not prompt for confirmation.")
|
|
44
|
+
@click.pass_context
|
|
44
45
|
def pull(
|
|
46
|
+
ctx: click.Context,
|
|
45
47
|
scope: str,
|
|
46
48
|
dataset: str,
|
|
47
49
|
directory: str,
|
|
@@ -53,6 +55,7 @@ def pull(
|
|
|
53
55
|
"""Download a dataset.
|
|
54
56
|
|
|
55
57
|
Args:
|
|
58
|
+
ctx (click.Context): Click context.
|
|
56
59
|
scope (str): Scope of dataset.
|
|
57
60
|
dataset (str): Name of dataset.
|
|
58
61
|
directory (str): Directory to pull data to.
|
|
@@ -62,13 +65,7 @@ def pull(
|
|
|
62
65
|
force (bool): Automatically download files.
|
|
63
66
|
"""
|
|
64
67
|
# Set logging level.
|
|
65
|
-
logger
|
|
66
|
-
if verbose == 1:
|
|
67
|
-
logger.setLevel("INFO")
|
|
68
|
-
elif verbose > 1:
|
|
69
|
-
logger.setLevel("DEBUG")
|
|
70
|
-
elif quiet:
|
|
71
|
-
logger.setLevel("ERROR")
|
|
68
|
+
set_log_level(logger, verbose, quiet)
|
|
72
69
|
logger.debug("`pull` called with:")
|
|
73
70
|
logger.debug(f"scope: {scope} [{type(scope)}]")
|
|
74
71
|
logger.debug(f"dataset: {dataset} [{type(dataset)}]")
|
|
@@ -91,26 +88,31 @@ def pull(
|
|
|
91
88
|
raise click.Abort()
|
|
92
89
|
|
|
93
90
|
if not validate_scope(scope):
|
|
94
|
-
|
|
91
|
+
error_console.print("Scope does not exist!")
|
|
92
|
+
console.print("Valid scopes are:")
|
|
93
|
+
ctx.invoke(list)
|
|
94
|
+
return None
|
|
95
95
|
|
|
96
96
|
# Find files missing from localhost.
|
|
97
97
|
console.print(f"\nSearching for files for {dataset} {scope}...\n")
|
|
98
|
-
files = find_missing_dataset_files(scope, dataset)
|
|
99
|
-
if len(files["missing"]) == 0:
|
|
98
|
+
files = find_missing_dataset_files(scope, dataset, directory, verbose)
|
|
99
|
+
if len(files["missing"]) == 0 and len(files["existing"]) == 0:
|
|
100
100
|
console.print("No files found at minoc.", style="bold red")
|
|
101
101
|
return None
|
|
102
102
|
files_paths = [f.replace("cadc:CHIMEFRB", "") for f in files["missing"]]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
to_download_size = 0.0
|
|
104
|
+
if len(files_paths) > 0:
|
|
105
|
+
common_path = path.commonpath(["/" + f for f in files_paths])
|
|
106
|
+
try:
|
|
107
|
+
to_download_size = size(common_path)
|
|
108
|
+
except SSLError:
|
|
109
|
+
error_console.print(
|
|
110
|
+
"""
|
|
109
111
|
No valid CADC certificate found.
|
|
110
112
|
Create one using 'cadc-get-cert -u <USERNAME>'.
|
|
111
113
|
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
)
|
|
115
|
+
return None
|
|
114
116
|
console.print(
|
|
115
117
|
f" - {len(files['existing'])} files found at {site}.",
|
|
116
118
|
style="green",
|
|
@@ -127,6 +129,8 @@ Create one using 'cadc-get-cert -u <USERNAME>'.
|
|
|
127
129
|
# Confirm download.
|
|
128
130
|
if force:
|
|
129
131
|
is_download = True
|
|
132
|
+
elif to_download_size == 0:
|
|
133
|
+
return None
|
|
130
134
|
else:
|
|
131
135
|
is_download = Confirm.ask(
|
|
132
136
|
f"Download {len(files['missing'])} files?",
|
|
@@ -33,13 +33,8 @@ def list( # noqa: C901
|
|
|
33
33
|
Dict[str, Any]: Keys 'error', 'scopes', or 'datasets'. Values are the
|
|
34
34
|
results or error message.
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
logger.setLevel("INFO")
|
|
39
|
-
elif verbose > 1:
|
|
40
|
-
logger.setLevel("DEBUG")
|
|
41
|
-
elif quiet:
|
|
42
|
-
logger.setLevel("ERROR")
|
|
36
|
+
# Set logging level.
|
|
37
|
+
utilities.set_log_level(logger, verbose, quiet)
|
|
43
38
|
# Load configuration.
|
|
44
39
|
logger.debug("Loading configuration.")
|
|
45
40
|
try:
|
|
@@ -108,7 +103,7 @@ def ps(
|
|
|
108
103
|
verbose: int = 0,
|
|
109
104
|
quiet: bool = False,
|
|
110
105
|
base_url: Optional[str] = None,
|
|
111
|
-
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
106
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[Dict[str, Any]]]:
|
|
112
107
|
"""List detailed information about a dataset.
|
|
113
108
|
|
|
114
109
|
Args:
|
|
@@ -123,13 +118,7 @@ def ps(
|
|
|
123
118
|
and dictionary of dataset's policies.
|
|
124
119
|
"""
|
|
125
120
|
# Set logging level.
|
|
126
|
-
|
|
127
|
-
if verbose == 1:
|
|
128
|
-
logger.setLevel("INFO")
|
|
129
|
-
elif verbose > 1:
|
|
130
|
-
logger.setLevel("DEBUG")
|
|
131
|
-
elif quiet:
|
|
132
|
-
logger.setLevel("ERROR")
|
|
121
|
+
utilities.set_log_level(logger, verbose, quiet)
|
|
133
122
|
|
|
134
123
|
# Load configuration.
|
|
135
124
|
logger.debug("Loading configuration.")
|
|
@@ -145,7 +134,7 @@ def ps(
|
|
|
145
134
|
logger.debug(f"Setting base_url to {server}.")
|
|
146
135
|
base_url = server
|
|
147
136
|
try:
|
|
148
|
-
files_response = get_dataset_file_info(scope, dataset)
|
|
137
|
+
files_response = get_dataset_file_info(scope, dataset, verbose, quiet)
|
|
149
138
|
|
|
150
139
|
logger.info(f"Getting policy for {dataset} in {scope}.")
|
|
151
140
|
url: str = str(base_url) + f"/query/dataset/{scope}/{dataset}"
|
|
@@ -153,11 +142,12 @@ def ps(
|
|
|
153
142
|
r = requests.get(url)
|
|
154
143
|
logger.debug(f"Status: {r.status_code}.")
|
|
155
144
|
policy_response = utilities.decode_response(r)
|
|
156
|
-
if "object has no attribute" in policy_response
|
|
145
|
+
if "object has no attribute" in policy_response and isinstance(
|
|
157
146
|
files_response, str
|
|
158
147
|
):
|
|
159
148
|
raise Exception(f"Could not find {dataset} {scope} in Datatrail.")
|
|
160
|
-
|
|
149
|
+
elif isinstance(files_response, str):
|
|
150
|
+
return None, policy_response
|
|
161
151
|
return files_response, policy_response # type: ignore
|
|
162
152
|
|
|
163
153
|
except requests.exceptions.ConnectionError as e:
|
|
@@ -185,13 +175,7 @@ def get_dataset_file_info(
|
|
|
185
175
|
Dict[str, Any]: JSON response from server or error string.
|
|
186
176
|
"""
|
|
187
177
|
# Set logging level.
|
|
188
|
-
|
|
189
|
-
if verbose == 1:
|
|
190
|
-
logger.setLevel("INFO")
|
|
191
|
-
elif verbose > 1:
|
|
192
|
-
logger.setLevel("DEBUG")
|
|
193
|
-
elif quiet:
|
|
194
|
-
logger.setLevel("ERROR")
|
|
178
|
+
utilities.set_log_level(logger, verbose, quiet)
|
|
195
179
|
|
|
196
180
|
# Load configuration.
|
|
197
181
|
config = procure()
|
|
@@ -216,48 +200,55 @@ def get_dataset_file_info(
|
|
|
216
200
|
return {"error": "Datatrail Server at CHIME is not responding."}
|
|
217
201
|
|
|
218
202
|
|
|
219
|
-
def find_missing_dataset_files(
|
|
203
|
+
def find_missing_dataset_files(
|
|
204
|
+
scope: str, dataset: str, root_path: Optional[str] = None, verbose: int = 0
|
|
205
|
+
) -> Dict:
|
|
220
206
|
"""List missing files for a dataset.
|
|
221
207
|
|
|
222
208
|
Args:
|
|
223
209
|
scope (str): Scope of dataset. Defaults to None.
|
|
224
210
|
dataset (str): Name of dataset. Defaults to None.
|
|
211
|
+
root_path (Optional[str]): Path to download files to. Defaults to None.
|
|
212
|
+
verbose (int): Verbosity. Defaults to 0.
|
|
225
213
|
|
|
226
214
|
Returns:
|
|
227
215
|
Dict: Dictionary of results.
|
|
228
216
|
"""
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
217
|
+
# Set logging level.
|
|
218
|
+
utilities.set_log_level(logger, verbose)
|
|
219
|
+
|
|
232
220
|
# find dataset
|
|
233
|
-
dataset_locations = get_dataset_file_info(scope, dataset)
|
|
221
|
+
dataset_locations = get_dataset_file_info(scope, dataset, verbose=verbose)
|
|
234
222
|
if isinstance(dataset_locations, str):
|
|
235
223
|
print(f"Could not find the dataset: {scope}, {dataset}")
|
|
236
224
|
return {}
|
|
237
225
|
|
|
238
|
-
# stage dataset
|
|
239
|
-
site_locations = ["chime", "allenby", "gbo", "hatcreek", "canfar"]
|
|
240
|
-
|
|
241
226
|
# check for local copy of the data.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
227
|
+
logger.info("Checking for local copies of files.")
|
|
228
|
+
if dataset_locations["file_replica_locations"].get("minoc"):
|
|
229
|
+
file_uris = dataset_locations["file_replica_locations"]["minoc"]
|
|
230
|
+
file_paths = []
|
|
231
|
+
# Clean up file paths
|
|
232
|
+
for f in file_uris:
|
|
233
|
+
if f.startswith("data/"):
|
|
234
|
+
file_paths.append(f)
|
|
235
|
+
elif f.startswith("cadc:CHIMEFRB/"):
|
|
236
|
+
file_paths.append(f.replace("//", "/").replace("cadc:CHIMEFRB/", ""))
|
|
237
|
+
elif f.startswith("/"):
|
|
238
|
+
file_paths.append(f.replace("//", "/")[1:])
|
|
245
239
|
# check for missing files
|
|
246
240
|
missing_files = []
|
|
247
241
|
existing_files = []
|
|
248
|
-
for f in
|
|
249
|
-
if Path(f).exists():
|
|
242
|
+
for f in file_paths:
|
|
243
|
+
if Path(root_path + f).exists():
|
|
244
|
+
logger.debug(f"- {f} : ✔")
|
|
250
245
|
existing_files.append(f)
|
|
251
246
|
else:
|
|
247
|
+
logger.debug(f"- {f} : ✘")
|
|
252
248
|
missing_files.append(f)
|
|
253
249
|
|
|
254
|
-
# For local, assume no files exist.
|
|
255
250
|
else:
|
|
256
|
-
|
|
257
|
-
if file_replicas:
|
|
258
|
-
missing_files = file_replicas.get("minoc")
|
|
259
|
-
else:
|
|
260
|
-
missing_files = []
|
|
251
|
+
missing_files = []
|
|
261
252
|
existing_files = []
|
|
262
253
|
return {"missing": missing_files, "existing": existing_files}
|
|
263
254
|
|
|
@@ -282,11 +273,8 @@ def get_files(
|
|
|
282
273
|
None
|
|
283
274
|
"""
|
|
284
275
|
# Set logging level.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
logger.setLevel("INFO")
|
|
288
|
-
elif verbose > 1:
|
|
289
|
-
logger.setLevel("DEBUG")
|
|
276
|
+
utilities.set_log_level(logger, verbose)
|
|
277
|
+
|
|
290
278
|
# Load configuration.
|
|
291
279
|
config = procure()
|
|
292
280
|
mounts = config["root_mounts"]
|
|
@@ -297,7 +285,9 @@ def get_files(
|
|
|
297
285
|
files = [f.replace("cadc:CHIMEFRB/", "") for f in files]
|
|
298
286
|
if not directory:
|
|
299
287
|
directory = mounts[site]
|
|
300
|
-
|
|
288
|
+
if not directory.endswith("/"):
|
|
289
|
+
directory += "/"
|
|
290
|
+
destinations = [(directory + f).replace("//", "/") for f in files]
|
|
301
291
|
# make directory structure if it does not exist.
|
|
302
292
|
folders = {os.path.dirname(path) for path in destinations}
|
|
303
293
|
for folder in folders:
|
|
@@ -322,13 +312,8 @@ def clear_dataset_path(
|
|
|
322
312
|
Returns:
|
|
323
313
|
bool: True if path was deleted.
|
|
324
314
|
"""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
logger.setLevel("INFO")
|
|
328
|
-
elif verbose > 1:
|
|
329
|
-
logger.setLevel("DEBUG")
|
|
330
|
-
elif quiet:
|
|
331
|
-
logger.setLevel("ERROR")
|
|
315
|
+
# Set logging level.
|
|
316
|
+
utilities.set_log_level(logger, verbose, quiet)
|
|
332
317
|
|
|
333
318
|
logger.debug(f"clear_parents: {clear_parents}")
|
|
334
319
|
|
|
@@ -379,13 +364,9 @@ def find_dataset_common_path(
|
|
|
379
364
|
Returns:
|
|
380
365
|
Optional[str]: Common path for dataset.
|
|
381
366
|
"""
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
elif verbose > 1:
|
|
386
|
-
logger.setLevel("DEBUG")
|
|
387
|
-
elif quiet:
|
|
388
|
-
logger.setLevel("ERROR")
|
|
367
|
+
# Set logging level.
|
|
368
|
+
utilities.set_log_level(logger, verbose, quiet)
|
|
369
|
+
|
|
389
370
|
# Load configuration.
|
|
390
371
|
logger.debug("Loading configuration.")
|
|
391
372
|
try:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Utility functions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, List, Union
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from requests.models import Response
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from packaging.version import parse
|
|
12
|
+
except ImportError:
|
|
13
|
+
from pip._vendor.packaging.version import parse
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def set_log_level(logger: logging.Logger, verbose: int = 0, quiet: bool = False) -> None:
|
|
17
|
+
"""Set log level."""
|
|
18
|
+
if verbose == 1:
|
|
19
|
+
logger.setLevel("INFO")
|
|
20
|
+
elif verbose > 1:
|
|
21
|
+
logger.setLevel("DEBUG")
|
|
22
|
+
elif quiet:
|
|
23
|
+
logger.setLevel("ERROR")
|
|
24
|
+
else:
|
|
25
|
+
logger.setLevel("WARNING")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decode_response(response: Response) -> Union[Dict, str]:
|
|
29
|
+
"""Decode response.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
response (Response): Request response.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Union[Dict, str]: JSON response or text.
|
|
36
|
+
"""
|
|
37
|
+
if response.status_code in [200, 201]:
|
|
38
|
+
return response.json()
|
|
39
|
+
else:
|
|
40
|
+
return response.text
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def split(data: List[Any], count: int) -> List[List[Any]]:
|
|
44
|
+
"""Split a list into batches.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
data (List[Any]): List to split.
|
|
48
|
+
count (int): Number of batches to split into.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List[List[Any]]: List of batches.
|
|
52
|
+
"""
|
|
53
|
+
batch_size = len(data) // count
|
|
54
|
+
remainder = len(data) % count
|
|
55
|
+
batches: List[Any] = []
|
|
56
|
+
idx = 0
|
|
57
|
+
for i in range(count):
|
|
58
|
+
if i < remainder:
|
|
59
|
+
batch = data[idx : idx + batch_size + 1] # noqa: E203
|
|
60
|
+
idx += batch_size + 1
|
|
61
|
+
else:
|
|
62
|
+
batch = data[idx : idx + batch_size] # noqa: E203
|
|
63
|
+
idx += batch_size
|
|
64
|
+
if len(batch) > 0:
|
|
65
|
+
batches.append(batch)
|
|
66
|
+
return batches
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def validate_scope(scope: str) -> bool:
|
|
70
|
+
"""Check if scope is valid.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
scope (str): Scope to check.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
bool: True if scope is valid.
|
|
77
|
+
"""
|
|
78
|
+
resp = requests.get("https://frb.chimenet.ca/datatrail/query/dataset/scopes")
|
|
79
|
+
scopes = decode_response(resp)
|
|
80
|
+
return scope in scopes
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_latest_released_version(
|
|
84
|
+
package: str = "datatrail-cli",
|
|
85
|
+
url_pattern: str = "https://pypi.python.org/pypi/{package}/json",
|
|
86
|
+
):
|
|
87
|
+
"""Get latest released version of a package from pypi.python.org.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
package (str): Package name. Defaults to "datatrail-cli".
|
|
91
|
+
url_pattern (str): URL pattern. Defaults to "https://pypi.python.org/pypi/{package}/json". # noqa: E501
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Latest released version.
|
|
95
|
+
"""
|
|
96
|
+
req = requests.get(url_pattern.format(package=package))
|
|
97
|
+
version = parse("0")
|
|
98
|
+
if req.status_code == requests.codes.ok:
|
|
99
|
+
j = json.loads(req.text.encode(req.encoding)) # type: ignore
|
|
100
|
+
releases = j.get("releases", [])
|
|
101
|
+
for release in releases:
|
|
102
|
+
ver = parse(release)
|
|
103
|
+
if not ver.is_prerelease:
|
|
104
|
+
version = max(version, ver)
|
|
105
|
+
return version
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cli_is_latest_release() -> bool:
|
|
109
|
+
"""Check if CLI is latest release."""
|
|
110
|
+
from pkg_resources import get_distribution
|
|
111
|
+
|
|
112
|
+
current_version = parse(get_distribution("datatrail-cli").version)
|
|
113
|
+
return current_version == get_latest_released_version()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "datatrail-cli"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "CHIME/FRB Datatrail CLI"
|
|
5
5
|
authors = ["CHIME FRB Project Office"]
|
|
6
6
|
license = "MIT"
|
|
@@ -8,7 +8,7 @@ readme = "README.md"
|
|
|
8
8
|
packages = [{ include = "dtcli" }]
|
|
9
9
|
|
|
10
10
|
[tool.poetry.dependencies]
|
|
11
|
-
python = "^3.8"
|
|
11
|
+
python = "^3.8.1"
|
|
12
12
|
click = "^8.1.3"
|
|
13
13
|
requests = "^2.29.0"
|
|
14
14
|
rich = "^13.3.5"
|
|
@@ -37,6 +37,7 @@ mkdocs = "^1.4.3"
|
|
|
37
37
|
mkdocs-material = "^9.1.11"
|
|
38
38
|
mkdocstrings = "^0.21.2"
|
|
39
39
|
mkdocs-click = "^0.8.0"
|
|
40
|
+
termynal = "^0.10.1"
|
|
40
41
|
|
|
41
42
|
[tool.flake8]
|
|
42
43
|
max-line-length=89
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
"""Utility functions."""
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict, List, Union
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
from requests.models import Response
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def decode_response(response: Response) -> Union[Dict, str]:
|
|
10
|
-
"""Decode response.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
response (Response): Request response.
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
Union[Dict, str]: JSON response or text.
|
|
17
|
-
"""
|
|
18
|
-
if response.status_code in [200, 201]:
|
|
19
|
-
return response.json()
|
|
20
|
-
else:
|
|
21
|
-
return response.text
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def split(data: List[Any], count: int) -> List[List[Any]]:
|
|
25
|
-
"""Split a list into batches.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
data (List[Any]): List to split.
|
|
29
|
-
count (int): Number of batches to split into.
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
List[List[Any]]: List of batches.
|
|
33
|
-
"""
|
|
34
|
-
batch_size = len(data) // count
|
|
35
|
-
remainder = len(data) % count
|
|
36
|
-
batches: List[Any] = []
|
|
37
|
-
idx = 0
|
|
38
|
-
for i in range(count):
|
|
39
|
-
if i < remainder:
|
|
40
|
-
batch = data[idx : idx + batch_size + 1] # noqa: E203
|
|
41
|
-
idx += batch_size + 1
|
|
42
|
-
else:
|
|
43
|
-
batch = data[idx : idx + batch_size] # noqa: E203
|
|
44
|
-
idx += batch_size
|
|
45
|
-
if len(batch) > 0:
|
|
46
|
-
batches.append(batch)
|
|
47
|
-
return batches
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def validate_scope(scope: str) -> bool:
|
|
51
|
-
"""Check if scope is valid.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
scope (str): Scope to check.
|
|
55
|
-
|
|
56
|
-
Returns:
|
|
57
|
-
bool: True if scope is valid.
|
|
58
|
-
"""
|
|
59
|
-
resp = requests.get("https://frb.chimenet.ca/datatrail/query/dataset/scopes")
|
|
60
|
-
scopes = decode_response(resp)
|
|
61
|
-
return scope in scopes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|