Skip to content

Commit d4d4c20

Browse files
authored
feat: add CLI for sanity check (#155)
* feat: add CLI to check sanity of T4 datasets Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * docs: add documentation of t4sanity Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * chore: remove click from dependencies Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: add tqdm Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * feat: rename option Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * docs: update document Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> * refactor: add sanity checker to common as the function Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp> --------- Signed-off-by: ktro2828 <kotaro.uetake@tier4.jp>
1 parent 759ca00 commit d4d4c20

7 files changed

Lines changed: 189 additions & 17 deletions

File tree

docs/apis/common.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
::: t4_devkit.common.io
99

10+
::: t4_devkit.common.sanity
11+
1012
::: t4_devkit.common.serialize
1113

1214
::: t4_devkit.common.timestamp

docs/tutorials/cli.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Following command line tools are supported:
66

77
- `t4viz`: Visualize T4 dataset features.
8+
- `t4sanity`: Sanity checker of T4 datasets.
89

910
### `t4viz`
1011

@@ -91,3 +92,55 @@ t4viz <COMMAND> ... -o <OUTPUT_DIR>
9192
```
9293

9394
Note that if you specify `--output` option, viewer will not be spawned.
95+
96+
### `t4sanity`
97+
98+
`t4sanity` performs sanity checks on T4 datasets, reporting any issues in a structured format.
99+
It checks the dataset directories and versions, tries to load them using the `Tier4` library, and reports any exceptions or warnings.
100+
101+
```shell
102+
$ t4sanity -h
103+
104+
Usage: t4sanity [OPTIONS] DB_PARENT
105+
106+
╭─ Arguments ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮
107+
* db_parent TEXT Path to parent directory of the databases [default: None] [required] │
108+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
109+
╭─ Options ────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
110+
│ --version -v Show the application version and exit. │
111+
│ --include-warning -iw Indicates whether to report any warnings. │
112+
│ --install-completion Install completion for the current shell. │
113+
│ --show-completion Show completion for the current shell, to copy it or customize the installation. │
114+
│ --help -h Show this message and exit. │
115+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
116+
```
117+
118+
#### Examples
119+
120+
As an example, we have the following the dataset structure:
121+
122+
```shell
123+
<DATA_ROOT>
124+
├── dataset1
125+
│ └── <VERSION>
126+
│ ├── annotation
127+
│ ├── data
128+
| ...
129+
├── dataset2
130+
│ ├── annotation
131+
│ ├── data
132+
| ...
133+
...
134+
```
135+
136+
To run sanity check ignoring warnings, providing the path to the parent directory of the datasets:
137+
138+
```shell
139+
t4sanity <DATA_ROOT>
140+
```
141+
142+
To run sanity check and report any warnings, use the `-iw; --include-warning` option:
143+
144+
```shell
145+
t4sanity <DATA_ROOT> -iw
146+
```

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ dependencies = [
1616
"shapely<2.0.0; python_version=='3.10'",
1717
"shapely>=2.0.0; python_version>'3.10'",
1818
"pycocotools>=2.0.8",
19-
"click>=8.1.8",
2019
"pyyaml>=6.0.2",
2120
"typer>=0.15.3",
21+
"tabulate>=0.9.0",
22+
"tqdm>=4.67.1",
2223
]
2324

2425
[dependency-groups]
@@ -36,6 +37,7 @@ dev = [
3637

3738
[project.scripts]
3839
t4viz = "t4_devkit.cli.visualize:cli"
40+
t4sanity = "t4_devkit.cli.sanity:cli"
3941

4042
[tool.ruff]
4143
line-length = 100

t4_devkit/cli/sanity.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import typer
6+
from tabulate import tabulate
7+
from tqdm import tqdm
8+
9+
from t4_devkit.common.sanity import DBException, sanity_check
10+
11+
from .version import version_callback
12+
13+
cli = typer.Typer(
14+
name="t4sanity",
15+
no_args_is_help=True,
16+
context_settings={"help_option_names": ["-h", "--help"]},
17+
pretty_exceptions_enable=False,
18+
)
19+
20+
21+
def _run_sanity_check(db_parent: str, *, include_warning: bool = False) -> list[DBException]:
22+
exceptions: list[DBException] = []
23+
24+
db_dirs: list[Path] = Path(db_parent).glob("*")
25+
26+
for db_root in tqdm(db_dirs, desc=">>>Sanity checking..."):
27+
result = sanity_check(db_root, include_warning=include_warning)
28+
if result:
29+
exceptions.append(result)
30+
return exceptions
31+
32+
33+
@cli.command()
34+
def main(
35+
version: bool = typer.Option(
36+
False,
37+
"--version",
38+
"-v",
39+
help="Show the application version and exit.",
40+
callback=version_callback,
41+
is_eager=True,
42+
),
43+
db_parent: str = typer.Argument(..., help="Path to parent directory of the databases."),
44+
include_warning: bool = typer.Option(
45+
False, "-iw", "--include-warning", help="Indicates whether to report any warnings."
46+
),
47+
) -> None:
48+
exceptions = _run_sanity_check(db_parent, include_warning=include_warning)
49+
50+
headers = ["DatasetID", "Version", "Message"]
51+
table = [[e.dataset_id, e.version, e.message] for e in exceptions]
52+
53+
print(tabulate(table, headers=headers, tablefmt="pretty"))

t4_devkit/cli/version.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import importlib.metadata
5+
6+
import typer
7+
from rich.console import Console
8+
9+
console = Console()
10+
11+
12+
def version_callback(value: bool) -> None:
13+
if value:
14+
version = importlib.metadata.version("t4-devkit")
15+
console.print(f"[bold green]t4viz[/bold green]: [cyan]{version}[/cyan]")
16+
raise typer.Exit()

t4_devkit/cli/visualize.py

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
from __future__ import annotations
22

3-
import importlib
4-
import importlib.metadata
53
import os
64
from typing import Annotated
75

86
import typer
9-
from rich.console import Console
107

118
from t4_devkit import Tier4
129

13-
console = Console()
10+
from .version import version_callback
1411

1512
cli = typer.Typer(
1613
name="t4viz",
@@ -123,26 +120,15 @@ def _create_dir(dir_path: str | None) -> None:
123120
os.makedirs(dir_path, exist_ok=True)
124121

125122

126-
def _version_callback(value: bool):
127-
if value:
128-
version = importlib.metadata.version("t4-devkit")
129-
console.print(f"[bold green]t4viz[/bold green]: [cyan]{version}[/cyan]")
130-
raise typer.Exit()
131-
132-
133123
@cli.callback()
134124
def main(
135125
version: bool = typer.Option(
136126
False,
137127
"--version",
138128
"-v",
139129
help="Show the application version and exit.",
140-
callback=_version_callback,
130+
callback=version_callback,
141131
is_eager=True,
142132
),
143133
) -> None:
144134
pass
145-
146-
147-
if __name__ == "__main__":
148-
cli()

t4_devkit/common/sanity.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from __future__ import annotations
2+
3+
import re
4+
import warnings
5+
from pathlib import Path
6+
7+
from attrs import define
8+
9+
from t4_devkit import Tier4
10+
11+
__all__ = ["DBException", "sanity_check"]
12+
13+
14+
@define
15+
class DBException:
16+
"""A dataclass to store error message of the corresponding dataset."""
17+
18+
dataset_id: str
19+
version: str | None
20+
message: str
21+
22+
23+
def sanity_check(db_root: str | Path, *, include_warning: bool = False) -> DBException | None:
24+
"""Perform sanity check and report exception or warning encountered while loading the dataset.
25+
26+
Args:
27+
db_root (str | Path): Path to root directory of the dataset.
28+
include_warning (bool, optional): Indicates whether to report warnings.
29+
30+
Returns:
31+
Exception or warning if exits, otherwise returns None.
32+
"""
33+
db_root_path = Path(db_root)
34+
35+
version_pattern = re.compile(r".*/\d+$")
36+
versions = [d.name for d in db_root_path.iterdir() if version_pattern.match(str(d))]
37+
38+
if versions:
39+
version = sorted(versions)[-1]
40+
data_root = db_root_path.joinpath(version).as_posix()
41+
else:
42+
version = None
43+
data_root = db_root_path.as_posix()
44+
45+
with warnings.catch_warnings():
46+
if include_warning:
47+
warnings.filterwarnings("error")
48+
else:
49+
warnings.filterwarnings("ignore")
50+
51+
try:
52+
_ = Tier4("annotation", data_root=data_root, verbose=False)
53+
exception = None
54+
except Exception as e:
55+
exception = DBException(
56+
dataset_id=db_root_path.name,
57+
version=version,
58+
message=str(e),
59+
)
60+
return exception

0 commit comments

Comments
 (0)