Skip to content

Commit

Permalink
visr_bear: add API for accessing metadata in data files
Browse files Browse the repository at this point in the history
  • Loading branch information
tomjnixon committed Feb 16, 2024
1 parent c34331f commit d53cc0a
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 0 deletions.
31 changes: 31 additions & 0 deletions visr_bear/include/bear/api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,35 @@ class Renderer {
std::unique_ptr<RendererImpl> impl;
};

class DataFileMetadataImpl;

/// metadata contained in a BEAR data file
class DataFileMetadata {
public:
/// get the metadata for a file with a given path
static DataFileMetadata read_from_file(const std::string &path);

/// does the file include metadata, or was it a file from before when
/// metadata was added
bool has_metadata() const;

/// short label that describes this file
const std::string &get_label() const;

/// longer description of this file
const std::string &get_description() const;

/// is this a data file released by the BEAR project, or a preliminary or
/// custom one?
bool is_released() const;

DataFileMetadata() = delete;
DataFileMetadata(DataFileMetadata &&);
~DataFileMetadata();

private:
DataFileMetadata(std::unique_ptr<DataFileMetadataImpl> impl);
std::unique_ptr<DataFileMetadataImpl> impl;
};

}; // namespace bear
7 changes: 7 additions & 0 deletions visr_bear/pythonwrappers/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ namespace python {
.def(py::init<int64_t, int64_t>())
.def_property_readonly("numerator", &Time::numerator)
.def_property_readonly("denominator", &Time::denominator);

py::class_<DataFileMetadata>(m, "DataFileMetadata")
.def_static("read_from_file", &DataFileMetadata::read_from_file)
.def_property_readonly("has_metadata", &DataFileMetadata::has_metadata)
.def_property_readonly("label", &DataFileMetadata::get_label)
.def_property_readonly("description", &DataFileMetadata::get_description)
.def_property_readonly("is_released", &DataFileMetadata::is_released);
}

} // namespace python
Expand Down
79 changes: 79 additions & 0 deletions visr_bear/src/api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <libvisr/time.hpp>

#include "config_impl.hpp"
#include "data_file.hpp"
#include "listener_impl.hpp"
#include "parameters.hpp"
#include "top.hpp"
Expand Down Expand Up @@ -278,4 +279,82 @@ void Renderer::set_listener(const Listener &l, const boost::optional<Time> &inte
}

Renderer::~Renderer() = default;

struct DataFileMetadataImpl {
rapidjson::Value metadata;

bool has_metadata = false;
std::string label;
std::string description;
bool released = false;
};

namespace {
std::string parse_label(rapidjson::Value &metadata)
{
auto it = metadata.FindMember("label");
if (it == metadata.MemberEnd()) throw std::runtime_error("expected label");

if (!it->value.IsString()) throw std::runtime_error("label should be a string");

return it->value.GetString();
}

std::string parse_description(rapidjson::Value &metadata)
{
auto it = metadata.FindMember("description");
if (it == metadata.MemberEnd()) return "";

if (!it->value.IsString()) throw std::runtime_error("description should be a string");

return it->value.GetString();
}

bool parse_released(rapidjson::Value &metadata)
{
auto it = metadata.FindMember("released");
if (it == metadata.MemberEnd()) throw std::runtime_error("expected released");

if (!it->value.IsBool()) throw std::runtime_error("released should be a bool");

return it->value.GetBool();
}
} // namespace

DataFileMetadata::DataFileMetadata(std::unique_ptr<DataFileMetadataImpl> impl) : impl(std::move(impl)) {}
DataFileMetadata::DataFileMetadata(DataFileMetadata &&other) = default;
DataFileMetadata::~DataFileMetadata() = default;

DataFileMetadata DataFileMetadata::read_from_file(const std::string &path)
{
auto tf = tensorfile::read(path);

check_data_file_version(tf);

auto impl = std::make_unique<DataFileMetadataImpl>();

auto it = tf.metadata.FindMember("metadata");
if (it != tf.metadata.MemberEnd()) {
if (!it->value.IsObject()) throw std::runtime_error("data file metadata should be object");

impl->has_metadata = true;

impl->metadata = it->value;

impl->label = parse_label(impl->metadata);
impl->description = parse_description(impl->metadata);
impl->released = parse_released(impl->metadata);
}

return {std::move(impl)};
}

bool DataFileMetadata::has_metadata() const { return impl->has_metadata; }

const std::string &DataFileMetadata::get_label() const { return impl->label; }

const std::string &DataFileMetadata::get_description() const { return impl->description; }

bool DataFileMetadata::is_released() const { return impl->released; }

} // namespace bear
94 changes: 94 additions & 0 deletions visr_bear/test/test_data_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from contextlib import contextmanager
import pytest
import visr_bear.api
from utils import data_path
import bear.tensorfile


@contextmanager
def mod_data(in_file, out_file):
with open(in_file, "rb") as f:
data = bear.tensorfile.read(f)

yield data

with open(out_file, "wb") as f:
bear.tensorfile.write(f, data)


def test_no_metadata(tmp_path):
data_path_mod = str(tmp_path / "default_no_meta.tf")

with mod_data(data_path, data_path_mod) as data:
if "metadata" in data:
del data["metadata"]

meta = visr_bear.api.DataFileMetadata.read_from_file(data_path_mod)

assert not meta.has_metadata
assert meta.label == ""
assert not meta.is_released


def test_with_metadata(tmp_path):
data_path_mod = str(tmp_path / "default_meta.tf")

with mod_data(data_path, data_path_mod) as data:
data["metadata"] = dict(
label="label with ünicode",
released=True,
)

meta = visr_bear.api.DataFileMetadata.read_from_file(data_path_mod)

assert meta.has_metadata
assert meta.label == "label with ünicode"
assert meta.description == ""
assert meta.is_released


def test_with_description(tmp_path):
data_path_mod = str(tmp_path / "default_description.tf")

with mod_data(data_path, data_path_mod) as data:
data["metadata"] = dict(
label="the label",
description="the description",
released=False,
)

meta = visr_bear.api.DataFileMetadata.read_from_file(data_path_mod)

assert meta.has_metadata
assert meta.label == "the label"
assert meta.description == "the description"
assert not meta.is_released


@pytest.mark.parametrize("version", [None, 0])
def test_version_ok(tmp_path, version):
data_path_mod = str(tmp_path / f"default_version_{version}.tf")

with mod_data(data_path, data_path_mod) as data:
version_attr = "bear_data_version"
if version is None:
if version_attr in data:
del data[version_attr]
else:
data[version_attr] = version

# checks version
visr_bear.api.DataFileMetadata.read_from_file(data_path_mod)


def test_version_error(tmp_path):
data_path_mod = str(tmp_path / f"default_version_error.tf")

with mod_data(data_path, data_path_mod) as data:
version_attr = "bear_data_version"
data[version_attr] = 1

with pytest.raises(
RuntimeError, match="data file version mismatch: expected 0, got 1"
):
visr_bear.api.DataFileMetadata.read_from_file(data_path_mod)

0 comments on commit d53cc0a

Please sign in to comment.