Skip to content

Commit adc2c60

Browse files
os.path converted to pathlib.Path (#94)
* os.path converted to pathlib.Path everywhere * rewrite & simplify SigMFArchive * read file modification times in convert_wav --------- Co-authored-by: Teque5 <[email protected]>
1 parent a7d9ae0 commit adc2c60

File tree

11 files changed

+229
-221
lines changed

11 files changed

+229
-221
lines changed

sigmf/apps/convert_wav.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,72 +9,78 @@
99
import argparse
1010
import getpass
1111
import logging
12-
import os
13-
import pathlib
1412
import tempfile
13+
from datetime import datetime, timezone
14+
from os import PathLike
15+
from pathlib import Path
1516
from typing import Optional
1617

1718
from scipy.io import wavfile
1819

19-
from .. import SigMFFile, __specification__
20+
from .. import SigMFFile
2021
from .. import __version__ as toolversion
21-
from .. import archive
22-
from ..utils import get_data_type_str, get_sigmf_iso8601_datetime_now
22+
from ..sigmffile import get_sigmf_filenames
23+
from ..utils import SIGMF_DATETIME_ISO8601_FMT, get_data_type_str
2324

2425
log = logging.getLogger()
2526

2627

2728
def convert_wav(
28-
input_wav_filename: str,
29-
archive_filename: Optional[str],
30-
start_datetime: Optional[str] = None,
29+
wav_path: str,
30+
out_path: Optional[str] = None,
3131
author: Optional[str] = None,
32-
):
32+
) -> PathLike:
3333
"""
34-
read a .wav and write a .sigmf archive
34+
Read a wav and write a sigmf archive.
3535
"""
36-
input_path = pathlib.Path(input_wav_filename)
37-
input_stem = input_path.stem
38-
samp_rate, wav_data = wavfile.read(input_wav_filename)
36+
wav_path = Path(wav_path)
37+
wav_stem = wav_path.stem
38+
samp_rate, wav_data = wavfile.read(wav_path)
3939

4040
global_info = {
4141
SigMFFile.AUTHOR_KEY: getpass.getuser() if author is None else author,
4242
SigMFFile.DATATYPE_KEY: get_data_type_str(wav_data),
43-
SigMFFile.DESCRIPTION_KEY: f"Converted from {input_wav_filename}",
43+
SigMFFile.DESCRIPTION_KEY: f"converted from {wav_path.name}",
4444
SigMFFile.NUM_CHANNELS_KEY: 1 if len(wav_data.shape) < 2 else wav_data.shape[1],
45-
SigMFFile.RECORDER_KEY: os.path.basename(__file__),
45+
SigMFFile.RECORDER_KEY: "Official SigMF wav converter",
4646
SigMFFile.SAMPLE_RATE_KEY: samp_rate,
47-
SigMFFile.VERSION_KEY: __specification__,
4847
}
4948

50-
if start_datetime is None:
51-
start_datetime = get_sigmf_iso8601_datetime_now()
49+
modify_time = wav_path.lstat().st_mtime
50+
wav_datetime = datetime.fromtimestamp(modify_time, tz=timezone.utc)
5251

5352
capture_info = {
5453
SigMFFile.START_INDEX_KEY: 0,
55-
SigMFFile.DATETIME_KEY: start_datetime,
54+
SigMFFile.DATETIME_KEY: wav_datetime.strftime(SIGMF_DATETIME_ISO8601_FMT),
5655
}
5756

58-
tmpdir = tempfile.mkdtemp()
59-
sigmf_data_filename = input_stem + archive.SIGMF_DATASET_EXT
60-
sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename)
61-
wav_data.tofile(sigmf_data_path)
57+
temp_dir = Path(tempfile.mkdtemp())
58+
if out_path is None:
59+
# extension will be changed
60+
out_path = Path(wav_stem)
61+
else:
62+
out_path = Path(out_path)
63+
filenames = get_sigmf_filenames(out_path)
6264

