bulum 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {bulum-0.3.2/src/bulum.egg-info → bulum-0.3.3}/PKG-INFO +37 -10
  2. {bulum-0.3.2 → bulum-0.3.3}/README.md +31 -0
  3. bulum-0.3.3/pyproject.toml +47 -0
  4. {bulum-0.3.2 → bulum-0.3.3}/setup.py +1 -1
  5. bulum-0.3.3/src/bulum/clim/tests/test_clim.py +71 -0
  6. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/io/__init__.py +0 -1
  7. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/io/csv_io.py +39 -16
  8. bulum-0.3.3/src/bulum/io/general_io.py +55 -0
  9. bulum-0.3.3/src/bulum/io/idx_io.py +341 -0
  10. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/io/iqqm_out_reader.py +98 -68
  11. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/io/lqn_io.py +13 -4
  12. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/io/res_csv_io.py +70 -31
  13. bulum-0.3.3/src/bulum/io/tests/test_csv_io.py +55 -0
  14. bulum-0.3.3/src/bulum/io/tests/test_general_io.py +22 -0
  15. bulum-0.3.3/src/bulum/io/tests/test_idx_io.py +112 -0
  16. bulum-0.3.3/src/bulum/io/tests/test_iqqm_out_reader.py +48 -0
  17. bulum-0.3.3/src/bulum/io/tests/test_lqn_io.py +52 -0
  18. bulum-0.3.3/src/bulum/io/tests/test_res_csv_io.py +60 -0
  19. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/maps/station_maps.py +1 -1
  20. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/altair_plots.py +3 -3
  21. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/ensemble_altair_plots.py +2 -0
  22. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/tests/test_plot_functions.py +13 -15
  23. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/__init__.py +0 -1
  24. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/negflo.py +336 -126
  25. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/reliability_stats_class.py +1 -1
  26. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/stochastic_data_check.py +1 -1
  27. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/storage_level_assessment.py +9 -7
  28. bulum-0.3.3/src/bulum/stats/swflo2s/tests/test_swflo2s.py +58 -0
  29. bulum-0.3.3/src/bulum/stats/tests/test_negflo.py +359 -0
  30. bulum-0.3.3/src/bulum/stats/tests/test_stats.py +365 -0
  31. bulum-0.3.3/src/bulum/stats/tests/test_storage_level_assessment.py +750 -0
  32. bulum-0.3.3/src/bulum/stoch/tests/test_stoch.py +13 -0
  33. bulum-0.3.3/src/bulum/trans/tests/test_trans.py +20 -0
  34. bulum-0.3.3/src/bulum/utils/dataframe_extensions.py +1224 -0
  35. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/utils/dataframe_functions.py +23 -12
  36. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/utils/datetime_functions.py +215 -76
  37. bulum-0.3.3/src/bulum/utils/tests/test_ensembles.py +710 -0
  38. bulum-0.3.3/src/bulum/utils/tests/test_interpolation.py +88 -0
  39. bulum-0.3.3/src/bulum/utils/tests/test_parametrized.py +219 -0
  40. bulum-0.3.3/src/bulum/utils/tests/test_tsdf.py +540 -0
  41. bulum-0.3.3/src/bulum/utils/tests/test_utils.py +701 -0
  42. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/version.py +1 -1
  43. {bulum-0.3.2 → bulum-0.3.3/src/bulum.egg-info}/PKG-INFO +37 -10
  44. {bulum-0.3.2 → bulum-0.3.3}/src/bulum.egg-info/SOURCES.txt +20 -3
  45. bulum-0.3.2/src/bulum/io/general_io.py +0 -33
  46. bulum-0.3.2/src/bulum/io/idx_io.py +0 -65
  47. bulum-0.3.2/src/bulum/io/idx_io_native.py +0 -159
  48. bulum-0.3.2/src/bulum/stats/negflo_helpers.py +0 -154
  49. bulum-0.3.2/src/bulum/utils/dataframe_extensions.py +0 -252
  50. {bulum-0.3.2 → bulum-0.3.3}/LICENSE.md +0 -0
  51. {bulum-0.3.2 → bulum-0.3.3}/setup.cfg +0 -0
  52. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/__init__.py +0 -0
  53. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/clim/__init__.py +0 -0
  54. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/clim/clim.py +0 -0
  55. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/demo.py +0 -0
  56. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/maps/__init__.py +0 -0
  57. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/__init__.py +0 -0
  58. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/node_diagrams.py +0 -0
  59. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/plot_functions.py +0 -0
  60. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/plotly_helpers.py +0 -0
  61. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/plots/tests/__init__.py +0 -0
  62. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/aggregate_stats.py +0 -0
  63. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/ensemble_stats.py +0 -0
  64. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/swflo2s/__init__.py +0 -0
  65. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stats/swflo2s/swflo2s.py +0 -0
  66. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stoch/__init__.py +0 -0
  67. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stoch/analyse.py +0 -0
  68. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/stoch/generate.py +0 -0
  69. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/test_circular_import.py +0 -0
  70. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/trans/__init__.py +0 -0
  71. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/trans/transformers.py +0 -0
  72. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/utils/__init__.py +0 -0
  73. {bulum-0.3.2 → bulum-0.3.3}/src/bulum/utils/interpolation.py +0 -0
  74. {bulum-0.3.2 → bulum-0.3.3}/src/bulum.egg-info/dependency_links.txt +0 -0
  75. {bulum-0.3.2 → bulum-0.3.3}/src/bulum.egg-info/requires.txt +0 -0
  76. {bulum-0.3.2 → bulum-0.3.3}/src/bulum.egg-info/top_level.txt +0 -0
