Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/ps.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ $ datatrail ps kko.event.baseband.raw 308892599 --json
]
}
},
"common_paths": {
"minoc": {
"common_path": "data/kko/baseband/raw/2023/08/07/astro_308892599",
"files": [
"baseband_308892599_129.h5",
"baseband_308892599_1013.h5",
...
]
}
},
"policies": {
"replication_policy": {
"preferred_storage_elements": ["chime"],
Expand All @@ -127,6 +137,13 @@ $ datatrail ps kko.event.baseband.raw 308892599 --json
}
```

The `common_paths` field gives, per storage element, the deepest common
directory of its file replicas and the file names relative to it, so scripts
do not have to re-derive the split. When no common directory exists, it is
`""` and the original paths are listed. Note that `minoc` paths keep any
collection prefix (such as `cadc:CHIMEFRB`) exactly as reported in
`file_replica_locations`.

### Usage in scripts

```python
Expand All @@ -145,6 +162,10 @@ data = json.loads(result.stdout)
file_locations = data["files"]["file_replica_locations"]
minoc_files = file_locations.get("minoc", [])

# Compose full paths from the derived common path
minoc = data["common_paths"].get("minoc", {})
full_paths = [f"{minoc['common_path']}/{name}" for name in minoc.get("files", [])]

# Access policies
replication_policy = data["policies"]["replication_policy"]
deletion_policy = data["policies"]["deletion_policy"]
Expand Down
23 changes: 14 additions & 9 deletions dtcli/ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import logging
import os
from pathlib import Path

import click
from requests.exceptions import SSLError
Expand All @@ -12,7 +11,12 @@
from dtcli.ls import list
from dtcli.src import functions
from dtcli.utilities import cadcclient
from dtcli.utilities.utilities import check_canfar_status, set_log_level, validate_scope
from dtcli.utilities.utilities import (
check_canfar_status,
common_paths,
set_log_level,
validate_scope,
)

logger = logging.getLogger("ps")

Expand Down Expand Up @@ -107,6 +111,9 @@ def ps( # noqa: C901
"scope": scope,
"files": files,
"policies": policies,
"common_paths": common_paths(files.get("file_replica_locations", {}))
if isinstance(files, dict)
else {},
}
print(json.dumps(result, indent=2))
return None
Expand Down Expand Up @@ -255,15 +262,13 @@ def create_files_table(dataset: str, scope: str, files: dict):
f"Datatrail: Files for {dataset} {scope}", style="bold magenta"
)

for se in files["file_replica_locations"]:
common_path = os.path.commonpath(files["file_replica_locations"][se])
names = [
Path(_).relative_to(common_path) for _ in files["file_replica_locations"][se]
]
for idx, fn in enumerate(names):
for se, derived in common_paths(files["file_replica_locations"]).items():
for idx, fn in enumerate(derived["files"]):
if idx == 0:
file_table.add_row(f"Storage Element: [magenta]{se}")
file_table.add_row(f"Common Path: {common_path}/", style="bold green")
file_table.add_row(
f"Common Path: {derived['common_path']}/", style="bold green"
)
file_table.add_row(f"[green]- {fn}")
else:
file_table.add_row(f"- {fn}", style="green")
Expand Down
34 changes: 34 additions & 0 deletions dtcli/utilities/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import json
import logging
import os
from pathlib import Path
from typing import Any, Dict, List, Tuple, Union

import requests
Expand All @@ -19,6 +21,38 @@
REQUEST_TIMEOUT: Tuple[float, float] = (10.0, 300.0)


def common_paths(
file_replica_locations: Dict[str, List[str]],
) -> Dict[str, Dict[str, Any]]:
"""Derive each storage element's common path and relative file names.

Args:
file_replica_locations (Dict[str, List[str]]): File paths per
storage element, as reported in a dataset's file information.

Returns:
Dict[str, Dict[str, Any]]: Per storage element, the deepest common
directory as 'common_path' and the file names relative to it as
'files'. When no common directory exists, 'common_path' is ""
and 'files' holds the original paths. A storage element with no
valid file list is omitted.
"""
derived: Dict[str, Dict[str, Any]] = {}
for se, paths in file_replica_locations.items():
if not paths or not all(isinstance(p, str) and p for p in paths):
continue
try:
if len(paths) == 1:
base = os.path.dirname(paths[0])
else:
base = os.path.commonpath(paths)
except ValueError:
base = ""
names = [str(Path(p).relative_to(base)) for p in paths] if base else [*paths]
derived[se] = {"common_path": base, "files": names}
return derived


def set_log_level(logger: logging.Logger, verbose: int = 0, quiet: bool = False) -> None:
"""Set log level."""
if verbose == 1:
Expand Down
2 changes: 2 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,8 @@ def test_cli_ps_json(runner: CliRunner) -> None:
assert "policies" in output_data
assert output_data["dataset"] == "289007650"
assert output_data["scope"] == "chime.event.baseband.raw"
# Derived per-storage-element common path and relative file names.
assert "common_paths" in output_data


def test_check_version_banner_on_stderr(monkeypatch, capsys) -> None:
Expand Down
40 changes: 40 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,43 @@ def test_split_more_batches_than_items():
assert len(result) <= len(test_array)
# every element appears exactly once
assert sorted(sum(result, [])) == sorted(test_array)


def test_common_paths() -> None:
"""Test common path derivation per storage element."""
derived = utilities.common_paths(
{
"minoc": [
"cadc:CHIMEFRB/data/event/1/a.h5",
"cadc:CHIMEFRB/data/event/1/b.h5",
"cadc:CHIMEFRB/data/event/1/sub/c.h5",
],
"arc": ["/arc/projects/chime_frb/data/event/1/a.h5"],
"empty": [],
}
)
assert derived["minoc"] == {
"common_path": "cadc:CHIMEFRB/data/event/1",
"files": ["a.h5", "b.h5", "sub/c.h5"],
}
assert derived["arc"] == {
"common_path": "/arc/projects/chime_frb/data/event/1",
"files": ["a.h5"],
}
# A storage element with no files is omitted, not invented.
assert "empty" not in derived


def test_common_paths_no_usable_split() -> None:
"""Test elements with no common directory keep their original paths."""
derived = utilities.common_paths(
{
"mixed": ["/abs/a.h5", "rel/b.h5"],
"no_common": ["a/b.h5", "c/d.h5"],
"bare": ["a.h5"],
}
)
# No usable split: common_path is "" and the original paths survive.
assert derived["mixed"] == {"common_path": "", "files": ["/abs/a.h5", "rel/b.h5"]}
assert derived["no_common"] == {"common_path": "", "files": ["a/b.h5", "c/d.h5"]}
assert derived["bare"] == {"common_path": "", "files": ["a.h5"]}
Loading