63-
meta = SigMFFile(data_file=sigmf_data_path, global_info=global_info)
65+
data_path = temp_dir / filenames["data_fn"]
66+
wav_data.tofile(data_path)
67+
68+
meta = SigMFFile(data_file=data_path, global_info=global_info)
6469
meta.add_capture(0, metadata=capture_info)
70+
log.debug("created %r", meta)
6571

66-
if archive_filename is None:
67-
archive_filename = input_stem + archive.SIGMF_ARCHIVE_EXT
68-
meta.tofile(archive_filename, toarchive=True)
69-
return os.path.abspath(archive_filename)
72+
arc_path = filenames["archive_fn"]
73+
meta.tofile(arc_path, toarchive=True)
74+
log.info("wrote %s", arc_path)
75+
return arc_path
7076

7177

72-
def main():
78+
def main() -> None:
7379
"""
7480
entry-point for sigmf_convert_wav
7581
"""
76-
parser = argparse.ArgumentParser(description="Convert .wav to .sigmf container.")
77-
parser.add_argument("input", type=str, help="Wavfile path")
82+
parser = argparse.ArgumentParser(description="Convert wav to sigmf archive.")
83+
parser.add_argument("input", type=str, help="wav path")
7884
parser.add_argument("--author", type=str, default=None, help=f"set {SigMFFile.AUTHOR_KEY} metadata")
7985
parser.add_argument("-v", "--verbose", action="count", default=0)
8086
parser.add_argument("--version", action="version", version=f"%(prog)s v{toolversion}")
@@ -87,11 +93,10 @@ def main():
8793
}
8894
logging.basicConfig(level=level_lut[min(args.verbose, 2)])
8995

