datatrail-cli 0.10.2__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.
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/CHANGELOG.md +14 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/PKG-INFO +1 -1
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/list.md +60 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/ps.md +70 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/clear.py +1 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/ls.py +13 -1
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/ps.py +32 -3
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/pull.py +2 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/src/functions.py +3 -2
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/utilities/cadcclient.py +17 -11
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/utilities/utilities.py +3 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/pyproject.toml +1 -1
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/tests/test_cli.py +107 -22
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/tests/test_utils.py +16 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/workflows/continuous-deployment.yml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/workflows/continuous-integration.yml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/workflows/docs.yml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.github/workflows/stale.yml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.gitignore +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/.pre-commit-config.yaml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/LICENSE +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/README.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/clear.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/cli.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/commands.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/images/Datatrail-logo.png +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/index.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/initialising.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/install.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/pull.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/scout.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/docs/user_guide.md +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/__init__.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/cli.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/config.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/config.yaml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/scout.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/dtcli/unregistered.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/mkdocs.yml +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/poetry.lock +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/tests/test_functions.py +0 -0
- {datatrail_cli-0.10.2 → datatrail_cli-0.11.0}/tox.ini +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [0.10.3](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.2...v0.10.3) (2026-05-19)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* connection errors raise correct status exit code ([9b4d7bc](https://github.com/CHIMEFRB/datatrail-cli/commit/9b4d7bcc74c6a7c6954d965f8ab421ea6181b016))
|
|
16
|
+
|
|
3
17
|
## [0.10.2](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.1...v0.10.2) (2026-05-07)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -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)
|
|
@@ -69,9 +72,17 @@ def list(
|
|
|
69
72
|
return None
|
|
70
73
|
except ConnectionError as e:
|
|
71
74
|
error_console.print(e)
|
|
75
|
+
ctx.exit(1)
|
|
72
76
|
return None
|
|
73
77
|
results = functions.list(scope, datasets, verbose, quiet)
|
|
74
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
|
+
|
|
75
86
|
# Display scopes.
|
|
76
87
|
if "scopes" in results.keys():
|
|
77
88
|
table = Table(
|
|
@@ -131,3 +142,4 @@ def list(
|
|
|
131
142
|
# No contact with server.
|
|
132
143
|
if "error" in results.keys():
|
|
133
144
|
error_console.print(results["error"])
|
|
145
|
+
ctx.exit(1)
|
|
@@ -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
|
|
@@ -107,6 +107,7 @@ def pull( # noqa: C901
|
|
|
107
107
|
return None
|
|
108
108
|
except ConnectionError as e:
|
|
109
109
|
error_console.print(e)
|
|
110
|
+
ctx.exit(1)
|
|
110
111
|
return None
|
|
111
112
|
|
|
112
113
|
# Check Canfar status.
|
|
@@ -120,6 +121,7 @@ def pull( # noqa: C901
|
|
|
120
121
|
files = find_missing_dataset_files(scope, dataset, directory, verbose)
|
|
121
122
|
if files.get("error"):
|
|
122
123
|
error_console.print(files["error"])
|
|
124
|
+
ctx.exit(1)
|
|
123
125
|
return None
|
|
124
126
|
elif len(files["missing"]) == 0 and len(files["existing"]) == 0:
|
|
125
127
|
console.print("No files found at minoc.", style="bold red")
|
|
@@ -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
|
-
|
|
301
|
-
|
|
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(
|
|
75
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] = []
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|