@@ -1,13 +1,14 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: bulum
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Open source python library for assessing hydrologic model results in Queensland
5
5
  Home-page: https://github.com/odhydrology/bulum
6
6
  Author: Chas Egan
7
- Author-email: chas@odhydrology.com
7
+ Author-email: Chas Egan <chas@odhydrology.com>
8
+ Project-URL: Homepage, https://github.com/odhydrology/bulum
8
9
  Classifier: Programming Language :: Python :: 3
9
10
  Classifier: Operating System :: OS Independent
10
- Requires-Python: >=3.9
11
+ Requires-Python: >=3.11
11
12
  Description-Content-Type: text/markdown
12
13
  License-File: LICENSE.md
13
14
  Requires-Dist: altair[all]>=5.5.0
@@ -17,14 +18,9 @@ Requires-Dist: numpy>=1.26.4
17
18
  Requires-Dist: pandas>=2.2.0
18
19
  Requires-Dist: plotly>=5.18.0
19
20
  Dynamic: author
20
- Dynamic: author-email
21
- Dynamic: classifier
22
- Dynamic: description
23
- Dynamic: description-content-type
24
21
  Dynamic: home-page
25
- Dynamic: requires-dist
22
+ Dynamic: license-file
26
23
  Dynamic: requires-python
27
- Dynamic: summary
28
24
 
29
25
  # bulum
30
26
 
@@ -81,8 +77,39 @@ Where can I find the API token password? Chas has it in his emails. It is also h
81
77
 
82
78
  How do I make a new API token? Go to your PyPi account settings, and click on "API tokens". Then click on "Add API token", and give it a name. The token will be displayed on the next screen.
83
79
 
80
+ Alternatively,
81
+ ```bash
82
+ uv build
83
+ uv publish --token XXXXX
84
+ ```
85
+
84
86
  ## Unit Tests
85
87
 