90-
out_fname = convert_wav(
91-
input_wav_filename=args.input,
96+
_ = convert_wav(
97+
wav_path=args.input,
9298
author=args.author,
9399
)
94-
log.info(f"Write {out_fname}")
95100

96101

97102
if __name__ == "__main__":

sigmf/archive.py

Lines changed: 110 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
"""Create and extract SigMF archives."""
88

99
import io
10-
import os
1110
import shutil
1211
import tarfile
1312
import tempfile
13+
from pathlib import Path
1414

1515
from .error import SigMFFileError
1616

@@ -21,135 +21,134 @@
2121

2222

2323
class SigMFArchive:
24-
"""Archive a SigMFFile.
24+
"""
25+
Archive a SigMFFile
2526
2627
A `.sigmf` file must include both valid metadata and data.
2728
If `self.data_file` is not set or the requested output file
28-
is not writable, raise `SigMFFileError`.
29-
30-
Parameters:
31-
32-
sigmffile -- A SigMFFile object with valid metadata and data_file
33-
34-
name -- path to archive file to create. If file exists, overwrite.
35-
If `name` doesn't end in .sigmf, it will be appended.
36-
For example: if `name` == "/tmp/archive1", then the
37-
following archive will be created:
38-
/tmp/archive1.sigmf
39-
- archive1/
40-
- archive1.sigmf-meta
41-
- archive1.sigmf-data
42-
43-
fileobj -- If `fileobj` is specified, it is used as an alternative to
44-
a file object opened in binary mode for `name`. It is
45-
supposed to be at position 0. `name` is not required, but
46-
if specified will be used to determine the directory and
47-
file names within the archive. `fileobj` won't be closed.
48-
For example: if `name` == "archive1" and fileobj is given,
49-
a tar archive will be written to fileobj with the
50-
following structure:
51-
- archive1/
52-
- archive1.sigmf-meta
53-
- archive1.sigmf-data
29+
is not writable, raises `SigMFFileError`.
30+
31+
Parameters
32+
----------
33+
34+
sigmffile : SigMFFile
35+
A SigMFFile object with valid metadata and data_file.
36+
37+
name : PathLike | str | bytes
38+
Path to archive file to create. If file exists, overwrite.
39+
If `name` doesn't end in .sigmf, it will be appended.
40+
For example: if `name` == "/tmp/archive1", then the
41+
following archive will be created:
42+
/tmp/archive1.sigmf
43+
- archive1/
44+
- archive1.sigmf-meta
45+
- archive1.sigmf-data
46+
47+
fileobj : BufferedWriter
48+
If `fileobj` is specified, it is used as an alternative to
49+
a file object opened in binary mode for `name`. It is
50+
supposed to be at position 0. `name` is not required, but
51+
if specified will be used to determine the directory and
52+
file names within the archive. `fileobj` won't be closed.
53+
For example: if `name` == "archive1" and fileobj is given,
54+
a tar archive will be written to fileobj with the
55+
following structure:
56+
- archive1/
57+
- archive1.sigmf-meta
58+
- archive1.sigmf-data
5459
"""
5560

5661
def __init__(self, sigmffile, name=None, fileobj=None):
62+
is_buffer = fileobj is not None
5763
self.sigmffile = sigmffile
58-
self.name = name
59-
self.fileobj = fileobj
60-
61-
self._check_input()
64+
self.path, arcname, fileobj = self._resolve(name, fileobj)
6265

63-
archive_name = self._get_archive_name()
64-
sigmf_fileobj = self._get_output_fileobj()
65-
sigmf_archive = tarfile.TarFile(mode="w", fileobj=sigmf_fileobj, format=tarfile.PAX_FORMAT)
66-
tmpdir = tempfile.mkdtemp()
67-
sigmf_md_filename = archive_name + SIGMF_METADATA_EXT
68-
sigmf_md_path = os.path.join(tmpdir, sigmf_md_filename)
69-
sigmf_data_filename = archive_name + SIGMF_DATASET_EXT
70-
sigmf_data_path = os.path.join(tmpdir, sigmf_data_filename)
66+
self._ensure_data_file_set()
67+
self._validate()
7168

72-
with open(sigmf_md_path, "w") as mdfile:
73-
self.sigmffile.dump(mdfile, pretty=True)
69+
tar = tarfile.TarFile(mode="w", fileobj=fileobj, format=tarfile.PAX_FORMAT)
70+
tmpdir = Path(tempfile.mkdtemp())
71+
meta_path = tmpdir / (arcname + SIGMF_METADATA_EXT)
72+
data_path = tmpdir / (arcname + SIGMF_DATASET_EXT)
7473

74+
# write files
75+
with open(meta_path, "w") as handle:
76+
self.sigmffile.dump(handle)
7577
if isinstance(self.sigmffile.data_buffer, io.BytesIO):
76-
self.sigmffile.data_file = sigmf_data_path
77-
with open(sigmf_data_path, "wb") as f:
78-
f.write(self.sigmffile.data_buffer.getbuffer())
78+
# write data buffer to archive
79+
self.sigmffile.data_file = data_path
80+
with open(data_path, "wb") as handle:
81+
handle.write(self.sigmffile.data_buffer.getbuffer())
7982
else:
80-
shutil.copy(self.sigmffile.data_file, sigmf_data_path)
81-
82-
def chmod(tarinfo):
83-
if tarinfo.isdir():
84-
tarinfo.mode = 0o755 # dwrxw-rw-r
85-
else:
86-
tarinfo.mode = 0o644 # -wr-r--r--
87-
return tarinfo
88-
89-
sigmf_archive.add(tmpdir, arcname=archive_name, filter=chmod)
90-
sigmf_archive.close()
91-
if not fileobj:
92-
sigmf_fileobj.close()
93-
83+
# copy data to archive
84+
shutil.copy(self.sigmffile.data_file, data_path)
85+
tar.add(tmpdir, arcname=arcname, filter=self.chmod)
86+
# close files & remove tmpdir
87+
tar.close()
88+
if not is_buffer:
89+
# only close fileobj if we aren't working w/a buffer
90+
fileobj.close()
9491
shutil.rmtree(tmpdir)
9592

96-
self.path = sigmf_archive.name
97-
98-
def _check_input(self):
99-
self._ensure_name_has_correct_extension()
100-
self._ensure_data_file_set()
101-
self._validate_sigmffile_metadata()
102-
103-
def _ensure_name_has_correct_extension(self):
104-
name = self.name
105-
if name is None:
106-
return
107-
108-
has_extension = "." in name
109-
has_correct_extension = name.endswith(SIGMF_ARCHIVE_EXT)
110-
if has_extension and not has_correct_extension:
111-
apparent_ext = os.path.splitext(name)[-1]
112-
err = "extension {} != {}".format(apparent_ext, SIGMF_ARCHIVE_EXT)
113-
raise SigMFFileError(err)
114-
115-
self.name = name if has_correct_extension else name + SIGMF_ARCHIVE_EXT
93+
@staticmethod
94+
def chmod(tarinfo: tarfile.TarInfo):
95+
"""permission filter for writing tar files"""
96+
if tarinfo.isdir():
97+
tarinfo.mode = 0o755 # dwrxw-rw-r
98+
else:
99+
tarinfo.mode = 0o644 # -wr-r--r--
100+
return tarinfo
116101

117102
def _ensure_data_file_set(self):
118103
if not self.sigmffile.data_file and not isinstance(self.sigmffile.data_buffer, io.BytesIO):
119-
err = "no data file - use `set_data_file`"
120-
raise SigMFFileError(err)
104+
raise SigMFFileError("No data file in SigMFFile; use `set_data_file` before archiving.")
121105

122-
def _validate_sigmffile_metadata(self):
106+
def _validate(self):
123107
self.sigmffile.validate()
124108

125-
def _get_archive_name(self):
126-
if self.fileobj and not self.name:
127-
pathname = self.fileobj.name
128-
else:
129-
pathname = self.name
130-
131-
filename = os.path.split(pathname)[-1]
132-
archive_name, archive_ext = os.path.splitext(filename)
133-
return archive_name
134-
135-
def _get_output_fileobj(self):
136-
try:
137-
fileobj = self._get_open_fileobj()
138-
except:
139-
if self.fileobj:
140-
err = "fileobj {!r} is not byte-writable".format(self.fileobj)
141-
else:
142-
err = "can't open {!r} for writing".format(self.name)
143-
144-
raise SigMFFileError(err)
145-
146-
return fileobj
147-
148-
def _get_open_fileobj(self):
149-
if self.fileobj:
150-
fileobj = self.fileobj
151-
fileobj.write(bytes()) # force exception if not byte-writable
109+
def _resolve(self, name, fileobj):
110+
"""
111+
Resolve both (name, fileobj) into (path, arcname, fileobj) given either or both.
112+
113+
Returns
114+
-------
115+
path : PathLike
116+
Path of the archive file.
117+
arcname : str
118+
Name of the sigmf object within the archive.
119+
fileobj : BufferedWriter
120+
Open file handle object.
121+
"""
122+
if fileobj:
123+
try:
124+
# exception if not byte-writable
125+
fileobj.write(bytes())
126+
# exception if no name property of handle
127+
path = Path(fileobj.name)
128+
if not name:
129+
arcname = path.stem
130+
else:
131+
arcname = name
132+
except io.UnsupportedOperation:
133+
raise SigMFFileError(f"fileobj {fileobj} is not byte-writable.")
134+
except AttributeError:
135+
raise SigMFFileError(f"fileobj {fileobj} is invalid.")
136+
elif name:
137+
path = Path(name)
138+
# ensure name has correct suffix if it exists
139+
if path.suffix == "":
140+
# add extension if none was given
141+
path = path.with_suffix(SIGMF_ARCHIVE_EXT)
142+
elif path.suffix != SIGMF_ARCHIVE_EXT:
143+
# ensure suffix is correct
144+
raise SigMFFileError(f"Invalid extension ({path.suffix} != {SIGMF_ARCHIVE_EXT}).")
145+
arcname = path.stem
146+
147+
try:
148+
fileobj = open(path, "wb")
149+
except (OSError, IOError):
150+
raise SigMFFileError(f"Can't open {name} for writing.")
152151
else:
153-
fileobj = open(self.name, "wb")
152+
raise SigMFFileError("Either `name` or `fileobj` needs to be defined.")
154153

155-
return fileobj
154+
return path, arcname, fileobj

0 commit comments

Comments
 (0)