Skip to content

Commit

Permalink
address feedback, created common base class for sigmffile and new sig…
Browse files Browse the repository at this point in the history
…mffilecollection class
  • Loading branch information
jhazentia committed Jan 17, 2024
1 parent 416e721 commit 66bbd40
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 76 deletions.
22 changes: 13 additions & 9 deletions README.md
Expand Up @@ -56,15 +56,16 @@ from sigmf.archivereader import SigMFArchiveReader
from sigmf.sigmffile import (fromarchive,
fromfile)

# read multirecording archive using fromarchive
sigmffiles = fromarchive("multi_recording_archive.sigmf")
# read multirecording archive using fromarchive_multirecording
sigmffile_collection = fromarchive("multi_recording_archive.sigmf")
# length should be equal to the number of recordings in the archive
print(len(sigmffiles))
print(sigmffile_collection.sigmffile_count())

# read multirecording archive using fromfile
sigmffiles = fromfile("multi_recording_archive.sigmf")
sigmffile_collection = fromfile("multi_recording_archive.sigmf")
# length should be equal to the number of recordings in the archive
print(len(sigmffiles))
print(sigmffile_collection.sigmffile_count())
sigmffile_collection.get_sigmffiles() # get sigmf files

# read multirecording archive using SigMFArchiveReader
reader = SigMFArchiveReader("multi_recording_archive.sigmf")
Expand Down Expand Up @@ -229,11 +230,11 @@ sigmf_file.add_capture(start_index=0)
sigmf_file.set_data_file(data_path)