88
+ For development testing, first sync with extras=dev via
89
+ ```bash
90
+ uv sync --extra dev
91
+ ```
92
+
93
+ To run tests with the current or a particular version of python, from the project root run pytest via (for example):
94
+ ```bash
95
+ uv run pytest
96
+ uv run
97
+ uv run tox -e py312 # to run a particular version
98
+ ```
99
+
100
+ Before publishing, check the library is functional across all supported versions of python via (one of):
101
+ ```bash
102
+ uv run tox
103
+ uv run tox -p auto # This is run in parallel
104
+ ```
105
+
106
+ Note: if you are missing python versions, install via (e.g.)
107
+ ```bash
108
+ uv python install 3.12
109
+ ```
110
+
111
+ ### Legacy
112
+
86
113
  WARNING: Run unit tests from an anaconda environment with compatible dependencies!
87
114
 
88
115
  Install the nose2 test-runner framework.
@@ -53,8 +53,39 @@ Where can I find the API token password? Chas has it in his emails. It is also h
53
53
 
54
54
  How do I make a new API token? Go to your PyPi account settings, and click on "API tokens". Then click on "Add API token", and give it a name. The token will be displayed on the next screen.
55
55
 
56
+ Alternatively,
57
+ ```bash
58
+ uv build
59
+ uv publish --token XXXXX
60
+ ```
61
+
56
62
  ## Unit Tests
57
63
 
64
+ For development testing, first sync with extras=dev via
65
+ ```bash
66
+ uv sync --extra dev
67
+ ```
68
+
69
+ To run tests with the current or a particular version of python, from the project root run pytest via (for example):
70
+ ```bash
71
+ uv run pytest
72
+ uv run
73
+ uv run tox -e py312 # to run a particular version
74
+ ```
75
+
76
+ Before publishing, check the library is functional across all supported versions of python via (one of):
77
+ ```bash
78
+ uv run tox
79
+ uv run tox -p auto # This is run in parallel
80
+ ```
81
+
82
+ Note: if you are missing python versions, install via (e.g.)
83
+ ```bash
84
+ uv python install 3.12
85
+ ```
86
+
87
+ ### Legacy
88
+
58
89
  WARNING: Run unit tests from an anaconda environment with compatible dependencies!
59
90
 
60
91
  Install the nose2 test-runner framework.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "bulum"
