datatrail-cli 0.10.3__tar.gz → 0.11.0__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.
Files changed (45) hide show
  1. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/CHANGELOG.md +7 -0
  2. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/PKG-INFO +1 -1
  3. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/list.md +60 -0
  4. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/ps.md +70 -0
  5. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/ls.py +11 -1
  6. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/ps.py +32 -3
  7. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/src/functions.py +3 -2
  8. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/utilities/cadcclient.py +17 -11
  9. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/utilities/utilities.py +3 -0
  10. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/pyproject.toml +1 -1
  11. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/tests/test_cli.py +107 -22
  12. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/tests/test_utils.py +16 -0
  13. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  14. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  15. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  16. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/workflows/continuous-deployment.yml +0 -0
  17. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/workflows/continuous-integration.yml +0 -0
  18. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/workflows/docs.yml +0 -0
  19. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.github/workflows/stale.yml +0 -0
  20. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.gitignore +0 -0
  21. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/.pre-commit-config.yaml +0 -0
  22. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/LICENSE +0 -0
  23. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/README.md +0 -0
  24. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/clear.md +0 -0
  25. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/cli.md +0 -0
  26. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/commands.md +0 -0
  27. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/images/Datatrail-logo.png +0 -0
  28. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/index.md +0 -0
  29. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/initialising.md +0 -0
  30. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/install.md +0 -0
  31. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/pull.md +0 -0
  32. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/scout.md +0 -0
  33. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/docs/user_guide.md +0 -0
  34. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/__init__.py +0 -0
  35. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/clear.py +0 -0
  36. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/cli.py +0 -0
  37. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/config.py +0 -0
  38. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/config.yaml +0 -0
  39. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/pull.py +0 -0
  40. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/scout.py +0 -0
  41. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/dtcli/unregistered.py +0 -0
  42. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/mkdocs.yml +0 -0
  43. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/poetry.lock +0 -0
  44. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/tests/test_functions.py +0 -0
  45. {datatrail_cli-0.10.3 → datatrail_cli-0.11.0}/tox.ini +0 -0
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.3...v0.11.0) (2026-07-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * **ls & ps:** add --json flag for machine readable output ([c4742a0](https://github.com/CHIMEFRB/datatrail-cli/commit/c4742a0dfe43823ce8e68f21e26e146b90db958c)), closes [#159](https://github.com/CHIMEFRB/datatrail-cli/issues/159)
9
+
3
10
  ## [0.10.3](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.2...v0.10.3) (2026-05-19)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datatrail-cli
3
- Version: 0.10.3
3
+ Version: 0.11.0
4
4
  Summary: CHIME/FRB Datatrail CLI
5
5
  Author: CHIME FRB Project Office
6
6
  Author-email: Tarik Zegmott <tarik.zegmott@mcgill.ca>
@@ -11,6 +11,7 @@ Options:
11
11
  -v, --verbose Verbosity: v=INFO, vv=DEBUG.
12
12
  -q, --quiet Only errors shown in logs.
13
13
  --write Write the events to file.
14
+ --json Output as JSON.
14
15
  --help Show this message and exit.
15
16
 
16
17
  ```
@@ -100,3 +101,62 @@ Within Datatrail, there are two types of datasets:
100
101
 
101
102
  Please see the CLI reference page for more information on the `list` command:
102
103
  [datatrail list](../cli/#datatrail-list)
104
+
105
+ ## 🤖 Machine-readable JSON output
106
+
107
+ The `--json` flag outputs structured JSON instead of formatted tables, making it easy to parse the output in scripts and pipelines:
108
+
109
+ ```bash
110
+ # Get all scopes as JSON
111
+ $ datatrail ls --json
112
+ {
113
+ "scopes": [
114
+ "chime.event.baseband.raw",
115
+ "chime.event.intensity.raw",
116
+ "kko.acquisition.processed",
117
+ ...
118
+ ]
119
+ }
120
+
121
+ # Get larger datasets as JSON
122
+ $ datatrail ls kko.scheduled.baseband.raw --json
123
+ {
124
+ "scope": "kko.scheduled.baseband.raw",
125
+ "larger_datasets": [
126
+ "20230804095251",
127
+ "scheduled.commissioning.steady"
128
+ ]
129
+ }
130
+
131
+ # Get child datasets as JSON
132
+ $ datatrail ls kko.scheduled.baseband.raw scheduled.commissioning.steady --json
133
+ {
134
+ "datasets": [
135
+ "20230616150511",
136
+ "20230604135840",
137
+ "20230604134842",
138
+ ...
139
+ ]
140
+ }
141
+ ```
142
+
143
+ ### Usage in scripts
144
+
145
+ ```python
146
+ import json
147
+ import subprocess
148
+
149
+ # Get all scopes
150
+ result = subprocess.run(["datatrail", "ls", "--json"], capture_output=True, text=True)
151
+ data = json.loads(result.stdout)
152
+ scopes = data["scopes"]
153
+
154
+ # Get datasets for a scope
155
+ result = subprocess.run(
156
+ ["datatrail", "ls", "chime.event.baseband.raw", "--json"],
157
+ capture_output=True,
158
+ text=True,
159
+ )
160
+ data = json.loads(result.stdout)
161
+ larger_datasets = data["larger_datasets"]
162
+ ```
@@ -11,6 +11,7 @@ Options:
11
11
  -s, --show-files Show file names.
12
12
  -v, --verbose Verbosity: v=INFO, vv=DEBUG.
13
13
  -q, --quiet Set log level to ERROR.
14
+ --json Output as JSON.
14
15
  --help Show this message and exit.
15
16
  ```
16
17
 
@@ -80,3 +81,72 @@ if the `--show-files` or `-s` flag is passed.
80
81
  │ - baseband_308892599_111.h5 │
81
82
  :
82
83
  ```
84
+
85
+ ## 🤖 Machine-readable JSON output
86
+
87
+ The `--json` flag outputs structured JSON instead of formatted tables, making it easy to parse dataset information in scripts and pipelines:
88
+
89
+ ```bash
90
+ $ datatrail ps kko.event.baseband.raw 308892599 --json
91
+ {
92
+ "dataset": "308892599",
93
+ "scope": "kko.event.baseband.raw",
94
+ "files": {
95
+ "contains_datasets": 0,
96
+ "datasets_contained": [],
97
+ "file_replica_locations": {
98
+ "minoc": [
99
+ "data/kko/baseband/raw/2023/08/07/astro_308892599/baseband_308892599_129.h5",
100
+ "data/kko/baseband/raw/2023/08/07/astro_308892599/baseband_308892599_1013.h5",
101
+ ...
102
+ ]
103
+ }
104
+ },
105
+ "policies": {
106
+ "replication_policy": {
107
+ "preferred_storage_elements": ["chime"],
108
+ "priority": "low",
109
+ "default": true
110
+ },
111
+ "deletion_policy": [
112
+ {
113
+ "storage_element": "minoc",
114
+ "priority": "low",
115
+ "default": true,
116
+ "delete_after_days": 36500
117
+ },
118
+ ...
119
+ ],
120
+ "belongs_to": [
121
+ {
122
+ "scope": "kko.event.baseband.raw",
123
+ "name": "B0531+21.commissioning.pulsar.temp"
124
+ }
125
+ ]
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### Usage in scripts
131
+
132
+ ```python
133
+ import json
134
+ import subprocess
135
+
136
+ # Get dataset information
137
+ result = subprocess.run(
138
+ ["datatrail", "ps", "kko.event.baseband.raw", "308892599", "--json"],
139
+ capture_output=True,
140
+ text=True,
141
+ )
142
+ data = json.loads(result.stdout)
143
+
144
+ # Access file locations
145
+ file_locations = data["files"]["file_replica_locations"]
146
+ minoc_files = file_locations.get("minoc", [])
147
+
148
+ # Access policies
149
+ replication_policy = data["policies"]["replication_policy"]
150
+ deletion_policy = data["policies"]["deletion_policy"]
151
+ belongs_to = data["policies"]["belongs_to"]
152
+ ```
@@ -34,14 +34,16 @@ error_console = Console(stderr=True, style="bold red")
34
34
  @click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
35
35
  @click.option("-q", "--quiet", is_flag=True, help="Only errors shown in logs.")
36
36
  @click.option("--write", is_flag=True, help="Write the events to file.")
37
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
37
38
  @click.pass_context
38
- def list(
39
+ def list( # noqa: C901
39
40
  ctx: click.Context,
40
41
  scope: Optional[str] = None,
41
42
  datasets: Optional[str] = None,
42
43
  verbose: int = 0,
43
44
  quiet: bool = False,
44
45
  write: bool = False,
46
+ output_json: bool = False,
45
47
  ):
46
48
  """List Datatrail Scopes & Datasets.
47
49
 
@@ -52,6 +54,7 @@ def list(
52
54
  verbose (int): Verbosity: v=INFO, vv=DEBUG.
53
55
  quiet (bool): Only errors shown in logs.
54
56
  write (bool): Write the events to file.
57
+ output_json (bool): Output as JSON.
55
58
  """
56
59
  # Set logging level.
57
60
  set_log_level(logger, verbose, quiet)
@@ -73,6 +76,13 @@ def list(
73
76
  return None
74
77
  results = functions.list(scope, datasets, verbose, quiet)
75
78
 
79
+ # Output JSON if requested.
80
+ if output_json:
81
+ print(json.dumps(results, indent=2))
82
+ if "error" in results:
83
+ ctx.exit(1)
84
+ return
85
+
76
86
  # Display scopes.
77
87
  if "scopes" in results.keys():
78
88
  table = Table(
@@ -26,14 +26,16 @@ error_console = Console(stderr=True, style="bold red")
26
26
  @click.option("-s", "--show-files", is_flag=True, help="Show file names.")
27
27
  @click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
28
28
  @click.option("-q", "--quiet", is_flag=True, help="Set log level to ERROR.")
29
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON.")
29
30
  @click.pass_context
30
- def ps(
31
+ def ps( # noqa: C901
31
32
  ctx: click.Context,
32
33
  scope: str,
33
34
  dataset: str,
34
35
  show_files: bool,
35
36
  verbose: int,
36
37
  quiet: bool,
38
+ output_json: bool,
37
39
  ):
38
40
  """Detailed status of a dataset.
39
41
 
@@ -44,6 +46,7 @@ def ps(
44
46
  show_files (bool): Show list of files.
45
47
  verbose (int): Verbosity: v=INFO, vv=DUBUG.
46
48
  quiet (bool): Set log level to ERROR.
49
+ output_json (bool): Output as JSON.
47
50
 
48
51
  Returns:
49
52
  None
@@ -73,13 +76,41 @@ def ps(
73
76
  try:
74
77
  files, policies = functions.ps(scope, dataset, verbose, quiet)
75
78
  if isinstance(files, str) or isinstance(policies, str):
79
+ if output_json:
80
+ import json
81
+
82
+ print(
83
+ json.dumps(
84
+ {"error": {"files": str(files), "policies": str(policies)}},
85
+ indent=2,
86
+ )
87
+ )
88
+ ctx.exit(1)
76
89
  error_console.print("Error: files = ", files)
77
90
  error_console.print("Error: policies = ", policies)
78
91
  return None
79
92
  except Exception as e:
93
+ if output_json:
94
+ import json
95
+
96
+ print(json.dumps({"error": str(e)}, indent=2))
97
+ ctx.exit(1)
80
98
  error_console.print(e)
81
99
  return None
82
100
 
101
+ # Handle JSON output
102
+ if output_json:
103
+ import json
104
+
105
+ result = {
106
+ "dataset": dataset,
107
+ "scope": scope,
108
+ "files": files,
109
+ "policies": policies,
110
+ }
111
+ print(json.dumps(result, indent=2))
112
+ return None
113
+
83
114
  if show_files and files:
84
115
  # Files table
85
116
  file_table = create_files_table(dataset, scope, files)
@@ -234,9 +265,7 @@ def create_files_table(dataset: str, scope: str, files: dict):
234
265
  file_table.add_row(f"Storage Element: [magenta]{se}")
235
266
  file_table.add_row(f"Common Path: {common_path}/", style="bold green")
236
267
  file_table.add_row(f"[green]- {fn}")
237
- # file_table.add_row(se, common_path, fn)
238
268
  else:
239
269
  file_table.add_row(f"- {fn}", style="green")
240
- # file_table.add_row("", "", fn)
241
270
  file_table.add_section()
242
271
  return file_table
@@ -4,6 +4,7 @@ import logging
4
4
  import os
5
5
  import re
6
6
  import shutil
7
+ import subprocess
7
8
  import time
8
9
  from collections import defaultdict
9
10
  from pathlib import Path
@@ -297,8 +298,8 @@ def get_files(
297
298
  if site == "canfar":
298
299
  for folder in folders:
299
300
  os.makedirs(folder, exist_ok=True)
300
- os.system(f"chgrp -R chime-frb-rw {folder}") # nosec
301
- os.system(f"chmod -R g+w {folder}") # nosec
301
+ subprocess.run(["chgrp", "-R", "chime-frb-rw", folder])
302
+ subprocess.run(["chmod", "-R", "g+w", folder])
302
303
  else:
303
304
  for folder in folders:
304
305
  os.makedirs(folder, exist_ok=True)
@@ -3,6 +3,7 @@
3
3
  import logging
4
4
  import os
5
5
  import sys
6
+ from concurrent.futures import ThreadPoolExecutor
6
7
  from io import StringIO
7
8
  from multiprocessing import Process # Use the standard library only
8
9
  from typing import Any, Dict, List, Optional, Tuple
@@ -69,10 +70,13 @@ def _connect(
69
70
  storage = StorageInventoryClient(cert, resource_id=storage_resource_id)
70
71
  query = CadcTapClient(cert, resource_id=query_resource_id)
71
72
  return cert, storage, query
72
- # TODO: Handle invalid cert
73
73
  except ValueError as error:
74
- logger.error(error)
75
- raise error
74
+ logger.error(
75
+ "Authorization failed: The provided CANFAR certificate is "
76
+ "invalid or expired. Please ensure you have a valid "
77
+ "certificate and try again."
78
+ )
79
+ raise ValueError("Invalid or expired CANFAR certificate.") from error
76
80
 
77
81
 
78
82
  def get(
@@ -410,23 +414,25 @@ def status(
410
414
  ]
411
415
  if not certfile:
412
416
  certfile = procure(key="vospace_certfile")
413
- minoc_status = False
414
- luskan_status = False
415
- for index, url in enumerate(urls):
417
+
418
+ def check_url(url: str) -> bool:
416
419
  response = requests.get(url, cert=certfile, allow_redirects=True)
417
420
  try:
418
421
  response.raise_for_status()
419
422
  authorised = response.headers.get("x-vo-authenticated")
420
423
  if isinstance(authorised, str):
421
- if index == 0:
422
- minoc_status = True
423
- else:
424
- luskan_status = True
424
+ return True
425
425
  else:
426
426
  raise TypeError
427
427
  except HTTPError as error:
428
428
  logger.warning(error)
429
429
  logger.warning(f"{url.split('/')[3]} is down.")
430
+ return False
430
431
  except TypeError:
431
432
  logger.error("Canfar certificate is not valid.")
432
- return minoc_status, luskan_status
433
+ return False
434
+
435
+ with ThreadPoolExecutor(max_workers=len(urls)) as executor:
436
+ results = list(executor.map(check_url, urls))
437
+
438
+ return results[0], results[1]
@@ -70,6 +70,9 @@ def split(data: List[Any], count: int) -> List[List[Any]]:
70
70
  Returns:
71
71
  List[List[Any]]: List of batches.
72
72
  """
73
+ if count <= 0:
74
+ raise ValueError("count must be greater than 0")
75
+
73
76
  batch_size = len(data) // count
74
77
  remainder = len(data) % count
75
78
  batches: List[Any] = []
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "datatrail-cli"
3
- version = "0.10.3"
3
+ version = "0.11.0"
4
4
  description = "CHIME/FRB Datatrail CLI"
5
5
  authors = [
6
6
  { name = "CHIME FRB Project Office" },
@@ -97,18 +97,15 @@ def test_cli_list_help(runner: CliRunner) -> None:
97
97
  runner (CliRunner) -> None: Click runner.
98
98
  """
99
99
  result = runner.invoke(datatrail, ["ls", "--help"])
100
- expected_response = """Usage: cli list [OPTIONS] [SCOPE] [DATASETS]
101
-
102
- List scopes & datasets
103
-
104
- Options:
105
- -v, --verbose Verbosity: v=INFO, vv=DEBUG.
106
- -q, --quiet Only errors shown in logs.
107
- --write Write the events to file.
108
- --help Show this message and exit.
109
- """
110
100
  assert result.exit_code == 0
111
- assert result.output == expected_response
101
+ # Check that all the expected elements are present
102
+ assert "Usage: cli list [OPTIONS] [SCOPE] [DATASETS]" in result.output
103
+ assert "List scopes & datasets" in result.output
104
+ assert "--verbose" in result.output
105
+ assert "--quiet" in result.output
106
+ assert "--write" in result.output
107
+ assert "--json" in result.output
108
+ assert "Output as JSON" in result.output
112
109
 
113
110
 
114
111
  def test_cli_ps_help(runner: CliRunner) -> None:
@@ -118,18 +115,15 @@ def test_cli_ps_help(runner: CliRunner) -> None:
118
115
  runner (CliRunner) -> None: Click runner.
119
116
  """
120
117
  result = runner.invoke(datatrail, ["ps", "--help"])
121
- expected_response = """Usage: cli ps [OPTIONS] SCOPE DATASET
122
-
123
- Details of a dataset.
124
-
125
- Options:
126
- -s, --show-files Show file names.
127
- -v, --verbose Verbosity: v=INFO, vv=DEBUG.
128
- -q, --quiet Set log level to ERROR.
129
- --help Show this message and exit.
130
- """
131
118
  assert result.exit_code == 0
132
- assert result.output == expected_response
119
+ # Check that all the expected elements are present
120
+ assert "Usage: cli ps [OPTIONS] SCOPE DATASET" in result.output
121
+ assert "Details of a dataset" in result.output
122
+ assert "--show-files" in result.output
123
+ assert "--verbose" in result.output
124
+ assert "--quiet" in result.output
125
+ assert "--json" in result.output
126
+ assert "Output as JSON" in result.output
133
127
 
134
128
 
135
129
  def test_cli_pull_help(runner: CliRunner) -> None:
@@ -580,3 +574,94 @@ def test_cli_version(runner: CliRunner) -> None:
580
574
  result = runner.invoke(datatrail, ["version"])
581
575
  assert result.exit_code == 0
582
576
  assert "Datatrail Versions" in result.output
577
+
578
+
579
+ def test_cli_list_scopes_json(runner: CliRunner) -> None:
580
+ """Test for CLI list to output scopes as JSON.
581
+
582
+ Args:
583
+ runner (CliRunner): Click runner.
584
+ """
585
+ import json
586
+
587
+ result = runner.invoke(datatrail, ["ls", "--json"])
588
+ assert result.exit_code == 0
589
+ # Extract JSON from output (skip version check message if present)
590
+ json_start = result.output.find("{")
591
+ json_output = result.output[json_start:]
592
+ # Parse the output as JSON
593
+ output_data = json.loads(json_output)
594
+ # Should have a 'scopes' key with a list of scopes
595
+ assert "scopes" in output_data
596
+ assert isinstance(output_data["scopes"], list)
597
+ assert "chime.event.intensity.raw" in output_data["scopes"]
598
+
599
+
600
+ def test_cli_list_larger_json(runner: CliRunner) -> None:
601
+ """Test for CLI to list larger datasets as JSON.
602
+
603
+ Args:
604
+ runner (CliRunner): Click runner.
605
+ """
606
+ import json
607
+
608
+ result = runner.invoke(datatrail, ["ls", "chime.event.baseband.raw", "--json"])
609
+ assert result.exit_code == 0
610
+ # Extract JSON from output (skip version check message if present)
611
+ json_start = result.output.find("{")
612
+ json_output = result.output[json_start:]
613
+ # Parse the output as JSON
614
+ output_data = json.loads(json_output)
615
+ # Should have a 'larger_datasets' key with a list of datasets
616
+ assert "larger_datasets" in output_data
617
+ assert isinstance(output_data["larger_datasets"], list)
618
+ assert "classified.FRB" in output_data["larger_datasets"]
619
+
620
+
621
+ def test_cli_list_children_json(runner: CliRunner) -> None:
622
+ """Test for CLI list to show child datasets as JSON.
623
+
624
+ Args:
625
+ runner (CliRunner): Click runner.
626
+ """
627
+ import json
628
+
629
+ result = runner.invoke(
630
+ datatrail, ["ls", "chime.event.baseband.raw", "classified.FRB", "--json"]
631
+ )
632
+ assert result.exit_code == 0
633
+ # Extract JSON from output (skip version check message if present)
634
+ json_start = result.output.find("{")
635
+ json_output = result.output[json_start:]
636
+ # Parse the output as JSON
637
+ output_data = json.loads(json_output)
638
+ # Should have a 'datasets' key with a list of datasets
639
+ assert "datasets" in output_data
640
+ assert isinstance(output_data["datasets"], list)
641
+ assert "289007650" in output_data["datasets"]
642
+
643
+
644
+ def test_cli_ps_json(runner: CliRunner) -> None:
645
+ """Test for CLI ps command with JSON output.
646
+
647
+ Args:
648
+ runner (CliRunner): Click runner.
649
+ """
650
+ import json
651
+
652
+ result = runner.invoke(
653
+ datatrail, ["ps", "chime.event.baseband.raw", "289007650", "--json"]
654
+ )
655
+ assert result.exit_code == 0
656
+ # Extract JSON from output (skip version check message if present)
657
+ json_start = result.output.find("{")
658
+ json_output = result.output[json_start:]
659
+ # Parse the output as JSON
660
+ output_data = json.loads(json_output)
661
+ # Should have dataset, scope, files, and policies keys
662
+ assert "dataset" in output_data
663
+ assert "scope" in output_data
664
+ assert "files" in output_data
665
+ assert "policies" in output_data
666
+ assert output_data["dataset"] == "289007650"
667
+ assert output_data["scope"] == "chime.event.baseband.raw"
@@ -1,3 +1,5 @@
1
+ import pytest
2
+
1
3
  from dtcli.utilities import utilities
2
4
 
3
5
 
@@ -8,6 +10,20 @@ def test_split():
8
10
  assert result == [["a"], ["b"]]
9
11
 
10
12
 
13
+ def test_split_edges():
14
+ """Test split() with edge cases like empty list and invalid counts."""
15
+ # Empty data
16
+ assert utilities.split([], 2) == []
17
+
18
+ # Zero count should raise ValueError
19
+ with pytest.raises(ValueError, match="count must be greater than 0"):
20
+ utilities.split([1, 2, 3], 0)
21
+
22
+ # Negative count should raise ValueError
23
+ with pytest.raises(ValueError, match="count must be greater than 0"):
24
+ utilities.split([1, 2, 3], -1)
25
+
26
+
11
27
  def test_split_more_batches_than_items():
12
28
  """split() must not return more batches than there are items.
13
29
 
File without changes
File without changes
File without changes