# create archive using SigMFArchive
archive = SigMFArchive(sigmffiles=sigmf_file,
archive = SigMFArchive(sigmffile_collection=sigmf_file,
path="single_recording_archive1.sigmf")

# create archive using SigMFFile archive()
archive_path = sigmf_file.archive(file_path="single_recording_archive2.sigmf")
archive_path = sigmf_file.archive(name="single_recording_archive2.sigmf")

# create archive using tofile
sigmf_file.tofile(file_path="single_recording_archive3.sigmf",
Expand All @@ -248,6 +249,7 @@ import numpy as np

from sigmf.sigmffile import (SigMFFile,
SigMFArchive)
from sigmf.sigmffile_collection import SigMFFileCollection


# create data files
Expand All @@ -272,10 +274,12 @@ sigmf_file_2.add_annotation(start_index=0, length=len(random_data2))
sigmf_file_2.add_capture(start_index=0)
sigmf_file_2.set_data_file(data2_path)

# create archive using SigMFFileCollection
sigmffile_collection = SigMFFileCollection([sigmf_file_1, sigmf_file_2])
sigmffile_collection.archive(name="multi_recording_archive.sigmf")

# create archive using SigMFArchive
sigmffiles = [sigmf_file_1, sigmf_file_2]
archive = SigMFArchive(sigmffiles=sigmffiles,
archive = SigMFArchive(sigmffile_collection=sigmffile_collection,
path="multi_recording_archive.sigmf")
```

Expand Down
17 changes: 3 additions & 14 deletions sigmf/archive.py
Expand Up @@ -6,13 +6,11 @@

"""Create and extract SigMF archives."""

import collections
from io import BytesIO
import os
import tarfile
import tempfile
import time
from typing import BinaryIO, Iterable, Union
from typing import BinaryIO, Union

import sigmf

Expand Down Expand Up @@ -55,8 +53,7 @@ class SigMFArchive():
files in the archive. Defaults to True.
"""
def __init__(self,
sigmffiles: Union["sigmf.sigmffile.SigMFFile",
Iterable["sigmf.sigmffile.SigMFFile"]],
sigmffile_collection: 'sigmf.sigmffile_collection.AbstractSigMFFileCollection',
path: Union[str, os.PathLike] = None,
fileobj: BinaryIO = None,
pretty: bool = True):
Expand All @@ -65,15 +62,7 @@ def __init__(self,
raise SigMFFileError("'path' or 'fileobj' required for creating "
"SigMF archive!")

if isinstance(sigmffiles, sigmf.sigmffile.SigMFFile):
self.sigmffiles = [sigmffiles]
elif (hasattr(collections, "Iterable") and
isinstance(sigmffiles, collections.Iterable)):
self.sigmffiles = sigmffiles
elif isinstance(sigmffiles, collections.abc.Iterable): # python 3.10
self.sigmffiles = sigmffiles
else:
raise SigMFFileError("Unknown type for sigmffiles argument!")
self.sigmffiles = sigmffile_collection.get_sigmffiles()

if path:
self.path = str(path)
Expand Down
49 changes: 31 additions & 18 deletions sigmf/sigmffile.py
Expand Up @@ -13,8 +13,9 @@
import warnings
import numpy as np

from . import __version__, schema, sigmf_hash, validate
from . import schema, sigmf_hash, validate
from .archive import SigMFArchive, SIGMF_DATASET_EXT, SIGMF_METADATA_EXT, SIGMF_ARCHIVE_EXT, SIGMF_COLLECTION_EXT
from .sigmffile_collection import AbstractSigMFFileCollection, SigMFFileCollection
from .utils import dict_merge
from .error import SigMFFileError, SigMFAccessError

Expand Down Expand Up @@ -98,7 +99,8 @@ def dumps(self, pretty=True):
separators=(',', ': ') if pretty else None,
) + "\n"

class SigMFFile(SigMFMetafile):

class SigMFFile(SigMFMetafile, AbstractSigMFFileCollection):
START_INDEX_KEY = "core:sample_start"
LENGTH_INDEX_KEY = "core:sample_count"
GLOBAL_INDEX_KEY = "core:global_index"
Expand Down Expand Up @@ -551,31 +553,31 @@ def validate(self):
version = self.get_global_field(self.VERSION_KEY)
validate.validate(self._metadata, self.get_schema())

def archive(self, file_path=None, fileobj=None, pretty=True):
def archive(self, name=None, fileobj=None, pretty=True):
"""Dump contents to SigMF archive format.
Keyword arguments:
file_path -- passed to SigMFArchive`path`. Path to archive file to
create. If file exists, overwrite. If `path` doesn't end
in .sigmf, it will be appended. If not given, `file_path`
name -- passed to SigMFArchive `path`. Path to archive file to
create. If file exists, overwrite. If `name` doesn't end
in .sigmf, it will be appended. If not given, `name`
will be set to self.name. (default None)
fileobj -- passed to SigMFArchive `fileobj`. If `fileobj` is
specified, it is used as an alternative to a file object
opened in binary mode for `file_path`. If `fileobj` is an
opened in binary mode for `name`. If `fileobj` is an
open tarfile, it will be appended to. It is supposed to
be at position 0. `fileobj` won't be closed. If `fileobj`
is given, `file_path` has no effect. (default None)
is given, `name` has no effect. (default None)
pretty -- passed to SigMFArchive `pretty`. If True, pretty print
JSON when creating the metadata and collection files in
the archive. (default True).
Returns the path to the created archive.
"""
if file_path is None:
file_path = self.name
if name is None:
name = self.name

archive = SigMFArchive(self,
path=file_path,
path=name,
fileobj=fileobj,
pretty=pretty)
return archive.path
Expand Down Expand Up @@ -701,6 +703,13 @@ def _read_datafile(self, first_byte, nitems, autoscale, raw_components):
fp.close()
return data

def sigmffile_count(self):
# This class always represents a single SigMF metadata/data file combination
return 1

def get_sigmffiles(self):
return [self]


class SigMFCollection(SigMFMetafile):
VERSION_KEY = "core:version"
Expand Down Expand Up @@ -949,12 +958,16 @@ def get_dataset_filename_from_metadata(meta_fn, metadata=None):
return None


def fromarchive(archive_path):
"""Extract an archive and return containing SigMFFiles.
def fromarchive(archive_path, dir=None) -> AbstractSigMFFileCollection:
"""Extract an archive and return AbstractSigMFFileCollection (either
a SigMFFile or a SigMFFileCollection).
If the archive contains a single recording, a single SigMFFile object will
be returned. If the archive contains multiple recordings a list of
SigMFFile objects will be returned.
be returned. If the archive contains multiple recordings a
SigMFFileCollection will be returned.
The `dir` parameter is no longer used as this function has been changed to
access SigMF archives without extracting them.
"""
from .archivereader import SigMFArchiveReader
reader = SigMFArchiveReader(archive_path)
Expand All @@ -963,15 +976,15 @@ def fromarchive(archive_path):
if len(sigmffiles) == 1:
ret = sigmffiles[0]
else:
ret = sigmffiles
ret = SigMFFileCollection(sigmffiles)

return ret


def fromfile(filename, skip_checksum=False):
'''
Creates and returns a SigMFFile or SigMFCollection instance with metadata
loaded from the specified file. The filename may be that of either a
Creates and returns AbstractSigMFFileCollection (either a SigMFFile or a SigMFFileCollection) or SigMFCollection
instance with metadata loaded from the specified file. The filename may be that of either a
sigmf-meta file, a sigmf-data file, a sigmf-collection file, or a sigmf
archive.
Expand Down
43 changes: 43 additions & 0 deletions sigmf/sigmffile_collection.py
@@ -0,0 +1,43 @@
from abc import ABC, abstractmethod
from typing import Iterable

import sigmf

from .archive import SigMFArchive


class AbstractSigMFFileCollection(ABC):

@abstractmethod
def sigmffile_count(self):
pass

@abstractmethod
def get_sigmffiles(self):
pass

# should tofile() be added?

@abstractmethod
def archive(self, name=None, fileobj=None, pretty=True):
pass


class SigMFFileCollection(AbstractSigMFFileCollection):

def __init__(self, sigmffiles: Iterable["sigmf.sigmffile.SigMFFile"]) -> None:
self.sigmffiles = sigmffiles

def sigmffile_count(self):
return len(self.sigmffiles)

def get_sigmffiles(self):
return self.sigmffiles

# should tofile() be added?
def archive(self, name=None, fileobj=None, pretty=True):
archive = SigMFArchive(self,
path=name,
fileobj=fileobj,
pretty=pretty)
return archive.path
29 changes: 15 additions & 14 deletions tests/test_archive.py
Expand Up @@ -15,6 +15,7 @@
SIGMF_METADATA_EXT,
SigMFArchive)
from sigmf.archivereader import SigMFArchiveReader
from sigmf.sigmffile_collection import SigMFFileCollection

from .testdata import TEST_FLOAT32_DATA_1, TEST_METADATA_1

Expand All @@ -29,20 +30,20 @@ def test_without_data_file_throws_fileerror(test_sigmffile):
test_sigmffile.data_file = None
with tempfile.NamedTemporaryFile() as temp:
with pytest.raises(error.SigMFFileError):
test_sigmffile.archive(file_path=temp.name)
test_sigmffile.archive(name=temp.name)


def test_invalid_md_throws_validationerror(test_sigmffile):
del test_sigmffile._metadata["global"]["core:datatype"] # required field
with tempfile.NamedTemporaryFile() as temp:
with pytest.raises(jsonschema.exceptions.ValidationError):
test_sigmffile.archive(file_path=temp.name)
test_sigmffile.archive(name=temp.name)


def test_name_wrong_extension_throws_fileerror(test_sigmffile):
with tempfile.NamedTemporaryFile() as temp:
with pytest.raises(error.SigMFFileError):
test_sigmffile.archive(file_path=temp.name + ".zip")
test_sigmffile.archive(name=temp.name + ".zip")


def test_fileobj_extension_ignored(test_sigmffile):
Expand All @@ -52,7 +53,7 @@ def test_fileobj_extension_ignored(test_sigmffile):

def test_name_used_in_fileobj(test_sigmffile):
with tempfile.NamedTemporaryFile() as temp:
sigmf_archive = test_sigmffile.archive(file_path="testarchive",
sigmf_archive = test_sigmffile.archive(name="testarchive",
fileobj=temp)
sigmf_tarfile = tarfile.open(sigmf_archive, mode="r")
basedir, file1, file2 = sigmf_tarfile.getmembers()
Expand Down Expand Up @@ -84,7 +85,7 @@ def test_unwritable_name_throws_fileerror(test_sigmffile):
# so use invalid filename
unwritable_file = '/bad_name/'
with pytest.raises(error.SigMFFileError):
test_sigmffile.archive(file_path=unwritable_file)
test_sigmffile.archive(name=unwritable_file)


def test_tarfile_layout(test_sigmffile):
Expand Down Expand Up @@ -193,16 +194,16 @@ def test_tarfile_type(test_sigmffile):

def test_create_archive_pathlike(test_sigmffile, test_alternate_sigmffile):
with tempfile.NamedTemporaryFile() as t:
input_sigmffiles = [test_sigmffile, test_alternate_sigmffile]
input_sigmffiles = SigMFFileCollection([test_sigmffile, test_alternate_sigmffile])
arch = SigMFArchive(input_sigmffiles, path=Path(t.name))
output_sigmf_files = sigmffile.fromarchive(archive_path=arch.path)
assert len(output_sigmf_files) == 2
assert input_sigmffiles == output_sigmf_files
output_sigmf_collection = sigmffile.fromarchive(archive_path=arch.path)
assert output_sigmf_collection.sigmffile_count() == 2
assert input_sigmffiles.get_sigmffiles() == output_sigmf_collection.get_sigmffiles()


def test_archive_names(test_sigmffile):
with tempfile.NamedTemporaryFile(suffix=".sigmf") as t:
a = SigMFArchive(sigmffiles=test_sigmffile, path=t.name)
a = SigMFArchive(sigmffile_collection=test_sigmffile, path=t.name)
assert a.path == t.name
observed_sigmffile = sigmffile.fromarchive(t.name)
observed_sigmffile.name == test_sigmffile.name
Expand Down Expand Up @@ -261,17 +262,17 @@ def test_create_archive_from_archive_reader(test_sigmffile,
""" This test is to ensure that SigMFArchive will correctly create archive
using SigMFFile offset_and_size which is set when using SigMFArchiveReader
"""
original_sigmffiles = [test_sigmffile, test_alternate_sigmffile]
original_sigmffiles = SigMFFileCollection([test_sigmffile, test_alternate_sigmffile])
with tempfile.TemporaryDirectory() as temp_dir:
archive_path1 = os.path.join(temp_dir, "original_archive.sigmf")
SigMFArchive(sigmffiles=original_sigmffiles, path=archive_path1)
SigMFArchive(sigmffile_collection=original_sigmffiles, path=archive_path1)
reader = SigMFArchiveReader(path=archive_path1)
archive_path2 = os.path.join(temp_dir, "archive_from_reader.sigmf")
SigMFArchive(sigmffiles=reader.sigmffiles, path=archive_path2)
SigMFArchive(sigmffile_collection=SigMFFileCollection(reader.sigmffiles), path=archive_path2)
read_archive_from_reader = SigMFArchiveReader(path=archive_path2)
# SigMFFile.__eq__() method will check metadata
# which includes datafile hash
assert original_sigmffiles == read_archive_from_reader.sigmffiles
assert original_sigmffiles.get_sigmffiles() == read_archive_from_reader.sigmffiles
observed_sigmffile0 = read_archive_from_reader.sigmffiles[0]
observed_sigmffile1 = read_archive_from_reader.sigmffiles[1]
assert test_sigmffile.name == observed_sigmffile0.name
Expand Down

0 comments on commit 66bbd40

Please sign in to comment.