dolphin 0.22.0__py3-none-any.whl
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.
- dolphin/__init__.py +9 -0
- dolphin/__main__.py +9 -0
- dolphin/_cli_timeseries.py +144 -0
- dolphin/_cli_unwrap.py +150 -0
- dolphin/_decorators.py +148 -0
- dolphin/_log.py +137 -0
- dolphin/_overviews.py +174 -0
- dolphin/_show_versions.py +130 -0
- dolphin/_types.py +136 -0
- dolphin/_version.py +16 -0
- dolphin/atmosphere/__init__.py +33 -0
- dolphin/atmosphere/_netcdf.py +521 -0
- dolphin/atmosphere/_utils.py +241 -0
- dolphin/atmosphere/ionosphere.py +416 -0
- dolphin/atmosphere/model_levels.py +1247 -0
- dolphin/atmosphere/troposphere.py +505 -0
- dolphin/atmosphere/weather_model.py +867 -0
- dolphin/baseline.py +157 -0
- dolphin/cli.py +34 -0
- dolphin/filtering.py +194 -0
- dolphin/goldstein.py +99 -0
- dolphin/interferogram.py +780 -0
- dolphin/interpolation.py +214 -0
- dolphin/io/__init__.py +8 -0
- dolphin/io/_background.py +264 -0
- dolphin/io/_blocks.py +349 -0
- dolphin/io/_core.py +779 -0
- dolphin/io/_paths.py +197 -0
- dolphin/io/_process.py +58 -0
- dolphin/io/_readers.py +1078 -0
- dolphin/io/_utils.py +264 -0
- dolphin/io/_writers.py +479 -0
- dolphin/log-config.json +45 -0
- dolphin/masking.py +147 -0
- dolphin/phase_link/__init__.py +9 -0
- dolphin/phase_link/_compress.py +45 -0
- dolphin/phase_link/_core.jl +19 -0
- dolphin/phase_link/_core.py +507 -0
- dolphin/phase_link/_eigenvalues.py +205 -0
- dolphin/phase_link/_ps_filling.py +153 -0
- dolphin/phase_link/covariance.py +195 -0
- dolphin/phase_link/metrics.py +95 -0
- dolphin/phase_link/simulate.py +433 -0
- dolphin/ps.py +489 -0
- dolphin/py.typed +0 -0
- dolphin/shp/__init__.py +117 -0
- dolphin/shp/_common.py +79 -0
- dolphin/shp/_glrt.py +206 -0
- dolphin/shp/_ks.py +243 -0
- dolphin/shp/glrt_cutoffs.csv +1201 -0
- dolphin/similarity.py +325 -0
- dolphin/stack.py +466 -0
- dolphin/stitching.py +631 -0
- dolphin/timeseries.py +926 -0
- dolphin/unwrap/__init__.py +5 -0
- dolphin/unwrap/_constants.py +11 -0
- dolphin/unwrap/_isce3.py +153 -0
- dolphin/unwrap/_post_process.py +127 -0
- dolphin/unwrap/_snaphu_py.py +227 -0
- dolphin/unwrap/_tophu.py +189 -0
- dolphin/unwrap/_unwrap.py +441 -0
- dolphin/unwrap/_unwrap_3d.py +108 -0
- dolphin/unwrap/_utils.py +162 -0
- dolphin/utils.py +685 -0
- dolphin/workflows/__init__.py +4 -0
- dolphin/workflows/_cli_config.py +407 -0
- dolphin/workflows/_cli_run.py +70 -0
- dolphin/workflows/_utils.py +32 -0
- dolphin/workflows/config/__init__.py +9 -0
- dolphin/workflows/config/_common.py +436 -0
- dolphin/workflows/config/_displacement.py +237 -0
- dolphin/workflows/config/_enums.py +33 -0
- dolphin/workflows/config/_ps.py +77 -0
- dolphin/workflows/config/_unwrap_options.py +265 -0
- dolphin/workflows/config/_unwrapping.py +74 -0
- dolphin/workflows/config/_yaml_model.py +220 -0
- dolphin/workflows/displacement.py +358 -0
- dolphin/workflows/ps.py +120 -0
- dolphin/workflows/sequential.py +183 -0
- dolphin/workflows/single.py +422 -0
- dolphin/workflows/stitching_bursts.py +168 -0
- dolphin/workflows/unwrapping.py +118 -0
- dolphin/workflows/wrapped_phase.py +386 -0
- dolphin-0.22.0.dist-info/LICENSE +236 -0
- dolphin-0.22.0.dist-info/METADATA +149 -0
- dolphin-0.22.0.dist-info/RECORD +89 -0
- dolphin-0.22.0.dist-info/WHEEL +5 -0
- dolphin-0.22.0.dist-info/entry_points.txt +2 -0
- dolphin-0.22.0.dist-info/top_level.txt +1 -0
dolphin/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# flake8: noqa
|
|
2
|
+
# version.py is autogenerated by setuptools_scm
|
|
3
|
+
from dolphin._version import version as __version__
|
|
4
|
+
|
|
5
|
+
from ._log import *
|
|
6
|
+
from ._show_versions import *
|
|
7
|
+
from ._types import *
|
|
8
|
+
from .goldstein import goldstein
|
|
9
|
+
from .interpolation import interpolate
|
dolphin/__main__.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from dolphin.workflows import CallFunc
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
|
|
9
|
+
else:
|
|
10
|
+
_SubparserType = Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_parser(subparser=None, subcommand_name="timeseries") -> argparse.ArgumentParser:
|
|
14
|
+
"""Set up the command line interface."""
|
|
15
|
+
metadata = {
|
|
16
|
+
"description": "Create a configuration file for a displacement workflow.",
|
|
17
|
+
"formatter_class": argparse.ArgumentDefaultsHelpFormatter,
|
|
18
|
+
# https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars
|
|
19
|
+
"fromfile_prefix_chars": "@",
|
|
20
|
+
}
|
|
21
|
+
if subparser:
|
|
22
|
+
# Used by the subparser to make a nested command line interface
|
|
23
|
+
parser = subparser.add_parser(subcommand_name, **metadata)
|
|
24
|
+
else:
|
|
25
|
+
parser = argparse.ArgumentParser(**metadata) # type: ignore[arg-type]
|
|
26
|
+
|
|
27
|
+
# parser._action_groups.pop()
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-o",
|
|
30
|
+
"--output-dir",
|
|
31
|
+
default=Path(),
|
|
32
|
+
help="Path to output directory to store results",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--unwrapped-paths",
|
|
36
|
+
nargs=argparse.ZERO_OR_MORE,
|
|
37
|
+
help=(
|
|
38
|
+
"List the paths of all unwrapped interferograms. Can pass a "
|
|
39
|
+
"newline delimited file with @ifg_filelist.txt"
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--conncomp-paths",
|
|
44
|
+
nargs=argparse.ZERO_OR_MORE,
|
|
45
|
+
help=(
|
|
46
|
+
"List the paths of all connected component files. Can pass a "
|
|
47
|
+
"newline delimited file with @conncomp_filelist.txt"
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--corr-paths",
|
|
52
|
+
nargs=argparse.ZERO_OR_MORE,
|
|
53
|
+
help=(
|
|
54
|
+
"List the paths of all correlation files. Can pass a newline delimited"
|
|
55
|
+
" file with @cor_filelist.txt"
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--condition-file",
|
|
60
|
+
help=(
|
|
61
|
+
"A file with the same size as each raster, like amplitude dispersion or"
|
|
62
|
+
"temporal coherence to find reference point. default: amplitude dispersion"
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--condition",
|
|
67
|
+
type=CallFunc,
|
|
68
|
+
default=CallFunc.MIN,
|
|
69
|
+
help=(
|
|
70
|
+
"A condition to apply to condition file to find the reference point"
|
|
71
|
+
"Options are [min, max]. default=min"
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--num-threads",
|
|
76
|
+
type=int,
|
|
77
|
+
default=5,
|
|
78
|
+
help="Number of threads for the inversion",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--run-velocity",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Run the velocity estimation from the phase time series",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--reference-point",
|
|
87
|
+
type=int,
|
|
88
|
+
nargs=2,
|
|
89
|
+
metavar=("ROW", "COL"),
|
|
90
|
+
default=(-1, -1),
|
|
91
|
+
help=(
|
|
92
|
+
"Reference point (row, col) used if performing a time series inversion. "
|
|
93
|
+
"If not provided, a point will be selected from a consistent connected "
|
|
94
|
+
"component with low amplitude dispersion or high temporal coherence."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--correlation-threshold",
|
|
99
|
+
type=range_limited_float_type,
|
|
100
|
+
default=0.2,
|
|
101
|
+
metavar="[0-1]",
|
|
102
|
+
help="Pixels with correlation below this value will be masked out.",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
parser.set_defaults(run_func=_run_timeseries)
|
|
106
|
+
|
|
107
|
+
return parser
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def range_limited_float_type(arg):
|
|
111
|
+
"""Type function for argparse - a float within some predefined bounds."""
|
|
112
|
+
try:
|
|
113
|
+
f = float(arg)
|
|
114
|
+
except ValueError as err:
|
|
115
|
+
raise argparse.ArgumentTypeError("Must be a floating point number") from err
|
|
116
|
+
if f < 0 or f > 1:
|
|
117
|
+
raise argparse.ArgumentTypeError(
|
|
118
|
+
"Argument must be < " + str(1) + "and > " + str(0)
|
|
119
|
+
)
|
|
120
|
+
return f
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _run_timeseries(*args, **kwargs):
|
|
124
|
+
"""Run `dolphin.timeseries.run`.
|
|
125
|
+
|
|
126
|
+
Wrapper for the dolphin.timeseries to invert and create velocity.
|
|
127
|
+
"""
|
|
128
|
+
from dolphin import timeseries
|
|
129
|
+
|
|
130
|
+
return timeseries.run(*args, **kwargs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(args=None):
|
|
134
|
+
"""Get the command line arguments for timeseries inversion."""
|
|
135
|
+
from dolphin import timeseries
|
|
136
|
+
|
|
137
|
+
parser = get_parser()
|
|
138
|
+
parsed_args = parser.parse_args(args)
|
|
139
|
+
|
|
140
|
+
timeseries.run(**vars(parsed_args))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
dolphin/_cli_unwrap.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from dolphin.workflows.config import UnwrapMethod
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
_SubparserType = argparse._SubParsersAction[argparse.ArgumentParser]
|
|
9
|
+
else:
|
|
10
|
+
_SubparserType = Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_parser(subparser=None, subcommand_name="unwrap") -> argparse.ArgumentParser:
|
|
14
|
+
"""Set up the command line interface."""
|
|
15
|
+
metadata = {
|
|
16
|
+
"description": "Create a configuration file for a displacement workflow.",
|
|
17
|
+
"formatter_class": argparse.ArgumentDefaultsHelpFormatter,
|
|
18
|
+
# https://docs.python.org/3/library/argparse.html#fromfile-prefix-chars
|
|
19
|
+
"fromfile_prefix_chars": "@",
|
|
20
|
+
}
|
|
21
|
+
if subparser:
|
|
22
|
+
# Used by the subparser to make a nested command line interface
|
|
23
|
+
parser = subparser.add_parser(subcommand_name, **metadata)
|
|
24
|
+
else:
|
|
25
|
+
parser = argparse.ArgumentParser(**metadata) # type: ignore[arg-type]
|
|
26
|
+
|
|
27
|
+
# parser._action_groups.pop()
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-o",
|
|
30
|
+
"--output-path",
|
|
31
|
+
default=Path(),
|
|
32
|
+
help="Path to output directory to store results",
|
|
33
|
+
)
|
|
34
|
+
# Get Inputs from the command line
|
|
35
|
+
inputs = parser.add_argument_group("Input options")
|
|
36
|
+
inputs.add_argument(
|
|
37
|
+
"--ifg-filenames",
|
|
38
|
+
nargs=argparse.ZERO_OR_MORE,
|
|
39
|
+
help=(
|
|
40
|
+
"List the paths of all ifg files to include. Can pass a newline delimited"
|
|
41
|
+
" file with @ifg_filelist.txt"
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
inputs.add_argument(
|
|
45
|
+
"--cor-filenames",
|
|
46
|
+
nargs=argparse.ZERO_OR_MORE,
|
|
47
|
+
help=(
|
|
48
|
+
"List the paths of all ifg files to include. Can pass a newline delimited"
|
|
49
|
+
" file with @cor_filelist.txt"
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
inputs.add_argument(
|
|
53
|
+
"--mask-filename",
|
|
54
|
+
help=(
|
|
55
|
+
"Path to Byte mask file used to ignore low correlation/bad data (e.g water"
|
|
56
|
+
" mask). Convention is 0 for no data/invalid, and 1 for good data."
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
inputs.add_argument(
|
|
60
|
+
"--temp-coh-filename",
|
|
61
|
+
help="Path to temporal coherence file from phase linking",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--nlooks",
|
|
65
|
+
type=int,
|
|
66
|
+
help="Effective number of looks used to form correlation",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--max-jobs",
|
|
71
|
+
type=int,
|
|
72
|
+
default=1,
|
|
73
|
+
help="Number of parallel files to unwrap",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
algorithm_opts = parser.add_argument_group("Algorithm options")
|
|
77
|
+
algorithm_opts.add_argument(
|
|
78
|
+
"--unwrap-method",
|
|
79
|
+
type=UnwrapMethod,
|
|
80
|
+
choices=[m.value for m in UnwrapMethod],
|
|
81
|
+
default=UnwrapMethod.SNAPHU.value,
|
|
82
|
+
help="Choice of unwrapping algorithm to use.",
|
|
83
|
+
)
|
|
84
|
+
algorithm_opts.add_argument(
|
|
85
|
+
"--run-goldstein",
|
|
86
|
+
action="store_true",
|
|
87
|
+
help="Run Goldstein filter before unwrapping.",
|
|
88
|
+
)
|
|
89
|
+
algorithm_opts.add_argument(
|
|
90
|
+
"--run-interpolation",
|
|
91
|
+
action="store_true",
|
|
92
|
+
help="Run interpolation before unwrapping.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
spurt_opts = parser.add_argument_group("Spurt options")
|
|
96
|
+
spurt_opts.add_argument(
|
|
97
|
+
"--temp-coh-threshold",
|
|
98
|
+
type=float,
|
|
99
|
+
help="Cutoff on temporal_coherence raster to choose pixels for unwrapping.",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
tophu_opts = parser.add_argument_group("Tophu options")
|
|
103
|
+
# Add ability for downsampling/tiling with tophu
|
|
104
|
+
tophu_opts.add_argument(
|
|
105
|
+
"--ntiles",
|
|
106
|
+
type=int,
|
|
107
|
+
nargs=2,
|
|
108
|
+
metavar=("ROW_TILES", "COL_TILES"),
|
|
109
|
+
default=(1, 1),
|
|
110
|
+
help=(
|
|
111
|
+
"(using tophu) Split the interferograms into this number of tiles along the"
|
|
112
|
+
" (row, col) axis."
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
tophu_opts.add_argument(
|
|
116
|
+
"--downsample-factor",
|
|
117
|
+
type=int,
|
|
118
|
+
nargs=2,
|
|
119
|
+
default=(1, 1),
|
|
120
|
+
help=(
|
|
121
|
+
"(using tophu) Downsample the interferograms by this factor "
|
|
122
|
+
" during multiresolution unwrapping."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
parser.set_defaults(run_func=_run_unwrap)
|
|
126
|
+
|
|
127
|
+
return parser
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _run_unwrap(*args, **kwargs):
|
|
131
|
+
"""Run `dolphin.unwrap.run`.
|
|
132
|
+
|
|
133
|
+
Wrapper for the dolphin.unwrap to delay import time.
|
|
134
|
+
"""
|
|
135
|
+
from dolphin import unwrap
|
|
136
|
+
|
|
137
|
+
return unwrap.run(*args, **kwargs)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def main(args=None):
|
|
141
|
+
"""Get the command line arguments and unwrap files."""
|
|
142
|
+
from dolphin import unwrap
|
|
143
|
+
|
|
144
|
+
parser = get_parser()
|
|
145
|
+
parsed_args = parser.parse_args(args)
|
|
146
|
+
unwrap.run(**vars(parsed_args))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
main()
|
dolphin/_decorators.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
import shutil
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"atomic_output",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def atomic_output(
|
|
18
|
+
output_arg: str = "output_file",
|
|
19
|
+
is_dir: bool = False,
|
|
20
|
+
use_tmp: bool = False,
|
|
21
|
+
overwrite: bool = False,
|
|
22
|
+
) -> Callable:
|
|
23
|
+
"""Use a temporary file/directory for the `output_arg` until the function finishes.
|
|
24
|
+
|
|
25
|
+
Decorator is used on a function which writes to an output file/directory in blocks.
|
|
26
|
+
If the function were interrupted, the file/directory would be partially complete.
|
|
27
|
+
|
|
28
|
+
This decorator replaces the final output name with a temp file/dir, and then
|
|
29
|
+
renames the temp file/dir to the final name after the function finishes.
|
|
30
|
+
|
|
31
|
+
Note that when `is_dir=True`, `output_arg` can be a directory (if multiple files
|
|
32
|
+
are being written to). In this case, the entire directory is temporary, and
|
|
33
|
+
renamed after the function finishes.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
output_arg : str, optional
|
|
38
|
+
The name of the argument to replace, by default 'output_file'
|
|
39
|
+
is_dir : bool, default = False
|
|
40
|
+
If `True`, the output argument is a directory, not a file
|
|
41
|
+
use_tmp : bool, default = False
|
|
42
|
+
If `False`, uses the parent directory of the desired output, with
|
|
43
|
+
a random suffix added to the name to distinguish from actual output.
|
|
44
|
+
If `True`, uses the `/tmp` directory (or wherever the default is
|
|
45
|
+
for the `tempfile` module).
|
|
46
|
+
overwrite : bool, default = False
|
|
47
|
+
Overwrite an existing file.
|
|
48
|
+
If `False` raises `FileExistsError` if the file already exists.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
Callable
|
|
53
|
+
The decorated function
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
FileExistsError
|
|
58
|
+
if the file for `output_arg` already exists (if out_dir=False), or
|
|
59
|
+
if the directory at `output_arg` exists and is non-empty.
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
The output at `output_arg` *must not* exist already, or the decorator will error
|
|
64
|
+
(though if `is_dir=True`, it is allowed to be an empty directory).
|
|
65
|
+
The function being decorated *must* be called with keyword args for `output_arg`.
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def decorator(func: Callable) -> Callable:
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
72
|
+
# Extract the output file path
|
|
73
|
+
if kwargs.get(output_arg):
|
|
74
|
+
final_out_name = kwargs[output_arg]
|
|
75
|
+
else:
|
|
76
|
+
msg = (
|
|
77
|
+
f"Argument {output_arg} not passed to function {func.__name__}:"
|
|
78
|
+
f" {kwargs}"
|
|
79
|
+
)
|
|
80
|
+
raise FileExistsError(msg)
|
|
81
|
+
|
|
82
|
+
final_path = Path(final_out_name)
|
|
83
|
+
# Make sure the desired final output doesn't already exist
|
|
84
|
+
_raise_if_exists(final_path, is_dir=is_dir, overwrite=overwrite)
|
|
85
|
+
# None means that tempfile will use /tmp
|
|
86
|
+
tmp_dir = final_path.parent if not use_tmp else None
|
|
87
|
+
|
|
88
|
+
# Make the tempfile start the same as the desired output
|
|
89
|
+
prefix = final_path.name
|
|
90
|
+
if is_dir:
|
|
91
|
+
# Create a temporary directory
|
|
92
|
+
temp_path = tempfile.mkdtemp(dir=tmp_dir, prefix=prefix)
|
|
93
|
+
else:
|
|
94
|
+
# Create a temporary file
|
|
95
|
+
suffix = final_path.suffix
|
|
96
|
+
_, temp_path = tempfile.mkstemp(
|
|
97
|
+
dir=tmp_dir, prefix=prefix, suffix=suffix
|
|
98
|
+
)
|
|
99
|
+
logger.debug("Writing to temp file %s instead of %s", temp_path, final_path)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Replace the output file path with the temp file
|
|
103
|
+
# It would be like this if we only allows keyword:
|
|
104
|
+
kwargs[output_arg] = temp_path
|
|
105
|
+
# Execute the original function
|
|
106
|
+
result = func(*args, **kwargs)
|
|
107
|
+
# Move the temp file to the final location
|
|
108
|
+
logger.debug("Moving %s to %s", temp_path, final_path)
|
|
109
|
+
shutil.move(temp_path, final_path)
|
|
110
|
+
|
|
111
|
+
return result
|
|
112
|
+
finally:
|
|
113
|
+
logger.debug("Cleaning up temp file %s", temp_path)
|
|
114
|
+
# Different cleanup is needed
|
|
115
|
+
if is_dir:
|
|
116
|
+
shutil.rmtree(temp_path, ignore_errors=True)
|
|
117
|
+
else:
|
|
118
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
119
|
+
|
|
120
|
+
return wrapper
|
|
121
|
+
|
|
122
|
+
return decorator
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _raise_if_exists(final_path: Path, is_dir: bool, overwrite: bool):
|
|
126
|
+
msg = f"{final_path} already exists"
|
|
127
|
+
if final_path.exists():
|
|
128
|
+
logger.debug(f"{final_path} already exists")
|
|
129
|
+
if overwrite:
|
|
130
|
+
if final_path.is_dir():
|
|
131
|
+
shutil.rmtree(final_path)
|
|
132
|
+
else:
|
|
133
|
+
final_path.unlink()
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if is_dir and final_path.is_dir():
|
|
137
|
+
# We can work with an empty directory
|
|
138
|
+
try:
|
|
139
|
+
final_path.rmdir()
|
|
140
|
+
except OSError as e:
|
|
141
|
+
err_msg = str(e)
|
|
142
|
+
if "Directory not empty" in err_msg:
|
|
143
|
+
raise FileExistsError(msg) from e
|
|
144
|
+
else:
|
|
145
|
+
# Some other error we don't know
|
|
146
|
+
raise
|
|
147
|
+
else:
|
|
148
|
+
raise FileExistsError(msg)
|
dolphin/_log.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import logging.config
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from dolphin._types import P, PathOrStr, T
|
|
13
|
+
|
|
14
|
+
LOG_RECORD_BUILTIN_ATTRS = {
|
|
15
|
+
"args",
|
|
16
|
+
"asctime",
|
|
17
|
+
"created",
|
|
18
|
+
"exc_info",
|
|
19
|
+
"exc_text",
|
|
20
|
+
"filename",
|
|
21
|
+
"funcName",
|
|
22
|
+
"levelname",
|
|
23
|
+
"levelno",
|
|
24
|
+
"lineno",
|
|
25
|
+
"module",
|
|
26
|
+
"msecs",
|
|
27
|
+
"message",
|
|
28
|
+
"msg",
|
|
29
|
+
"name",
|
|
30
|
+
"pathname",
|
|
31
|
+
"process",
|
|
32
|
+
"processName",
|
|
33
|
+
"relativeCreated",
|
|
34
|
+
"stack_info",
|
|
35
|
+
"thread",
|
|
36
|
+
"threadName",
|
|
37
|
+
"taskName",
|
|
38
|
+
}
|
|
39
|
+
__all__ = ["log_runtime"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def setup_logging(debug: bool = False, filename: PathOrStr | None = None):
|
|
43
|
+
config_file = Path(__file__).parent / Path("log-config.json")
|
|
44
|
+
with open(config_file) as f_in:
|
|
45
|
+
config = json.load(f_in)
|
|
46
|
+
|
|
47
|
+
if debug:
|
|
48
|
+
config["loggers"]["dolphin"]["level"] = "DEBUG"
|
|
49
|
+
|
|
50
|
+
if filename:
|
|
51
|
+
config["loggers"]["dolphin"]["handlers"].append("file")
|
|
52
|
+
config["handlers"]["file"]["filename"] = os.fspath(filename)
|
|
53
|
+
Path(filename).parent.mkdir(exist_ok=True, parents=True)
|
|
54
|
+
|
|
55
|
+
if "filename" not in config["handlers"]["file"]:
|
|
56
|
+
# We never passed in a filename: don't log to a file
|
|
57
|
+
config["handlers"].pop("file")
|
|
58
|
+
|
|
59
|
+
logging.config.dictConfig(config)
|
|
60
|
+
# Temp work around for tqdm on py312
|
|
61
|
+
if sys.version_info.major == 3 and sys.version_info.minor == 12:
|
|
62
|
+
os.environ["TQDM_DISABLE"] = "1"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def log_runtime(f: Callable[P, T]) -> Callable[P, T]:
|
|
66
|
+
"""Decorate a function to time how long it takes to run.
|
|
67
|
+
|
|
68
|
+
Usage
|
|
69
|
+
-----
|
|
70
|
+
@log_runtime
|
|
71
|
+
def test_func():
|
|
72
|
+
return 2 + 4
|
|
73
|
+
"""
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
@wraps(f)
|
|
77
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs):
|
|
78
|
+
t1 = time.time()
|
|
79
|
+
|
|
80
|
+
result = f(*args, **kwargs)
|
|
81
|
+
|
|
82
|
+
t2 = time.time()
|
|
83
|
+
elapsed_seconds = t2 - t1
|
|
84
|
+
elapsed_minutes = elapsed_seconds / 60.0
|
|
85
|
+
time_string = (
|
|
86
|
+
f"Total elapsed time for {f.__module__}.{f.__name__} : "
|
|
87
|
+
f"{elapsed_minutes:.2f} minutes ({elapsed_seconds:.2f} seconds)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
logger.info(time_string)
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
return wrapper
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class JSONFormatter(logging.Formatter):
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
fmt_keys: dict[str, str] | None = None,
|
|
102
|
+
):
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.fmt_keys = fmt_keys if fmt_keys is not None else {}
|
|
105
|
+
|
|
106
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
107
|
+
message = self._prepare_log_dict(record)
|
|
108
|
+
return json.dumps(message, default=str)
|
|
109
|
+
|
|
110
|
+
def _prepare_log_dict(self, record: logging.LogRecord):
|
|
111
|
+
always_fields = {
|
|
112
|
+
"message": record.getMessage(),
|
|
113
|
+
"timestamp": datetime.fromtimestamp(
|
|
114
|
+
record.created, tz=timezone.utc
|
|
115
|
+
).isoformat(),
|
|
116
|
+
}
|
|
117
|
+
if record.exc_info is not None:
|
|
118
|
+
always_fields["exc_info"] = self.formatException(record.exc_info)
|
|
119
|
+
|
|
120
|
+
if record.stack_info is not None:
|
|
121
|
+
always_fields["stack_info"] = self.formatStack(record.stack_info)
|
|
122
|
+
|
|
123
|
+
message = {
|
|
124
|
+
key: (
|
|
125
|
+
msg_val
|
|
126
|
+
if (msg_val := always_fields.pop(val, None)) is not None
|
|
127
|
+
else getattr(record, val)
|
|
128
|
+
)
|
|
129
|
+
for key, val in self.fmt_keys.items()
|
|
130
|
+
}
|
|
131
|
+
message.update(always_fields)
|
|
132
|
+
|
|
133
|
+
for key, val in record.__dict__.items():
|
|
134
|
+
if key not in LOG_RECORD_BUILTIN_ATTRS:
|
|
135
|
+
message[key] = val
|
|
136
|
+
|
|
137
|
+
return message
|