Skip to content

Commit 8950aef

Browse files
authored
Merge pull request #33 from CentralValleyModeling/main
Release `0.8.3`
2 parents c54f3fd + 0cb30fb commit 8950aef

7 files changed

Lines changed: 161 additions & 16 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pandss"
7-
version = "0.8.2"
7+
version = "0.8.3"
88
dependencies = [
99
"pyhecdss==1.3",
1010
"pandas==2",

src/pandss/dss.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .paths import DatasetPath, DatasetPathCollection
1111
from .quiet import silent, suppress_stdout_stderr
1212
from .timeseries import RegularTimeseries
13+
from .timeseries.interval import Interval
1314

1415
module_engine = ModuleEngine()
1516

@@ -349,6 +350,73 @@ def write_rts(self, path: DatasetPath | str, rts: RegularTimeseries):
349350
with suppress_stdout_stderr():
350351
return self.engine.write_rts(path, rts)
351352

353+
def write_dataframe(
354+
self,
355+
paths: list[DatasetPath],
356+
df: pd.DataFrame,
357+
units: str | list[str],
358+
period_types: str | list[str],
359+
intervals: str | Interval | list[str | Interval],
360+
):
361+
"""Write DSS data from a DataFrame, where each column is a RegularTimeseries
362+
363+
Parameters
364+
----------
365+
paths : list[DatasetPath]
366+
The paths to write to in the DSS file.
367+
df : pd.DataFrame
368+
The data to convert into a DSS file.
369+
units : str | list[str]
370+
The units to use for the data in the DSS file.
371+
period_types : str | list[str]
372+
The DSS period types to use for the data in the DSS file.
373+
intervals : str | Interval | list[str | Interval]
374+
The DSS intervals to use for the data in the DSS file.
375+
376+
Raises
377+
------
378+
ValueError
379+
Raised when the data given cannot be assigned uniquely to the columns of the
380+
DataFrame, or when a RegularTimeseries object cannot be created.
381+
382+
"""
383+
if isinstance(units, str):
384+
units = [units for _ in paths]
385+
if isinstance(period_types, str):
386+
period_types = [period_types for _ in paths]
387+
if isinstance(intervals, (str, Interval)):
388+
intervals = [intervals for _ in paths]
389+
390+
try:
391+
data = zip(
392+
paths,
393+
df.columns,
394+
units,
395+
period_types,
396+
intervals,
397+
)
398+
except Exception as e:
399+
raise ValueError(
400+
"The lengths of the data given do not match,"
401+
+ " could not determine a 1-to-1 retionship between inputs"
402+
+ " and dataframe columns"
403+
) from e
404+
for p, col, unit, pt, inter in data:
405+
series = df[col]
406+
try:
407+
rts = RegularTimeseries.from_series(
408+
s=series,
409+
path=p,
410+
units=unit,
411+
period_type=pt,
412+
interval=inter,
413+
)
414+
except Exception as e:
415+
raise ValueError(
416+
"Could not create a RegularTimeseries object from the data given."
417+
) from e
418+
self.write_rts(path=p, rts=rts)
419+
352420
def resolve_wildcard(
353421
self,
354422
path: DatasetPath | str,

src/pandss/paths/dataset_path.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,16 @@ def __eq__(self, __other: Any):
158158
for f in fields(self)
159159
)
160160
except AttributeError:
161+
# If the attributes don't match, return False
161162
return False
162163
elif isinstance(__other, str):
163164
# First clean up, then do str comparison
164-
return str(self) == str(self.__class__.from_str(__other))
165+
try:
166+
__other = self.__class__.from_str(__other)
167+
return str(self) == str(__other)
168+
except DatasetPathParseError:
169+
# If you can't parse the second string, return False
170+
return False
165171
else:
166172
# Resort to regular string comparison
167173
return str(self) == str(__other)

src/pandss/timeseries/regular_timeseries.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from numpy import array, datetime64, datetime_as_string, intersect1d, ndarray
66
from numpy.typing import NDArray
7-
from pandas import DataFrame, Index, MultiIndex
7+
from pandas import DataFrame, Index, MultiIndex, Series
88

99
from ..paths import DatasetPath
1010
from .interval import Interval
@@ -278,6 +278,28 @@ def to_frame(
278278

279279
return df
280280

281+
@classmethod
282+
def from_series(
283+
cls,
284+
s: Series,
285+
path: DatasetPath | str,
286+
period_type: str,
287+
units: str,
288+
interval: Interval | str,
289+
) -> "RegularTimeseries":
290+
if isinstance(interval, str):
291+
interval = Interval(interval)
292+
if isinstance(path, str):
293+
path = DatasetPath.from_str(path)
294+
return cls(
295+
path=path,
296+
values=s.values, # type: ignore
297+
dates=s.index, # type: ignore
298+
period_type=period_type,
299+
units=units,
300+
interval=interval,
301+
)
302+
281303
def to_json(self) -> dict:
282304
"""Create a JSON-compliant dictionary with the RegularTimeseries data
283305

tests/test_dss.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,23 @@ def test_to_plaintext(dss, created_dir, random_name, request: FixtureRequest):
9191
rts_new = new_dss.read_rts(p)
9292
assert len(rts_old.values) == len(rts_new.values)
9393
assert (sum(rts_old.values) - sum(rts_new.values)) < 0.000_000_000_1
94+
95+
96+
@pytest.mark.parametrize("dss", ("dss_6", "dss_7", "dss_large"))
97+
def test_write_from_dataframe(dss, request: FixtureRequest):
98+
dss: Path = request.getfixturevalue(dss)
99+
p = "/CALSIM/MONTH_DAYS/DAY//1MON/L2020A/"
100+
p_new = "/CALSIM/TEST_WRITE_FROM_DF/DAY//1MON/L2020A/"
101+
with pdss.DSS(dss) as DSS:
102+
rts = DSS.read_rts(p)
103+
df = rts.to_frame()
104+
DSS.write_dataframe(
105+
paths=[pdss.DatasetPath.from_str(p_new)],
106+
df=df,
107+
units=[rts.units],
108+
period_types=[rts.period_type],
109+
intervals=[rts.interval],
110+
)
111+
with pdss.DSS(dss) as DSS:
112+
rts_new = DSS.read_rts(p_new)
113+
assert rts_new.interval == rts.interval

tests/test_paths.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,27 @@ def test_to_catalog():
250250
assert tuple(df.columns) == ("A", "B", "C", "D", "E", "F")
251251
assert tuple(df["B"]) == ("1", "2")
252252
assert tuple(df["C"]) == (".*", ".*")
253+
254+
255+
def test_pattern_matching_as_str():
256+
dsp = pdss.DatasetPath(b="FOO")
257+
match dsp:
258+
case "/OTHER/FOO/.*/.*/.*/.*/":
259+
assert False, "pattern matching succeeded too early"
260+
case "FOO":
261+
assert False, "pattern matching succeeded too early"
262+
case "/.*/FOO/.*/.*/.*/.*/":
263+
pass
264+
case _:
265+
assert False, "pattern matching failed"
266+
267+
268+
def test_pattern_matching():
269+
dsp = pdss.DatasetPath(b="FOO")
270+
match dsp:
271+
case pdss.DatasetPath(b="FOO", c="BAR"):
272+
assert False, "pattern matching succeeded too early"
273+
case pdss.DatasetPath(b="FOO"):
274+
pass
275+
case _:
276+
assert False, "pattern matching failed"

tests/test_rts.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,24 @@ def test_accept_path_as_str():
207207
assert rts.path.b == "MONTH_DAYS"
208208

209209

210+
def test_rts_from_series():
211+
series = pd.Series(
212+
data=[1, 2, 3, 4, 5],
213+
index=pd.DatetimeIndex(
214+
data=["2000-01-01", "2000-02-01", "2000-03-01", "2000-04-01", "2000-05-01"]
215+
),
216+
)
217+
path = pdss.DatasetPath(b="TEST")
218+
rts = pdss.RegularTimeseries.from_series(
219+
series,
220+
path,
221+
period_type="PER-AVG",
222+
units="NONE",
223+
interval="1MON",
224+
)
225+
assert isinstance(rts, pdss.RegularTimeseries)
226+
227+
210228
@pytest.mark.parametrize("dss", ("dss_6", "dss_7"))
211229
def test_write_rts(dss, temporary_dss, request: FixtureRequest):
212230
dss = request.getfixturevalue(dss)
@@ -306,17 +324,4 @@ def test_read_write_daily_inst(dss, temporary_dss, request: FixtureRequest):
306324
assert L == R
307325

308326

309-
@pytest.mark.parametrize("dss", ("dss_6", "dss_7"))
310-
def test_to_frame_small_header(dss, request: FixtureRequest):
311-
dss = request.getfixturevalue(dss)
312-
p1 = pdss.DatasetPath(b="MONTH_DAYS")
313-
rts = pdss.read_rts(dss, p1)
314-
df_1 = rts.to_frame(include_in_header="B")
315-
assert isinstance(df_1.columns, pd.Index)
316-
assert len(df_1.columns) == 1
317-
df_2 = rts.to_frame(include_in_header=("B", "C"))
318-
assert isinstance(df_2.columns, pd.MultiIndex)
319-
assert len(df_2.columns) == 1
320-
321-
322327
# TODO: add tests for daily data

0 commit comments

Comments
 (0)