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.
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: datatrail-cli
3
- Version: 0.4.2
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(name="clear", help="Clear a dataset.")
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="Directory to clear data from.",
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="Do not prompt for confirmation.")
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
- logger.setLevel("WARNING")
60
- if verbose == 1:
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
- raise ValueError("Scope does not exist.")
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
- pass
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(name="init", help="Initialize configuration.")
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", "-s", type=click.STRING, help="Site to initialize.", required=True
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
- """Initialize configuration.
90
+ """Initialise configuration.
77
91
 
78
92
  Args:
79
- site (str): Site to initialize.
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
- logger.setLevel("WARNING")
44
- if verbose == 1:
45
- logger.setLevel("INFO")
46
- elif verbose > 1:
47
- logger.setLevel("DEBUG")
48
- elif quiet:
49
- logger.setLevel("ERROR")
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
- raise ValueError("Scope does not exist.")
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"], key=int, reverse=True)
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
- console.print(results["error"], style="red bold")
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
- def ps( # noqa: C901
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.setLevel("WARNING")
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
- raise ValueError("Scope does not exist.")
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 Minoc",
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
- if files["file_replica_locations"].get("minoc"):
81
- minoc_files = files["file_replica_locations"]["minoc"]
82
- minoc_files = [f.replace("cadc:CHIMEFRB", "") for f in minoc_files]
83
- # Make sure starts with a /
84
- common_path = os.path.commonpath(
85
- ["/" + f if not f.startswith("/") else f for f in minoc_files]
86
- )
87
- try:
88
- size = cadcclient.size(common_path)
89
- except SSLError as error:
90
- logger.error(error)
91
- error_console.print(
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
- return None
98
- info_table.add_row("minoc", f"{len(minoc_files)}", f"{size:.2f}")
99
- else:
100
- info_table.add_row("minoc", str(0), str(0))
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
- for se in files["file_replica_locations"]:
113
- common_path = os.path.commonpath(files["file_replica_locations"][se])
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=policies["belongs_to"][0]["name"])
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
- if show_files:
174
- with console.pager():
175
- logger.debug("Showing file table.")
176
- console.print(file_table)
177
- else:
178
- logger.debug("Showing info table.")
179
- console.print(info_table)
180
- logger.debug("Showing policy table.")
181
- console.print(policy_table)
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.setLevel("WARNING")
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
- raise ValueError("Scope does not exist.")
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
- common_path = path.commonpath(["/" + f for f in files_paths])
104
- try:
105
- to_download_size = size(common_path)
106
- except SSLError:
107
- error_console.print(
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
- return None
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
- logger.setLevel("WARNING")
37
- if verbose == 1:
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
- logger.setLevel("WARNING")
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 or isinstance(
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
- logger.setLevel("WARNING")
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(scope: str, dataset: str) -> Dict:
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
- # Load configuration.
230
- config = procure()
231
- SITE = config["site"]
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
- if SITE in site_locations and dataset_locations["file_replica_locations"].get(SITE):
243
- file_uris = dataset_locations["file_replica_locations"][SITE]
244
- print(f"Files found at {SITE}")
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 file_uris:
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
- file_replicas = dataset_locations.get("file_replica_locations")
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
- logger.setLevel("WARNING")
286
- if verbose == 1:
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
- destinations = [directory + "/" + f for f in files]
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
- logger.setLevel("WARNING")
326
- if verbose == 1:
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
- logger.setLevel("WARNING")
383
- if verbose == 1:
384
- logger.setLevel("INFO")
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.2"
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