datatrail-cli 0.10.3__tar.gz → 0.11.1__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.3 → datatrail_cli-0.11.1}/CHANGELOG.md +14 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/PKG-INFO +2 -2
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/list.md +60 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/ps.md +70 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/ls.py +11 -1
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/ps.py +32 -3
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/src/functions.py +17 -23
- datatrail_cli-0.11.1/dtcli/unregistered.py +102 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/utilities/cadcclient.py +17 -11
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/utilities/utilities.py +3 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/poetry.lock +1153 -653
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/pyproject.toml +2 -2
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/tests/test_cli.py +107 -22
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/tests/test_utils.py +16 -0
- datatrail_cli-0.10.3/dtcli/unregistered.py +0 -61
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/workflows/continuous-deployment.yml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/workflows/continuous-integration.yml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/workflows/docs.yml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.github/workflows/stale.yml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.gitignore +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/.pre-commit-config.yaml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/LICENSE +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/README.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/clear.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/cli.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/commands.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/images/Datatrail-logo.png +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/index.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/initialising.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/install.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/pull.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/scout.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/docs/user_guide.md +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/__init__.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/clear.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/cli.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/config.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/config.yaml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/pull.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/dtcli/scout.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/mkdocs.yml +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/tests/test_functions.py +0 -0
- {datatrail_cli-0.10.3 → datatrail_cli-0.11.1}/tox.ini +0 -0
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.1](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.11.0...v0.11.1) (2026-07-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **unregistered:** improve way info displayed ([c5f0fca](https://github.com/CHIMEFRB/datatrail-cli/commit/c5f0fca6f6f53e5004e1f1868517100572d68d1a))
|
|
9
|
+
|
|
10
|
+
## [0.11.0](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.3...v0.11.0) (2026-07-07)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **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)
|
|
16
|
+
|
|
3
17
|
## [0.10.3](https://github.com/CHIMEFRB/datatrail-cli/compare/v0.10.2...v0.10.3) (2026-05-19)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datatrail-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.1
|
|
4
4
|
Summary: CHIME/FRB Datatrail CLI
|
|
5
5
|
Author: CHIME FRB Project Office
|
|
6
6
|
Author-email: Tarik Zegmott <tarik.zegmott@mcgill.ca>
|
|
@@ -13,7 +13,7 @@ Requires-Dist: cadcutils>=1.5.1.1
|
|
|
13
13
|
Requires-Dist: click-aliasing>=1.0.0
|
|
14
14
|
Requires-Dist: click>=8.1.3
|
|
15
15
|
Requires-Dist: dill>=0.3.6
|
|
16
|
-
Requires-Dist: lxml>=
|
|
16
|
+
Requires-Dist: lxml>=6.1.0
|
|
17
17
|
Requires-Dist: mergedeep>=1.3.4
|
|
18
18
|
Requires-Dist: pyaml>=21.10.1
|
|
19
19
|
Requires-Dist: requests>=2.29.0
|
|
@@ -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,8 +4,9 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
6
|
import shutil
|
|
7
|
+
import subprocess
|
|
7
8
|
import time
|
|
8
|
-
from collections import
|
|
9
|
+
from collections import Counter
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any, Dict, List, Optional, Tuple
|
|
11
12
|
|
|
@@ -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)
|
|
@@ -485,6 +486,15 @@ def get_unregistered_dataset(dataset: str, scope: str) -> Optional[Dict[str, Any
|
|
|
485
486
|
return response[0]
|
|
486
487
|
|
|
487
488
|
|
|
489
|
+
ATTACH_RE = re.compile(
|
|
490
|
+
r"Could not attach datasets: .+? ERROR: \"?dataset (.+?), (.+?) not found" # noqa: E501
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
CREATE_RE = re.compile(
|
|
494
|
+
r"Could not create dataset: (.+?), scope: (.+?)\. .*UniqueViolation" # noqa: E501
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
488
498
|
def signature(msg: str) -> str:
|
|
489
499
|
"""Create a signature for a reason unregistered message.
|
|
490
500
|
|
|
@@ -494,16 +504,6 @@ def signature(msg: str) -> str:
|
|
|
494
504
|
Returns:
|
|
495
505
|
str: Signature for error message.
|
|
496
506
|
"""
|
|
497
|
-
ATTACH_RE = re.compile(
|
|
498
|
-
r"Could not attach datasets: .+ ERROR: dataset (.+), (.+) not found" # noqa: E501
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
CREATE_RE = re.compile(
|
|
502
|
-
r"Could not create dataset: (.+), scope: (.+)\. .*UniqueViolation" # noqa: E501
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
POSTGRES_RE = re.compile(r".*psycopg.*")
|
|
506
|
-
|
|
507
507
|
msg = msg.strip()
|
|
508
508
|
|
|
509
509
|
# Attach-dataset errors
|
|
@@ -516,11 +516,12 @@ def signature(msg: str) -> str:
|
|
|
516
516
|
m = CREATE_RE.search(msg)
|
|
517
517
|
if m:
|
|
518
518
|
dataset, scope = m.groups()
|
|
519
|
+
dataset = re.sub(r"\d+", "<ID>", dataset)
|
|
519
520
|
return f"CREATE_DUPLICATE:{dataset}:{scope}"
|
|
520
521
|
|
|
521
522
|
# PostgreSQL violation
|
|
522
|
-
|
|
523
|
-
|
|
523
|
+
if "psycopg" in msg:
|
|
524
|
+
msg = re.sub(r"\s+", " ", msg)
|
|
524
525
|
return f"POSTGRES:{msg[:120]}"
|
|
525
526
|
|
|
526
527
|
# Short status / token messages
|
|
@@ -551,11 +552,4 @@ def summarise_unregistered_datasets() -> Dict[str, int]:
|
|
|
551
552
|
Dict[str, int]: Dictionary of error message signatures and their counts.
|
|
552
553
|
"""
|
|
553
554
|
response = get_all_unregistered_datasets()
|
|
554
|
-
|
|
555
|
-
messages = [str(r["results"]["reason"]) for r in response]
|
|
556
|
-
|
|
557
|
-
for msg in messages:
|
|
558
|
-
sig = signature(msg)
|
|
559
|
-
reason_groups[sig] += 1
|
|
560
|
-
|
|
561
|
-
return reason_groups
|
|
555
|
+
return Counter(signature(str(r["results"]["reason"])) for r in response)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Datatrail Unregistered datasets commands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import DefaultDict, List, Tuple
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
from dtcli.src import functions
|
|
13
|
+
from dtcli.utilities.utilities import set_log_level
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
error_console = Console(stderr=True, style="bold red")
|
|
19
|
+
|
|
20
|
+
CATEGORY_STYLES = {
|
|
21
|
+
"ATTACH_MISSING": "yellow",
|
|
22
|
+
"CREATE_DUPLICATE": "magenta",
|
|
23
|
+
"POSTGRES": "red",
|
|
24
|
+
"OTHER": "red",
|
|
25
|
+
"STATUS": "cyan",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group(help="Commands related to unregistered datasets.")
|
|
30
|
+
def unregistered():
|
|
31
|
+
"""Group of commands related to unregistered datasets."""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@unregistered.command(help="Summarise the reasons for unregistered datasets.")
|
|
36
|
+
@click.option("-v", "--verbose", count=True, help="Verbosity: v=INFO, vv=DEBUG.")
|
|
37
|
+
@click.option("-q", "--quiet", is_flag=True, help="Only errors shown in logs.")
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def summary(
|
|
40
|
+
ctx: click.Context,
|
|
41
|
+
verbose: int = 0,
|
|
42
|
+
quiet: bool = False,
|
|
43
|
+
):
|
|
44
|
+
"""Show a summary of the unregistered datasets.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
ctx (click.Context): Click context.
|
|
48
|
+
verbose (int): Verbosity: v=INFO, vv=DEBUG.
|
|
49
|
+
quiet (bool): Only errors shown in logs.
|
|
50
|
+
"""
|
|
51
|
+
# Set logging level.
|
|
52
|
+
set_log_level(logger, verbose, quiet)
|
|
53
|
+
logger.debug("`summary` called with:")
|
|
54
|
+
logger.debug(f"verbose: {verbose} [{type(verbose)}]")
|
|
55
|
+
logger.debug(f"quiet: {quiet} [{type(quiet)}]")
|
|
56
|
+
|
|
57
|
+
results = functions.summarise_unregistered_datasets()
|
|
58
|
+
|
|
59
|
+
if not results:
|
|
60
|
+
console.print("No unregistered datasets found.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
total = sum(results.values())
|
|
64
|
+
|
|
65
|
+
table = Table(
|
|
66
|
+
title=f"Summary of reasons — {total:,} unregistered datasets",
|
|
67
|
+
header_style="magenta",
|
|
68
|
+
title_style="bold magenta",
|
|
69
|
+
row_styles=["none", "dim"],
|
|
70
|
+
)
|
|
71
|
+
table.add_column("Category")
|
|
72
|
+
table.add_column("Detail")
|
|
73
|
+
table.add_column("Count", justify="right")
|
|
74
|
+
table.add_column("%", justify="right")
|
|
75
|
+
|
|
76
|
+
# Group signatures by their category prefix, e.g. "ATTACH_MISSING:...".
|
|
77
|
+
groups: DefaultDict[str, List[Tuple[str, int]]] = defaultdict(list)
|
|
78
|
+
for sig, count in results.items():
|
|
79
|
+
category, _, detail = sig.partition(":")
|
|
80
|
+
groups[category].append((detail, count))
|
|
81
|
+
|
|
82
|
+
ordered = sorted(
|
|
83
|
+
groups.items(),
|
|
84
|
+
key=lambda group: sum(count for _, count in group[1]),
|
|
85
|
+
reverse=True,
|
|
86
|
+
)
|
|
87
|
+
for index, (category, reasons) in enumerate(ordered):
|
|
88
|
+
if index:
|
|
89
|
+
table.add_section()
|
|
90
|
+
style = CATEGORY_STYLES.get(category, "white")
|
|
91
|
+
reasons.sort(key=lambda reason: reason[1], reverse=True)
|
|
92
|
+
for row, (detail, count) in enumerate(reasons):
|
|
93
|
+
if category in ("ATTACH_MISSING", "CREATE_DUPLICATE"):
|
|
94
|
+
detail = detail.replace(":", " → ", 1)
|
|
95
|
+
table.add_row(
|
|
96
|
+
Text(category, style=style) if row == 0 else "",
|
|
97
|
+
Text(detail) if detail else "(no reason recorded)",
|
|
98
|
+
f"{count:,}",
|
|
99
|
+
f"{count / total:.1%}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
console.print(table)
|
|
@@ -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] = []
|