Skip to content

Commit a297909

Browse files
ENH: Allow pulling of partial results (#46)
1 parent e456c8a commit a297909

2 files changed

Lines changed: 76 additions & 8 deletions

File tree

fourinsight/engineroom/utils/_core.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -436,42 +436,51 @@ def append(self, dataframe):
436436
self.new_row(row_i)
437437
self.collect(**result_i)
438438

439-
def pull(self, raise_on_missing=True):
439+
def pull(self, raise_on_missing=True, strict=True):
440440
"""
441441
Pull results from source. Remote source overwrites existing values.
442442
443443
Parameters
444444
----------
445445
raise_on_missing : bool
446446
Raise exception if results can not be pulled from source.
447+
strict : bool
448+
Whether to be strict with respect to headers in the source or not. Setting
449+
`strict=True` (default) will require that the source has the exact same
450+
headers as the `ResultCollector`. Setting `strict=False` will allow pulling
451+
of partial results (i.e., headers that does not have results in source,
452+
will be populated with `None` values).
447453
"""
448454

449455
self._handler.pull(raise_on_missing=raise_on_missing)
450456
if not self._handler.getvalue():
451457
return
452458

453459
self._handler.seek(0)
454-
df = pd.read_csv(
460+
df_source = pd.read_csv(
455461
self._handler, index_col=0, parse_dates=True, dtype=self._headers
456462
)
457463

458-
if not (set(df.columns) == set(self._headers.keys())):
464+
if strict and set(df_source.columns) != set(self._headers.keys()):
459465
raise ValueError("Header is not valid.")
460466

461467
if (
462-
not df.index.empty
468+
not df_source.index.empty
463469
and (self._indexing_mode == "auto")
464-
and not (df.index.dtype == "int64")
470+
and not (df_source.index.dtype == "int64")
465471
):
466472
raise ValueError("Index dtype must be 'int64'.")
467473
elif (
468-
not df.index.empty
474+
not df_source.index.empty
469475
and (self._indexing_mode == "timestamp")
470-
and not (isinstance(df.index, pd.DatetimeIndex))
476+
and not (isinstance(df_source.index, pd.DatetimeIndex))
471477
):
472478
raise ValueError("Index must be 'DatetimeIndex'.")
473479

474-
self._dataframe = df
480+
columns_missing = self.dataframe.columns.difference(df_source.columns)
481+
df_source[columns_missing] = None
482+
483+
self._dataframe = df_source[self._headers.keys()].astype(self._headers)
475484

476485
def push(self):
477486
"""

tests/test_core.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,65 @@ def test_pull_non_existing_raises(self):
667667
with pytest.raises(FileNotFoundError):
668668
results.pull(raise_on_missing=True)
669669

670+
def test_pull_strict(self, tmp_path):
671+
handler = LocalFileHandler(tmp_path / "results.csv")
672+
673+
headers_a = {"a": float, "b": str, "c": int}
674+
results_a = ResultCollector(headers_a, handler=handler)
675+
results_a.new_row()
676+
results_a.collect(a=1.2, b="foo", c=3)
677+
results_a.new_row()
678+
results_a.collect(a=4.5, b="bar", c=6)
679+
results_a.push()
680+
681+
headers_b = {"b": str, "c": int, "d": float} # partially different headers
682+
results_b = ResultCollector(headers_b, handler=handler)
683+
results_b.pull(strict=False)
684+
685+
results_b.push()
686+
results_a.pull(strict=False)
687+
688+
df_out_a = results_a.dataframe
689+
df_out_b = results_b.dataframe
690+
691+
df_expect_a = pd.DataFrame(
692+
data={
693+
"a": [None, None],
694+
"b": ["foo", "bar"],
695+
"c": [3, 6],
696+
},
697+
index=pd.Index([0, 1], dtype="int64"),
698+
).astype({"a": "float64", "b": "string", "c": "Int64"})
699+
700+
df_expect_b = pd.DataFrame(
701+
data={
702+
"b": ["foo", "bar"],
703+
"c": [3, 6],
704+
"d": [None, None],
705+
},
706+
index=pd.Index([0, 1], dtype="int64"),
707+
).astype({"b": "string", "c": "Int64", "d": "float64"})
708+
709+
# Remove `check_index_type=False` when issue with index type in `pull` is fixed
710+
pd.testing.assert_frame_equal(df_out_a, df_expect_a, check_index_type=False)
711+
pd.testing.assert_frame_equal(df_out_b, df_expect_b, check_index_type=False)
712+
713+
def test_pull_strict_raises(self, tmp_path):
714+
handler = LocalFileHandler(tmp_path / "results.csv")
715+
716+
headers_a = {"a": float, "b": str, "c": int}
717+
results_a = ResultCollector(headers_a, handler=handler)
718+
results_a.new_row()
719+
results_a.collect(a=1.2, b="foo", c=3)
720+
results_a.new_row()
721+
results_a.collect(a=4.5, b="bar", c=6)
722+
results_a.push()
723+
724+
headers_b = {"b": str, "c": int, "d": float} # different headers
725+
results_b = ResultCollector(headers_b, handler=handler)
726+
with pytest.raises(ValueError):
727+
results_b.pull()
728+
670729
def test_push_auto(self, tmp_path):
671730
handler = LocalFileHandler(tmp_path / "results.csv")
672731
headers = {"a": float, "b": str, "c": float, "d": str}

0 commit comments

Comments
 (0)