Skip to content

Commit 4e407a1

Browse files
committed
feat: add decorator to check if las file is written correctly
1 parent 29a38e0 commit 4e407a1

7 files changed

Lines changed: 115 additions & 6 deletions

pdaltools/check_las.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Check if a LAS file is written correcty and can be opened by PDAL"""
2+
3+
import functools
4+
import time
5+
6+
import pdal
7+
8+
9+
def check_pdal_can_open_file(filepath: str) -> bool:
10+
try:
11+
pipeline = pdal.Reader(filepath).pipeline()
12+
pipeline.execute()
13+
return True
14+
except RuntimeError as e:
15+
if e.__str__().startswith("readers.las:"):
16+
print(f"Pdal could not read {filepath} due to error: {e}")
17+
return False
18+
else:
19+
# For example: file not found
20+
raise e
21+
22+
23+
def check_pdal_can_open_file_with_retry(filepath: str, delay: int) -> bool:
24+
if check_pdal_can_open_file(filepath):
25+
return True
26+
else:
27+
# Sometimes TScan has not fully written the output file yet when we parse the report.
28+
# So we try again after a delay
29+
print(f"New attempt in {delay} seconds.")
30+
time.sleep(delay)
31+
return check_pdal_can_open_file(filepath)
32+
33+
34+
def check_pdal_can_open_file_with_retry_decorator(delay: int):
35+
"""Decorator factory to check if pdal can open a file (with one retry after `delay` seconds).
36+
37+
CAUTION: The decorated function must have the path to a las/laz input file as its first argument.
38+
39+
Usage::
40+
41+
@check_pdal_can_open_file_with_retry_decorator(delay=5)
42+
def process(filepath, ...):
43+
...
44+
"""
45+
46+
def decorator(fn):
47+
@functools.wraps(fn)
48+
def wrapper(filepath: str, *args, **kwargs):
49+
if not check_pdal_can_open_file_with_retry(filepath, delay):
50+
raise RuntimeError(f"Pdal could not read {filepath} after retry.")
51+
return fn(filepath, *args, **kwargs)
52+
53+
return wrapper
54+
55+
return decorator

pdaltools/color.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pdal
77

88
import pdaltools.las_info as las_info
9+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
910
from pdaltools.download_image import download_image
1011

1112

@@ -31,6 +32,7 @@ def match_min_max_with_pixel_size(min_d: float, max_d: float, pixel_per_meter: f
3132
return min_d, max_d
3233

3334

35+
@check_pdal_can_open_file_with_retry_decorator(delay=10)
3436
def color_from_stream(
3537
input_file: str,
3638
output_file: str,
@@ -156,6 +158,7 @@ def color_from_stream(
156158
return tmp_ortho, tmp_ortho_irc
157159

158160

161+
@check_pdal_can_open_file_with_retry_decorator(delay=10)
159162
def color_from_files(
160163
input_file: str,
161164
output_file: str,

pdaltools/las_add_extra_dims_from_las.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import laspy
1616
import numpy as np
1717

18+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
19+
1820
_LAS_SUFFIXES = frozenset({".las", ".laz"})
1921
logger = logging.getLogger(__name__)
2022
# ANSI for terminal emphasis (ignored by non-TTY handlers in most setups).
@@ -160,6 +162,7 @@ def _dims_to_copy(base: laspy.LasData, source: laspy.LasData, dimensions: Option
160162
return [d for d in requested if d in missing]
161163

162164

165+
@check_pdal_can_open_file_with_retry_decorator(delay=10)
163166
def add_extra_dims_from_las(
164167
base_las: Path | str,
165168
source_las: Path | str,

pdaltools/standardize_format.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import pdal
1717

18+
from pdaltools.check_las import check_pdal_can_open_file_with_retry_decorator
1819
from pdaltools.las_rename_dimension import rename_dimension
1920
from pdaltools.unlock_file import copy_and_hack_decorator
2021

@@ -78,17 +79,14 @@ def get_writer_parameters(new_parameters: Dict) -> Dict:
7879
return params
7980

8081

82+
@check_pdal_can_open_file_with_retry_decorator(delay=10)
8183
@copy_and_hack_decorator
8284
def standardize(
83-
input_file: str,
84-
output_file: str,
85-
params_from_parser: Dict,
86-
classes_to_remove: List = [],
87-
rename_dims: List = []
85+
input_file: str, output_file: str, params_from_parser: Dict, classes_to_remove: List = [], rename_dims: List = []
8886
) -> None:
8987
"""
9088
Standardize a LAS/LAZ file with improved error handling and resource management.
91-
89+
9290
Args:
9391
input_file: Input file path
9492
output_file: Output file path
450 KB
Binary file not shown.
610 KB
Binary file not shown.

test/test_check_las.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Check if a LAS file is written correcty and can be opened by PDAL"""
2+
3+
import os
4+
5+
import pytest
6+
7+
from pdaltools.check_las import (
8+
check_pdal_can_open_file,
9+
check_pdal_can_open_file_with_retry,
10+
check_pdal_can_open_file_with_retry_decorator,
11+
)
12+
13+
TEST_PATH = os.path.dirname(os.path.abspath(__file__))
14+
INPUT_DIR = os.path.join(TEST_PATH, "data/check_las")
15+
16+
17+
def test_check_pdal_can_open_file():
18+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
19+
assert check_pdal_can_open_file(filepath)
20+
21+
22+
def test_check_pdal_can_open_file_nok():
23+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
24+
assert not check_pdal_can_open_file(filepath)
25+
26+
27+
def test_check_pdal_can_open_file_with_retry():
28+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
29+
assert check_pdal_can_open_file_with_retry(filepath, 10)
30+
31+
32+
def test_check_pdal_can_open_file_with_retry_nok():
33+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
34+
assert not check_pdal_can_open_file_with_retry(filepath, 10)
35+
36+
37+
@check_pdal_can_open_file_with_retry_decorator(delay=0) # or mock time.sleep
38+
def echo(filepath):
39+
return filepath
40+
41+
42+
def test_decorator_passes_through_on_ok():
43+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_ok.laz")
44+
assert echo(filepath) == filepath
45+
46+
47+
def test_decorator_raises_on_nok():
48+
filepath = os.path.join(INPUT_DIR, "Semis_2022_0906_6665_LA93_IGN69_nok.laz")
49+
with pytest.raises(RuntimeError, match="after retry"):
50+
echo(filepath)

0 commit comments

Comments
 (0)