3
+ dynamic = ["version"]
4
+ description = "Open source python library for assessing hydrologic model results in Queensland"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ authors = [
8
+ { name = "Chas Egan", email = "chas@odhydrology.com" }
9
+ ]
10
+ classifiers = [
11
+ "Programming Language :: Python :: 3",
12
+ "Operating System :: OS Independent",
13
+ ]
14
+ dependencies = [
15
+ "altair[all]>=5.5.0",
16
+ "folium>=0.14",
17
+ "matplotlib>=3.8.3",
18
+ "numpy>=1.26.4",
19
+ "pandas>=2.2.0",
20
+ "plotly>=5.18.0",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/odhydrology/bulum"
25
+
26
+ [build-system]
27
+ requires = ["setuptools>=61.0", "wheel"]
28
+ build-backend = "setuptools.build_meta"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.setuptools.dynamic]
34
+ version = { attr = "bulum.version.__version__" }
35
+
36
+ [tool.pytest.ini_options]
37
+ filterwarnings = [
38
+ "ignore::DeprecationWarning:matplotlib",
39
+ "ignore::DeprecationWarning:PIL",
40
+ "ignore::PendingDeprecationWarning",
41
+ ]
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "pytest>=9.0.3",
46
+ "tox>=4.54.0",
47
+ ]
@@ -8,7 +8,7 @@ exec(open('src/bulum/version.py').read())
8
8
  setuptools.setup(
9
9
  name="bulum",
10
10
  version=__version__,
11
- python_requires=">=3.9",
11
+ python_requires=">=3.11",
12
12
  author="Chas Egan",
13
13
  author_email="chas@odhydrology.com",
14
14
  description="Open source python library for assessing hydrologic model results in Queensland",
@@ -0,0 +1,71 @@
1
+ import unittest
2
+ import os
3
+ import bulum.io as bio
4
+ import bulum.clim as ocl
5
+ import bulum.utils as out
6
+ import pandas as pd
7
+
8
+
9
+ class Tests(unittest.TestCase):
10
+
11
+ @classmethod
12
+ def setUpClass(cls):
13
+ os.makedirs("./trash", exist_ok=True)
14
+ return super().setUpClass()
15
+
16
+ def test_load_data_into_df(self):
17
+ """
18
+ The purpose of this is just to test that if I load 2 different files as
19
+ dataframes, and then simply put them together. I.e. does it join on the
20
+ dates? YES IT APPEARS TO LEFT-JOIN on the dates from the first file.
21
+ """
22
+ df1 = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.withmissing.csv", date_format=r"%Y-%m-%d")["FRex_FR.qqm"]
23
+ df2 = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.RCP85_P2050_GCM8.csv", date_format=r"%Y-%m-%d")["FRex_FR.qqm"]
24
+ df3 = pd.DataFrame()
25
+ df3["x"] = df1
26
+ df3["y"] = df2
27
+ # print(df3.head())
28
+ # print(df3.tail())
29
+ self.assertTrue(len(df3) == len(df1))
30
+ self.assertAlmostEqual(df3.x.values[-1], 136) # these values should line up despite different starting dates for each file
31
+ self.assertAlmostEqual(df3.y.values[-1], 62) # these values should line up despite different starting dates for each file
32
+ # And now doe the same again with files loaded in the opposite order
33
+ df1 = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.RCP85_P2050_GCM8.csv", date_format=r"%Y-%m-%d")["FRex_FR.qqm"]
34
+ df2 = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.withmissing.csv", date_format=r"%Y-%m-%d")["FRex_FR.qqm"]
35
+ df3 = pd.DataFrame()
36
+ df3["x"] = df1
37
+ df3["y"] = df2
38
+ # print(df3.head())
39
+ # print(df3.tail())
40
+ self.assertTrue(len(df3) == len(df1))
41
+ self.assertAlmostEqual(df3.x.values[-1], 62) # these values should line up despite different starting dates for each file
42
+ self.assertAlmostEqual(df3.y.values[-1], 136) # these values should line up despite different starting dates for each file
43
+
44
+ def test_get_exc_transform(self):
45
+ """
46
+ This gets transformation curves based on current & future climate datasets, and then checks that the transformation
47
+ curves have the same total number of points regardless of whether we use default seasons (monthly) or define 3 custom
48
+ seasons.
49
+ """
50
+ current_climate = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.withmissing.csv", date_format=r"%Y-%m-%d")
51
+ future_climate = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.RCP85_P2050_GCM8.csv", date_format=r"%Y-%m-%d")
52
+ answer1 = ocl.derive_transformation_curves(current_climate["FRex_FR.qqm"], future_climate["FRex_FR.qqm"])
53
+ answer2 = ocl.derive_transformation_curves(current_climate["FRex_FR.qqm"], future_climate["FRex_FR.qqm"], season_start_months=[3, 7, 9])
54
+ total_len_1 = sum([len(a[1]) for a in answer1.values()])
55
+ total_len_2 = sum([len(a[1]) for a in answer2.values()])
56
+ self.assertEqual(total_len_1, total_len_2)
57
+ self.assertTrue(total_len_1 > 0)
58
+
59
+ def test_apply_transformation_curves(self):
60
+ current_climate = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.withmissing.csv", date_format=r"%Y-%m-%d")
61
+ future_climate = bio.read_ts_csv("./src/bulum/clim/tests/Flow.out.RCP85_P2050_GCM8.csv", date_format=r"%Y-%m-%d")
62
+ t = ocl.derive_transformation_curves(current_climate["FRex_FR.qqm"], future_climate["FRex_FR.qqm"])
63
+ answer = pd.DataFrame()
64
+ answer["RexCurrent"] = current_climate["FRex_FR.qqm"]
65
+ answer["RexCurrent"] = future_climate["FRex_FR.qqm"]
66
+ answer["Transf"] = ocl.apply_transformation_curves(t, current_climate["FRex_FR.qqm"])
67
+ # print(answer["Current"].sum())
68
+ # print(answer["Future"].sum())
69
+ # print(answer["Transf"].sum())
70
+ self.assertAlmostEqual(answer["Transf"].sum(), 4845710.795475168) # Actually haven't checked that this is the correct answer but looks plausible.
71
+ answer.to_csv("./trash/transformed.csv")
@@ -1,7 +1,6 @@
1
1
  from .csv_io import *
2
2
  from .res_csv_io import *
3
3
  from .idx_io import *
4
- from .idx_io_native import *
5
4
  from .iqqm_out_reader import *
6
5
  from .lqn_io import *
7
6
  from .general_io import *
@@ -1,11 +1,13 @@
1
- """
1
+ """
2
2
  Functions for reading CSVs, particularly time-series CSVs.
3
3
  """
4
4
 
5
+ import os
6
+ from typing import Union
7
+
5
8
  import numpy as np
6
9
  import pandas as pd
7
10
  from bulum import utils
8
- import os
9
11
 
10
12
  na_values = ['', ' ', 'null', 'NULL', 'NAN', 'NaN', 'nan', 'NA', 'na', 'N/A' 'n/a', '#N/A', '#NA', '-NaN', '-nan']
11
13
 
@@ -20,33 +22,43 @@ def read_ts_csv(filename: str | os.PathLike, date_format=None,
20
22
 
21
23
  Parameters
22
24
  ----------
23
- filename : str | PathLike
25
+ filename : str or PathLike
26
+ Path to the CSV file to read.
24
27
  date_format : str, optional
25
28
  defaults to "%d/%m/%Y" as per Fors. Other common formats include "%Y-%m-%d", "%Y/%m/%d".
26
29
  df : pd.DataFrame, optional
27
30
  If provided, the reader will append columns to this dataframe. Defaults to None.
28
31
  colprefix : str, optional
29
- If provided, the reader will append this prefix to the start of each column name. Defaults to None.
32
+ If provided, the reader will prepend this to column name. Defaults to None.
30
33
  allow_nonnumeric : bool, optional
31
- If false, the method will assert that all columns are numerical. Defaults to False.
34
+ If False, raises if any column is not numeric. Default is False.
32
35
  assert_date : bool, optional
33
- If true, the method will assert that date index meets "%Y-%m-%d" format. Defaults to True.
36
+ If True, asserts the date index conforms to ``"%Y-%m-%d"``. Default is True.
37
+
38
+ Returns
39
+ -------
40
+ utils.TimeseriesDataframe
34
41
 
35
- Returns:
36
- pd.DataFrame: Dataframe containing the data from the csv file.
42
+ Raises
43
+ ------
44
+ TypeError
45
+ If a column is not numeric and ``allow_nonnumeric`` is False.
46
+ ValueError
47
+ If the new date range does not overlap with ``df``.
37
48
  """
49
+ new_df: pd.DataFrame
38
50
  new_df = pd.read_csv(filename, na_values=na_values, **kwargs)
39
51
  # Date index
40
52
  new_df.set_index(new_df.columns[0], inplace=True)
41
53
  if assert_date:
42
- new_df.index = utils.standardize_datestring_format(new_df.index)
43
- new_df.index.name = "Date"
54
+ new_df.index = utils.standardize_datestring_format(new_df.index, as_index=True)
55
+ new_df.index.name = "Date" # type: ignore
44
56
  # df = df.replace(r'^\s*$', np.nan, regex=True)
45
57
  # Check values
46
58
  if not allow_nonnumeric:
47
59
  for col in new_df.columns:
48
- if not np.issubdtype(new_df[col].dtype, np.number):
49
- raise Exception(f"ERROR: Column '{col}' is not numeric!")
60
+ if not np.issubdtype(new_df[col].dtype, np.number): # type: ignore
61
+ raise TypeError(f"ERROR: Column '{col}' is not numeric!")
50
62
  # Rename columns if required
51
63
  if colprefix is not None:
52
64
  for c in new_df.columns:
@@ -55,17 +67,28 @@ def read_ts_csv(filename: str | os.PathLike, date_format=None,
55
67
  if df is None:
56
68
  df = new_df
57
69
  else:
70
+ assert isinstance(df, pd.DataFrame), f"Dataframe {df=} is not a dataframe!"
58
71
  if len(df) > 0:
59
72
  # Check that the dates overlap
60
73
  newdf_ends_before_df_starts = new_df.index[0] < df.index[-1]
61
74
  df_ends_before_newdf_starts = df.index[-1] < new_df.index[0]
62
75
  if newdf_ends_before_df_starts or df_ends_before_newdf_starts:
63
- raise Exception("ERROR: The dates in the new dataframe do not overlap with the existing dataframe!")
76
+ raise ValueError("ERROR: The dates in the new dataframe do not"
77
+ " overlap with the existing dataframe!")
64
78
  df = df.join(new_df, how="outer")
65
79
  return utils.TimeseriesDataframe.from_dataframe(df)
66
80
 
67
81
 
68
- def write_ts_csv(df: pd.DataFrame, filename: str,
69
- *args, **kwargs):
70
- """Wrapper around ``pandas.DataFrame.to_csv()``."""
82
+ def write_ts_csv(df: pd.DataFrame, filename: str, *args, **kwargs) -> None:
83
+ """Write a dataframe to CSV via :meth:`pandas.DataFrame.to_csv`.
84
+
85
+ Parameters
86
+ ----------
87
+ df : pd.DataFrame
88
+ DataFrame to write.
89
+ filename : str or PathLike
90
+ Path to the output CSV file.
91
+ *args, **kwargs
92
+ Passed through to :meth:`pandas.DataFrame.to_csv`.
93
+ """
71
94
  df.to_csv(filename, *args, **kwargs)
@@ -0,0 +1,55 @@
1
+ """
2
+ General use IO functions.
3
+ """
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+
8
+ import bulum.io as bio
9
+ from bulum import utils
10
+
11
+
12
+ def read(filename: str | os.PathLike, **kwargs) -> utils.TimeseriesDataframe:
13
+ """Read a timeseries file, dispatching to the appropriate reader based on file extension.
14
+
15
+ Supported formats: ``.res.csv``, ``.csv``, ``.idx``, and IQQM listquan
16
+ outputs (e.g. ``.01d``).
17
+
18
+ Parameters
19
+ ----------
20
+ filename : str or PathLike
21
+ Path to the file to read.
22
+ **kwargs
23
+ Passed through to the underlying reader function.
24
+
25
+ Returns
26
+ -------
27
+ utils.TimeseriesDataframe
28
+
29
+ Raises
30
+ ------
31
+ ValueError
32
+ If the file extension is not recognised, or if a ``.res.csv`` file
33
+ cannot be parsed.
34
+ """
35
+ if not isinstance(filename, Path):
36
+ filename = Path(filename)
37
+ filename_suffix = filename.suffix.lower()
38
+
39
+ df = None
40
+ if filename.name.lower().endswith(".res.csv"):
41
+ df = bio.read_res_csv(filename, **kwargs)
42
+ if df is None:
43
+ raise ValueError("Res csv could not be read.")
44
+ elif filename_suffix == ".csv":
45
+ df = bio.read_ts_csv(filename, **kwargs)
46
+ elif filename_suffix == ".idx":
47
+ df = bio.read_idx(filename, **kwargs)
48
+ elif re.search(".[0-9a-f]{2}d$", filename_suffix):
49
+ df = bio.read_iqqm_lqn_output(filename, **kwargs)
50
+ else:
51
+ raise ValueError(f"Unknown file extension: {filename}")
52
+
53
+ assert isinstance(df, utils.TimeseriesDataframe), \
54
+ "Output of `read` is not a TimeseriesDataframe."
55
+ return df