SPSFTMLRZE2R4EBAAQKWBUZDRYZ36S2VFTBIJRE6XS7JMKELV4AQC
UKQAGL5F5LWZZR7RWFJI3H2MW6LXPRGPXAIGNBZI5IVJLWUFHLGAC
GQVS55HIQLU7KPJNRMF57QUM4EATSWFQRCS7ZEJMJPUXFX2NHSYAC
XI5ALEH6NPTQWB6O62QV62EP4H3K7WSNTHCOGT3LZIIU6I2YDGIQC
TTR5IFSG25VNBQ2F2FNOLUMTEIHVBHFOEXYB2ZWEHWOURUV4GJMQC
SAHJYVNBUBBIUBI4ZMAXK4QJFOT54M5UA3W2HQMTNDSP3GGCRX7QC
2SABVMY3A2RZDF3KJXZSMJ2UQ4Q5EW422G4DVBJRKK26S2ESGVQAC
U4VCAFXQNTKC7KWWE3B2JJMZMFRGLBOHSZIOXCE6EEXW7WE2Q5NAC
GUXZCEWWPBCHXO26JVWZ74CTSDFDDO775YR7FKY7UGVZCA7GCSYAC
A6ZAYJNBYFXPUTCHTQDGAWUZIYOXNO3IJBN73ZX6PEJ3T4NMNOGQC
77SIQZ3EGGV6KSECMLPDKQFGEC7CCFAPWGER7ZARQ5STDKJNU6GQC
UUD3CJZLSTAVLMIXIWE4CHN5HQSFM6K3DCPWECJMKGXDOROK4G4AC
4MG5JFXTKAE3SOVKGGNKEUTNCKOWEBHTGKVZHJWLWE3PTZTQKHPAC
5POF332LJEBGWEJUGI34P33Q4BQP7CAQNV5ODQT3PWG7FI7VNLOQC
AT753JPOOWZCIYNKAG27LZSZZ72ZILWVENG42Y6U2S34JD3B6ZZQC
CUADTSHQNPGMWHIJCWXKHNNIY4UJOYZ7XBZA5CJ2VVJJAMSNT4BAC
JTX5OHWHO647X4XLQPUDLH34QG2VDP7N7X6OL7XEIZQYVWELV6CAC
let series_spec = data_spec.get_series_spec(&self.series_id);
series_spec.time_series_data(root_path)
let series_spec = match data_spec.get_series_spec(&self.series_id) {
Some(series_spec) => series_spec,
None => {
return Err(
series_id_not_in_dataspec(
file!(),
line!(),
&self.series_id.to_string()
)
)
},
};
data_from_file(
series_spec.country,
series_spec.data_type,
series_spec.series_id,
root_path
)
use crate::error::{
Error,
};
use crate::error::*;
// --- Client-facing data-structures --------------------------------------------------------------
/// `(DataType, Country)` key to lookup data or spec.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct PageKey {
///
pub country: Country,
///
pub data_type: DataType,
///
pub index: usize,
}
impl PageKey {
/// Return a new `PageKey`.
pub fn new(country: Country, data_type: DataType, index: usize) -> Self {
PageKey { country, data_type, index }
}
}
impl<'a> TryInto<PageKey> for KeyTreeRef<'a> {
type Error = keytree::Error;
// -- "ts_spec.keytree" ---------------------------------------------------------------------------
fn try_into(self) -> Result<PageKey, Self::Error> {
Ok(PageKey {
index: self.value("key::index")?,
data_type: self.value("key::data_type")?,
country: self.value("key::country")?,
})
}
}
/// Top-level time-series specification data-structure.
///
/// It can be serialized into something like,
/// ```text
/// ts_spec:
/// page:
/// data_type: u
/// country: Australia
/// graphic:
/// series:
/// id: AUSURANAA
/// id: AUSURAQS
/// id: AUSURHARMADSMEI
/// id: AUSURHARMMDSMEI
/// ```
/// Represents a series of graphics to be plotted on one page. A `PageKey` is a combination of
/// `Country` and `DataType`. It is served to the client as one chunk of JSON.
// GraphicJson. Json holds the JSON serialization as a String. PageJson is the largest component
// that is serializable into JSON.
pub (crate) fn new() -> TSSpec {
TSSpec(Vec::new())
/// Build time-series data from a time-series specification.
pub fn from_spec(spec: &Spec, root_path: &str) -> Result<Self, Error> {
spec
.into_data(root_path)
/// Read from file.
pub fn from_file(path: &str) -> Self {
let ts_spec = fs::read_to_string(path).unwrap();
let kt = KeyTree::parse(&ts_spec).unwrap();
kt.to_ref().try_into().unwrap()
pub (crate) fn insert(&mut self, key: &PageKey, value: String) {
match self.0.get_mut(key) {
Some(_) => {
println!("Tried to insert page_key: {:?} twice.", key);
panic!();
},
None => {
self.0.insert(*key, value);
},
}
/// ts_spec:
/// page:
/// data_type: u
/// country: Australia
/// graphic:
/// series:
/// id: AUSURANAA
/// id: AUSURAQS
/// id: AUSURHARMADSMEI
/// id: AUSURHARMMDSMEI
pub fn into_json(
&self,
data_spec: &DataSpec,
root_path: &str) -> Result<TSJson, Error>
{
let mut ts_json = TSJson::new();
/// Converts specification data into `TSData`.
pub struct Json(Vec<PageJson>);
for graphic in &page_spec.graphics {
let json = graphic.into_json(data_spec, root_path)?;
value.push(json);
let key = PageKey::new(
page_json.country,
page_json.data_type,
page_json.index
);
let value = match serde_json::to_string(&page_json) {
Ok(s) => s,
Err(err) => {
eprintln!("{}", err.to_string());
panic!();
},
impl IntoKeyTree for TSSpec {
fn keytree(&self) -> KeyTreeString {
let mut kt = KeyTreeString::new();
kt.push_key(0, "ts_spec");
for page in &self.0 {
kt.push_keytree(1, page.keytree());
#[derive(Debug, Serialize)]
/// Serializable into JSON data for a time-series HTML page.
pub struct PageJson {
country: Country,
data_type: DataType,
index: usize,
graphics: Vec<GraphicJson>,
}
impl PageJson {
pub (crate) fn new(country: Country, data_type: DataType, index: usize) -> Self {
PageJson {
country, data_type, index, graphics: Vec::new()
impl<'a> TryInto<TSSpec> for KeyTreeRef<'a> {
type Error = keytree::Error;
fn try_into(self) -> Result<TSSpec, Self::Error> {
let page_vec: Vec<TSPageSpec> = self.vec_at("ts_spec::page")?;
Ok(TSSpec(page_vec))
pub (crate) fn push(&mut self, graphic_json: GraphicJson) {
self.graphics.push(graphic_json);
/// Component of `TSSpec`.
///
/// Mirrored with a keytree specification which is the `country:` key in
/// ```
/// page:
/// country: Australia
/// data_type: u
/// index: 0
/// graphic:
/// series:
/// series_id: AUSURANAA
/// data_type: u
/// series:
/// data_type: u
/// series_id: AUSURAQS
/// series:
/// data_type: u
/// series_id: AUSURHARMADSMEI
/// series:
/// data_type: u
/// series_id: AUSURHARMMDSMEI
/// ```
#[derive(Debug)]
pub struct TSPageSpec {
/// GraphicJson is the largest component that is serializable into JSON.
#[derive(Debug, Serialize)]
pub struct GraphicJson {
impl TSPageSpec {
pub (crate) fn new(key: PageKey, graphics: Vec<TSGraphicSpec>) -> Self {
TSPageSpec {
country: key.country,
data_type: key.data_type,
index: key.index,
graphics: graphics,
impl GraphicJson {
pub (crate) fn new() -> Self {
GraphicJson {
height: None,
series: Vec::new(),
pub (crate) fn key(&self) -> PageKey {
PageKey {
country: self.country,
data_type: self.data_type,
index: self.index,
// === Time-series Specification ==================================================================
/// Time-series specification.
/// ```
/// ts_spec:
/// page:
/// country: Australia
/// data_type: u
/// index: 0
/// graphic:
/// series:
/// data_type: u
/// series_id: AUSURAMS
/// series:
/// data_type: u
/// series_id: AUSURANAA
/// ```
pub struct Spec(Vec<PageSpec>);
impl Spec {
// The Spec components do not map well to Json components.
// 1. Json is not serializable - it is a HashMap which maps PageKeys to Strings to serve.
// 2. PageJson does not exists. PageSpec.into_json creates a Vec<GraphicJson>.
pub (crate) fn into_data(&self, root_path: &str) -> Result<TSData, Error> {
let mut json = Json::new();
for page_spec in &self.0 {
let mut page_json = PageJson::new(
page_spec.country,
page_spec.data_type,
page_spec.index,
);
for graphic_spec in &page_spec.graphics {
let mut graphic_json = GraphicJson::new();
for series_spec in &graphic_spec.seriess {
let rts = data_from_file(
page_spec.country,
series_spec.data_type,
series_spec.series_id.clone(),
root_path,
)?;
let meta = meta_from_file(
page_spec.country,
series_spec.data_type,
series_spec.series_id.clone(),
root_path,
);
graphic_json.push(rts, meta);
}
page_json.push(graphic_json);
}
json.push(page_json);
impl IntoKeyTree for TSPageSpec {
fn keytree(&self) -> KeyTreeString {
/// Read in ts specification from file.
/// ```
/// let ts_spec = ts::Spec::from_file("ts_spec.keytree");
/// ```
pub fn from_file(path: &str) -> Result<Self, Error> {
let source_spec = match fs::read_to_string(path) {
Ok(ss) => ss,
Err(err) => { return Err(
failed_to_read_file(
file!(),
line!(),
&err.to_string()
))
},
};
let kt = KeyTree::parse(&source_spec).unwrap();
kt.to_ref().try_into().map_err(|err: keytree::error::Error| {
keytree_error(file!(), line!(), &err.to_string())
})
}
kt.push_key(0, "page");
kt.push_value(1, "country", &self.country.to_string());
kt.push_value(1, "data_type", &self.data_type.to_string());
kt.push_value(1, "index", &self.index.to_string());
for graphic in &self.graphics {
kt.push_keytree(1, graphic.keytree());
}
kt
pub (crate) fn push(&mut self, page_spec: PageSpec) {
self.0.push(page_spec)
fn try_into(self) -> Result<TSPageSpec, Self::Error> {
Ok(TSPageSpec {
country: self.value("page::country")?,
data_type: self.value("page::data_type")?,
index: self.value("page::index")?,
graphics: self.vec_at("page::graphic")?,
})
fn try_into(self) -> Result<Spec, keytree::Error> {
Ok(Spec(self.vec_at("ts_spec::page")?))
/// graphic:
/// series:
/// series_id: AUSURANAA
/// data_type: u
/// series:
/// data_type: u
/// series_id: AUSURAQS
/// series:
/// data_type: u
/// series_id: AUSURHARMADSMEI
/// series:
/// data_type: u
/// series_id: AUSURHARMMDSMEI
/// page:
/// country: Australia
/// data_type: u
/// index: 0
/// graphic:
/// series:
/// data_type: u
/// series_id: AUSURAMS
/// series:
/// data_type: u
/// series_id: AUSURANAA
#[derive(Debug)]
pub struct TSGraphicSpec {
/// Height in pixels of the graphic.
pub height: Option<f32>,
/// e.g. [ AUSURHARMADSMEI, ... ]
pub series: Vec<TSSeriesSpec>,
pub struct PageSpec {
country: Country,
data_type: DataType,
index: usize,
graphics: Vec<GraphicSpec>,
let series_spec = data_spec.get_series_spec(series_id);
let ts = series_spec.time_series_data(root_path)?;
let meta = series_spec.meta(root_path);
series_json.push(ts, meta);
}
fn try_into(self) -> Result<PageSpec, keytree::Error> {
impl IntoKeyTree for TSGraphicSpec {
fn keytree(&self) -> KeyTreeString {
let mut kt = KeyTreeString::new();
/// Component of time-series specification.
/// ```
/// graphic:
/// series:
/// data_type: u
/// series_id: AUSURAMS
/// series:
/// data_type: u
/// series_id: AUSURANAA
/// ```
pub struct GraphicSpec {
height: Option<f32>,
seriess: Vec<self::SeriesSpec>,
}
kt.push_key(0, "graphic");
if let Some(height) = self.height {
kt.push_value(1, "height", &height.to_string());
};
for series in &self.series {
kt.push_keytree(1, series.keytree());
impl GraphicSpec {
pub (crate) fn new() -> Self {
GraphicSpec {
height: None,
seriess: Vec::new(),
TSGraphicSpec {
height: self.opt_value("graphic::height")?,
series: self.vec_at("graphic::series")?,
GraphicSpec{
height: self.opt_value("graphic::height")?,
seriess: self.vec_at("graphic::series")?,
}
}
impl IntoKeyTree for TSSeriesSpec {
fn keytree(&self) -> KeyTreeString {
let mut kt = KeyTreeString::new();
kt.push_key(0, "series");
kt.push_value(1, "data_type", &self.data_type.to_string());
kt.push_value(1, "series_id", &self.series_id.to_string());
kt
/// Represents a series of graphics to be plotted on one page. A `PageKey` is a combination of
/// `Country` and `DataType`. It is served to the client as one chunk of JSON.
#[derive(Debug)]
pub struct TSJson(pub HashMap<PageKey, String>);
impl TSJson {
pub (crate) fn new() -> TSJson {
TSJson(HashMap::new())
}
pub (crate) fn insert(&mut self, key: &PageKey, value: Vec<GraphicJson>) {
match self.0.get_mut(key) {
Some(_) => {
println!("Tried to insert page_key: {:?} twice.", key);
panic!();
},
None => {
self.0.insert(
*key,
serde_json::to_string(&value).unwrap(),
);
},
}
}
}
/// Respresents a graphic to be served to the client as JSON.
#[derive(Debug, Serialize)]
pub struct GraphicJson {
///
pub height: Option<f32>,
///
pub series: SeriesJson,
}
/// `SeriesJson` is a time-series to be served to the client as JSON.
#[derive(Debug, Serialize)]
pub struct SeriesJson(Vec<(RegularTimeSeries<1>, SeriesMetaData)>);
impl SeriesJson {
/// Create a new, empty `SeriesJson`.
pub fn new() -> Self {
SeriesJson(Vec::new())
}
/// Push time-series data and associated meta-data onto `SeriesJson`.
pub fn push(&mut self, ts: RegularTimeSeries<1>, meta: SeriesMetaData) {
self.0.push((ts, meta));
}
}
/// `(DataType, Country)` key to lookup data or spec.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct PageKey {
///
pub country: Country,
///
pub data_type: DataType,
///
pub index: usize,
}
impl PageKey {
/// Return a new `PageKey`.
pub fn new(country: Country, data_type: DataType, index: usize) -> Self {
PageKey { country, data_type, index }
}
}
impl<'a> TryInto<PageKey> for KeyTreeRef<'a> {
type Error = keytree::Error;
fn try_into(self) -> Result<PageKey, Self::Error> {
Ok(PageKey {
index: self.value("key::index")?,
data_type: self.value("key::data_type")?,
country: self.value("key::country")?,
})
}
}
let data_spec = DataSpec::from_file("source_data.keytree").unwrap();
let ts_spec = TSSpec::from_file("ts_spec.keytree");
// Step 1
// println!("{}", DataSelector::from_file("series_selector.keytree")
// .into_data_spec()
// .unwrap()
// .keytree());
let ts_json = ts_spec.into_json(&data_spec, &root_dir);
}
// println!("{}", DataSelector::from_file("germany_selector.keytree")
// .into_data_spec()
// .unwrap()
// .keytree());
// println!("{}", DataSelector::from_file("austria_selector.keytree")
// .into_data_spec()
// .unwrap()
// .keytree());
// (paste this)
// Step 2
let data_spec = DataSpec::from_file("source_data.keytree").unwrap();
// data_spec.update_write(&root_dir).unwrap();
// println!("{}", data_spec.generic_ts_spec().keytree());
// (paste this)
// Step 4
let ts_spec = ts::Spec::from_file("ts_spec.keytree").unwrap();
let ts_data = TSData::from_spec(&ts_spec, &root_dir).unwrap();
dbg!(&ts_data);
// Fred::series_observations("AUSURTOTMDSMEI").unwrap();
// JSON does have "." as first value. What should we do? drop_until_parsable ? Or just define
// dates?
// We have to know where the parsing happens.
// It happens in "TimeSeries::<1>::from_csv(&path)"
}
//! to resume.
//! to resume. To update the data files due to a change of the DataSpec,
//! ```
//! let date = OffsetDateTime::now_utc() - Duration::days(3);
//! data_spec
//! .update_write(date.into(), &root_dir)
//! .unwrap();
//! ```
let year = s[..4].parse().map_err(|_| parse_date("failed"))?;
let month = s[5..7].parse().map_err(|_| parse_date("failed"))?;
let year = s[..4].parse().map_err(|_| parse_date(file!(), line!(), "failed"))?;
let month = s[5..7].parse().map_err(|_| parse_date(file!(), line!(), "failed"))?;
pub fn get_series_spec(&self, series_id: &SeriesId) -> SeriesSpec {
let key = self.reverse.get(&series_id).unwrap();
let seriess = self.map.get(&key).unwrap();
(*seriess.iter().find(|series| &series.series_id == series_id).unwrap()).clone()
}
pub fn get_series_spec(&self, series_id: &SeriesId) -> Option<SeriesSpec> {
/// Test if a key `(DataType, Country)` has a `SeriesId`.
pub fn key_has_series_id(&self, key: (DataType, Country), series_id: &SeriesId) -> Result<bool, Error> {
let series_specs = match self.map.get(&key) {
Some(series_specs) => series_specs,
None => { return Err(key_not_in_dataspec(&key.0.to_string(), &key.1.to_string())) },
let key = match self.reverse.get(&series_id) {
Some(key) => key,
None => { return None },
series_spec.write_data(root_path);
series_spec.write_meta(root_path);
series_spec.write_data(root_path)?;
series_spec.write_meta(root_path)?;
}
}
Ok(())
}
// Need to keep a record of all files and remove files that shouldn't exist.
//
// i.e. its not sufficient to check if a file exists. We need to check if a file remains. So we
// need to create a file list.
/// Only make requests and updata data to Fred for files that are in `DataSpec` but do not exist
/// as data files.
pub fn update_write(&self, root_path: &str) -> Result<(), Error> {
for (_, series_specs) in &self.map {
for series_spec in series_specs {
if !series_spec.exists(root_path)? {
series_spec.write_data(root_path)?;
series_spec.write_meta(root_path)?;
}
}
}
self.remove_old(root_path)?;
Ok(())
}
/// Run through data files, query and remove any files that are not in directory.
pub fn remove_old(&self, root_path: &str) -> Result<(), Error> {
for entry in WalkDir::new(root_path) {
let entry = entry.unwrap();
if entry.file_type().is_dir() { continue };
let pathbuf = entry.path();
let mut path_iter = pathbuf.iter().rev().map(|os_str| os_str.to_str().unwrap());
let mut file_parts = path_iter.next().unwrap().split('.');
let file_stem = file_parts.next().unwrap();
let file_ext = file_parts.next().unwrap();
if (file_ext != "csv") && (file_ext != "meta") {
println!("Unknown file [{}]", pathbuf.to_str().unwrap());
panic!();
.skip_while(|((data_type, country), series_specs)| {
match self.key_has_series_id((*data_type, *country), &sid) {
Err(err) => {
println!("{}", err);
panic!();
},
Ok(is_true) => !is_true,
.skip_while(|_| {
match self.get_series_spec(&sid) {
Some(series_spec) => sid != series_spec.series_id,
None => true,
/// Check if a file exists.
pub fn exists(&self, root_path: &str) -> Result<bool, Error> {
let csv_path = data_path(
root_path,
self.data_type,
self.country,
self.series_id.clone(),
"csv",
);
let meta_path = data_path(
root_path,
self.data_type,
self.country,
self.series_id.clone(),
"meta",
);
Ok(
Path::new(&csv_path).exists() &&
Path::new(&meta_path).exists()
)
}
/// Return the meta data for a `SeriesSpec`.
pub fn meta(&self, root_path: &str) -> SeriesMetaData {
/// Use `Self` to make a Fred request for series data, and return a `RegularTimeSeries`.
pub fn into_time_series(&self) -> Result<RegularTimeSeries<1>, Error> {
// Select the observation data from the FRED data.
let observations = match Fred::series_observations(&self.series_id.to_string()) {
Ok(series_obs) => series_obs.observations,
Err(err) => { return Err(fred_error(file!(), line!(), &err.to_string())) },
};
let path = &format!(
"{}/{}/{}/{}.meta",
root_path,
self.data_type,
self.country.as_path(),
self.series_id,
);
// We need to build a RegularTimeSeries here, and then use RegularTimeSeries here
// to build csv file.
// Data is parsed into a RegularTimeSeries
let path = &format!(
"{}/{}/{}/{}.csv",
root_path,
self.data_type,
self.country.as_path(),
self.series_id,
);
// Drop the first n items
let skip = match self.drop_first {
None => 0,
Some(n) => n,
};
for (i, obs) in observations.iter().enumerate().skip(skip) {
let date = MonthlyDate::from_str(&obs.date)?;
let value: f32 = match obs.value.parse() {
Ok(n) => n,
Err(_) => {
let err = parse_fred_value_failed(
file!(),
line!(),
&self.data_type.to_string(),
&self.country.to_string(),
&self.series_id.to_string(),
&format!(
"{}, {}",
obs.date,
obs.value,
),
i + 1,
);
self.soft_parse(observations, err);
unreachable!();
},
};
match TimeSeries::<1>::from_csv(&path) {
Ok(ts) => {
match ts.try_into() {
Ok(rts) => Ok(rts),
Err(err) => Err(from_time_series(&err.to_string())),
}
let date_point = DatePoint::<1>::new(date.0, [value]);
v.push(date_point)
}
let ts = TimeSeries::new(v);
let rts = match ts.try_into() {
Ok(rts) => {
rts
},
Err(_) => {
let err = expected_regular_time_series(
file!(),
line!(),
&self.data_type.to_string(),
&self.country.to_string(),
&self.series_id.to_string(),
);
self.soft_parse(observations, err);
unreachable!();
Err(err) => Err(from_time_series(&err.to_string())),
};
Ok(rts)
}
/// Parse observations into string for debugging and exit.
pub fn soft_parse(&self, observations: fred_api::Observations, err: Error) {
for (i, obs) in observations.iter().enumerate() {
eprintln!(
"{} {}, {}",
i + 1,
obs.date,
obs.value,
)
/// Fetches data as specified in `checked_data.keytree` and saves to disk.
pub fn write_data(&self, root_path: &str) {
let mut csv = String::new();
for date_point in rts.iter(DateRange::new(None, None)) {
let date = date_point.date();
let date_str = format!(
"{}-{:02}-01",
date.year(),
date.month(),
);
// Select the observation data from the FRED data.
let observations = match Fred::series_observations(&self.series_id.to_string()) {
Ok(series_obs) => series_obs.observations,
Err(err) => {
println!("{}", err);
panic! ();
},
};
let mut data = String::new();
for obs in observations.iter() {
data.push_str(&obs.to_string());
data.push('\n');
let line = format!(
"{}, {}\n",
date_str,
date_point.value(0),
);
csv.push_str(&line);
/// Fetches data as specified in `checked_data.keytree` and saves meta_data to disk.
pub fn write_meta(&self, root_path: &str) {
/// Fetches data as specified in `source_data.keytree` and saves meta_data to disk.
pub fn write_meta(&self, root_path: &str) -> Result<(), Error> {
// self.time_stamp = Some(TimeStamp::now());
/// Read csv data from file and return a time-series.
pub fn data_from_file(
country: Country,
data_type: DataType,
series_id: SeriesId,
root_path: &str) -> Result<RegularTimeSeries<1>, Error>
{
let path = data_path(
root_path,
data_type,
country,
series_id.clone(),
"csv"
);
match TimeSeries::<1>::from_csv(&path) {
Ok(ts) => {
match ts.try_into() {
Ok(rts) => Ok(rts),
Err(err) => {
Err(time_series_from_csv_failed(
file!(),
line!(),
&data_type.to_string(),
&country.to_string(),
&series_id.to_string(),
&err.to_string(),
))
},
}
},
Err(err) => Err(time_series_from_csv_failed(
file!(),
line!(),
&data_type.to_string(),
&country.to_string(),
&series_id.to_string(),
&err.to_string(),
))
}
}
/// Return the meta data for a `SeriesSpec`.
pub fn meta_from_file(
country: Country,
data_type: DataType,
series_id: SeriesId,
root_path: &str) -> SeriesMetaData
{
let path = data_path(
root_path,
data_type,
country,
series_id.clone(),
"meta"
);
let meta_str = fs::read_to_string(path).unwrap();
let kt = KeyTree::parse(&meta_str).unwrap();
kt.to_ref().try_into().unwrap()
}
walkdir = "2.3.2"