dycw-utilities 0.109.13__py3-none-any.whl → 0.109.15__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-utilities
3
- Version: 0.109.13
3
+ Version: 0.109.15
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -1,5 +1,5 @@
1
- utilities/__init__.py,sha256=8b_ivOe7dAl9N0yVvE9__TwO49X5ejp8NxQ28w6Xp0g,61
2
- utilities/altair.py,sha256=5WPwsHJoM-ZDh9iek4eSqf8_Ifb3w3KDwi7f2mrtXn4,9174
1
+ utilities/__init__.py,sha256=qZyUzwQ2zZu6ZkKszEeJGs10nbVWvqm_izEvqG8MgXM,61
2
+ utilities/altair.py,sha256=Gpja-flOo-Db0PIPJLJsgzAlXWoKUjPU1qY-DQ829ek,9156
3
3
  utilities/astor.py,sha256=xuDUkjq0-b6fhtwjhbnebzbqQZAjMSHR1IIS5uOodVg,777
4
4
  utilities/asyncio.py,sha256=41oQUurWMvadFK5gFnaG21hMM0Vmfn2WS6OpC0R9mas,14757
5
5
  utilities/atomicwrites.py,sha256=geFjn9Pwn-tTrtoGjDDxWli9NqbYfy3gGL6ZBctiqSo,5393
@@ -26,8 +26,9 @@ utilities/hashlib.py,sha256=SVTgtguur0P4elppvzOBbLEjVM3Pea0eWB61yg2ilxo,309
26
26
  utilities/http.py,sha256=WcahTcKYRtZ04WXQoWt5EGCgFPcyHD3EJdlMfxvDt-0,946
27
27
  utilities/hypothesis.py,sha256=sLqYcrFn0I3o0R7maqliIERRtAcREk2CKG8rSHY1t5U,46205
28
28
  utilities/ipython.py,sha256=V2oMYHvEKvlNBzxDXdLvKi48oUq2SclRg5xasjaXStw,763
29
- utilities/iterables.py,sha256=GRuwMOPP7jqOnDdF113XYPjVg23VYn0ts1UlV-1Tzzo,45014
29
+ utilities/iterables.py,sha256=2Yy9gZ7BR4LXR4nlX7outFAjd4dpb3lgUo7ji_sdylY,45076
30
30
  utilities/jupyter.py,sha256=ft5JA7fBxXKzP-L9W8f2-wbF0QeYc_2uLQNFDVk4Z-M,2917
31
+ utilities/lightweight_charts.py,sha256=0xNfcsrgFI0R9xL25LtSm-W5yhfBI93qQNT6HyaXAhg,2769
31
32
  utilities/logging.py,sha256=opIwFjGKOYyMntVeCsFNXOmTY2z02hMf2UtCB76SaI4,25142
32
33
  utilities/loguru.py,sha256=MEMQVWrdECxk1e3FxGzmOf21vWT9j8CAir98SEXFKPA,3809
33
34
  utilities/luigi.py,sha256=fpH9MbxJDuo6-k9iCXRayFRtiVbUtibCJKugf7ygpv0,5988
@@ -45,8 +46,8 @@ utilities/pathlib.py,sha256=31WPMXdLIyXgYOMMl_HOI2wlo66MGSE-cgeelk-Lias,1410
45
46
  utilities/period.py,sha256=ikHXsWtDLr553cfH6p9mMaiCnIAP69B7q84ckWV3HaA,10884
46
47
  utilities/pickle.py,sha256=Bhvd7cZl-zQKQDFjUerqGuSKlHvnW1K2QXeU5UZibtg,657
47
48
  utilities/platform.py,sha256=NU7ycTvAXAG-fdYmDXaM1m4EOml2cGiaYwaUzfzSqyU,1767
48
- utilities/polars.py,sha256=nB2pfK8N8HRpPE_tdbiTfFGLWC_TekAqgHlYDhnUzAM,52169
49
- utilities/polars_ols.py,sha256=AQe3RFOMv8CEI_ZCoscb_-PxB4JWjO0TAEmk8DKLeaI,2138
49
+ utilities/polars.py,sha256=aOQNVyV04qYZjg7Exi6zYERhSQoCMzBP74oufxqANFY,52167
50
+ utilities/polars_ols.py,sha256=Uc9V5kvlWZ5cU93lKZ-cfAKdVFFw81tqwLW9PxtUvMs,5618
50
51
  utilities/pqdm.py,sha256=foRytQybmOQ05pjt5LF7ANyzrIa--4ScDE3T2wd31a4,3118
51
52
  utilities/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
53
  utilities/pydantic.py,sha256=f6qtR5mO2YMuyvNmbaEj5YeD9eGA4YYfb7Bjzh9jUs0,1845
@@ -85,7 +86,7 @@ utilities/warnings.py,sha256=yUgjnmkCRf6QhdyAXzl7u0qQFejhQG3PrjoSwxpbHrs,1819
85
86
  utilities/whenever.py,sha256=TjoTAJ1R27-rKXiXzdE4GzPidmYqm0W58XydDXp-QZM,17786
86
87
  utilities/zipfile.py,sha256=24lQc9ATcJxHXBPc_tBDiJk48pWyRrlxO2fIsFxU0A8,699
87
88
  utilities/zoneinfo.py,sha256=-DQz5a0Ikw9jfSZtL0BEQkXOMC9yGn_xiJYNCLMiqEc,1989
88
- dycw_utilities-0.109.13.dist-info/METADATA,sha256=7LZJJAQIYMDDVTGd220fgWLkFlrPCQQYDATGv4AnkeI,13005
89
- dycw_utilities-0.109.13.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
90
- dycw_utilities-0.109.13.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
91
- dycw_utilities-0.109.13.dist-info/RECORD,,
89
+ dycw_utilities-0.109.15.dist-info/METADATA,sha256=sqk1b4cCCM8V737VBkLkTToWlAJ5Mb7Co4LmlynJ4Tg,13005
90
+ dycw_utilities-0.109.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
91
+ dycw_utilities-0.109.15.dist-info/licenses/LICENSE,sha256=gppZp16M6nSVpBbUBrNL6JuYfvKwZiKgV7XoKKsHzqo,1066
92
+ dycw_utilities-0.109.15.dist-info/RECORD,,
utilities/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.109.13"
3
+ __version__ = "0.109.15"
utilities/altair.py CHANGED
@@ -22,7 +22,6 @@ from altair import (
22
22
  vconcat,
23
23
  )
24
24
  from altair.utils.schemapi import Undefined
25
- from polars import Date, Datetime
26
25
 
27
26
  from utilities.functions import ensure_bytes, ensure_number
28
27
  from utilities.iterables import always_iterable
@@ -66,7 +65,7 @@ def plot_dataframes(
66
65
  ) -> VConcatChart:
67
66
  """Plot a DataFrame as a set of time series, with a multi-line tooltip."""
68
67
  import polars as pl
69
- from polars import int_range
68
+ from polars import Date, Datetime, int_range
70
69
 
71
70
  from utilities.polars import replace_time_zone
72
71
 
utilities/iterables.py CHANGED
@@ -1009,7 +1009,7 @@ def one(*iterables: Iterable[_T]) -> _T:
1009
1009
  try:
1010
1010
  first = next(it)
1011
1011
  except StopIteration:
1012
- raise OneEmptyError from None
1012
+ raise OneEmptyError(iterables=iterables) from None
1013
1013
  try:
1014
1014
  second = next(it)
1015
1015
  except StopIteration:
@@ -1018,19 +1018,19 @@ def one(*iterables: Iterable[_T]) -> _T:
1018
1018
 
1019
1019
 
1020
1020
  @dataclass(kw_only=True, slots=True)
1021
- class OneError(Exception): ...
1021
+ class OneError(Exception, Generic[_T]):
1022
+ iterables: tuple[Iterable[_T], ...]
1022
1023
 
1023
1024
 
1024
1025
  @dataclass(kw_only=True, slots=True)
1025
- class OneEmptyError(OneError):
1026
+ class OneEmptyError(OneError[_T]):
1026
1027
  @override
1027
1028
  def __str__(self) -> str:
1028
- return "Iterable(s) must not be empty"
1029
+ return f"Iterable(s) {get_repr(self.iterables)} must not be empty"
1029
1030
 
1030
1031
 
1031
1032
  @dataclass(kw_only=True, slots=True)
1032
1033
  class OneNonUniqueError(OneError, Generic[_T]):
1033
- iterables: tuple[Iterable[_T], ...]
1034
1034
  first: _T
1035
1035
  second: _T
1036
1036
 
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import asynccontextmanager
4
+ from dataclasses import dataclass
5
+ from typing import TYPE_CHECKING, override
6
+
7
+ from utilities.iterables import OneEmptyError, OneNonUniqueError, one
8
+ from utilities.reprlib import get_repr
9
+
10
+ if TYPE_CHECKING:
11
+ from collections.abc import AsyncIterator
12
+
13
+ from lightweight_charts import AbstractChart, Chart
14
+ from lightweight_charts.abstract import SeriesCommon
15
+ from polars import DataFrame
16
+ from polars._typing import SchemaDict
17
+
18
+ from utilities.types import PathLike
19
+
20
+
21
+ ##
22
+
23
+
24
+ def save_chart(chart: Chart, path: PathLike, /, *, overwrite: bool = False) -> None:
25
+ """Atomically save a chart to disk."""
26
+ from utilities.atomicwrites import writer # pragma: no cover
27
+
28
+ chart.show(block=False) # pragma: no cover
29
+ with ( # pragma: no cover
30
+ writer(path, overwrite=overwrite) as temp,
31
+ temp.open(mode="wb") as fh,
32
+ ):
33
+ _ = fh.write(chart.screenshot())
34
+ chart.exit() # pragma: no cover
35
+
36
+
37
+ ##
38
+
39
+
40
+ def set_dataframe(df: DataFrame, obj: AbstractChart | SeriesCommon, /) -> None:
41
+ """Set a `polars` DataFrame onto a Chart."""
42
+ from polars import Date, Datetime, col # pragma: no cover
43
+
44
+ try:
45
+ name = one(k for k, v in df.schema.items() if isinstance(v, Date | Datetime))
46
+ except OneEmptyError:
47
+ raise _SetDataFrameEmptyError(schema=df.schema) from None
48
+ except OneNonUniqueError as error:
49
+ raise _SetDataFrameNonUniqueError(
50
+ schema=df.schema, first=error.first, second=error.second
51
+ ) from None
52
+ return obj.set(
53
+ df.select(
54
+ col(name).alias("date").dt.strftime("iso"),
55
+ *[c for c in df.columns if c != name],
56
+ ).to_pandas()
57
+ )
58
+
59
+
60
+ @dataclass(kw_only=True, slots=True)
61
+ class SetDataFrameError(Exception):
62
+ schema: SchemaDict
63
+
64
+
65
+ @dataclass(kw_only=True, slots=True)
66
+ class _SetDataFrameEmptyError(SetDataFrameError):
67
+ @override
68
+ def __str__(self) -> str:
69
+ return "At least 1 column must be of date/datetime type; got 0"
70
+
71
+
72
+ @dataclass(kw_only=True, slots=True)
73
+ class _SetDataFrameNonUniqueError(SetDataFrameError):
74
+ first: str
75
+ second: str
76
+
77
+ @override
78
+ def __str__(self) -> str:
79
+ return f"{get_repr(self.schema)} must contain exactly 1 date/datetime column; got {self.first!r}, {self.second!r} and perhaps more"
80
+
81
+
82
+ ##
83
+
84
+
85
+ @asynccontextmanager
86
+ async def yield_chart(chart: Chart, /) -> AsyncIterator[None]:
87
+ """Yield a chart for visualization in a notebook."""
88
+ try: # pragma: no cover
89
+ yield await chart.show_async()
90
+ except BaseException: # pragma: no cover # noqa: BLE001, S110
91
+ pass
92
+ finally: # pragma: no cover
93
+ chart.exit()
94
+
95
+
96
+ __all__ = ["save_chart", "set_dataframe", "yield_chart"]
utilities/polars.py CHANGED
@@ -714,7 +714,7 @@ class _DataClassToDataFrameNonUniqueError(DataClassToDataFrameError):
714
714
 
715
715
  @override
716
716
  def __str__(self) -> str:
717
- return f"Iterable {get_repr(self.objs)} must contain exactly one class; got {self.first}, {self.second} and perhaps more"
717
+ return f"Iterable {get_repr(self.objs)} must contain exactly 1 class; got {self.first}, {self.second} and perhaps more"
718
718
 
719
719
 
720
720
  ##
utilities/polars_ols.py CHANGED
@@ -1,19 +1,22 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING, overload
4
4
 
5
- from polars import struct
5
+ from polars import Expr, Series, struct
6
6
  from polars_ols import RollingKwargs, compute_rolling_least_squares
7
7
 
8
- from utilities.polars import ensure_expr_or_series
8
+ from utilities.errors import ImpossibleCaseError
9
+ from utilities.functions import is_sequence_of
10
+ from utilities.polars import concat_series, ensure_expr_or_series
9
11
 
10
12
  if TYPE_CHECKING:
11
- from polars import Expr
13
+ from polars._typing import IntoExprColumn
12
14
  from polars_ols import NullPolicy
13
15
 
14
16
  from utilities.polars import ExprLike
15
17
 
16
18
 
19
+ @overload
17
20
  def compute_rolling_ols(
18
21
  target: ExprLike,
19
22
  *features: ExprLike,
@@ -24,9 +27,106 @@ def compute_rolling_ols(
24
27
  min_periods: int | None = None,
25
28
  use_woodbury: bool | None = None,
26
29
  alpha: float | None = None,
30
+ ) -> Expr: ...
31
+ @overload
32
+ def compute_rolling_ols(
33
+ target: Series,
34
+ *features: Series,
35
+ sample_weights: Series | None = None,
36
+ add_intercept: bool = False,
37
+ null_policy: NullPolicy = "drop_window",
38
+ window_size: int = 1000000,
39
+ min_periods: int | None = None,
40
+ use_woodbury: bool | None = None,
41
+ alpha: float | None = None,
42
+ ) -> Series: ...
43
+ @overload
44
+ def compute_rolling_ols(
45
+ target: IntoExprColumn,
46
+ *features: IntoExprColumn,
47
+ sample_weights: IntoExprColumn | None = None,
48
+ add_intercept: bool = False,
49
+ null_policy: NullPolicy = "drop_window",
50
+ window_size: int = 1000000,
51
+ min_periods: int | None = None,
52
+ use_woodbury: bool | None = None,
53
+ alpha: float | None = None,
54
+ ) -> Expr | Series: ...
55
+ def compute_rolling_ols(
56
+ target: IntoExprColumn,
57
+ *features: IntoExprColumn,
58
+ sample_weights: IntoExprColumn | None = None,
59
+ add_intercept: bool = False,
60
+ null_policy: NullPolicy = "drop_window",
61
+ window_size: int = 1000000,
62
+ min_periods: int | None = None,
63
+ use_woodbury: bool | None = None,
64
+ alpha: float | None = None,
65
+ ) -> Expr | Series:
66
+ """Compute a rolling OLS."""
67
+ target = ensure_expr_or_series(target)
68
+ features2 = tuple(map(ensure_expr_or_series, features))
69
+ sample_weights = (
70
+ None if sample_weights is None else ensure_expr_or_series(sample_weights)
71
+ )
72
+ if (
73
+ isinstance(target, Expr)
74
+ and is_sequence_of(features2, Expr)
75
+ and ((sample_weights is None) or isinstance(sample_weights, Expr))
76
+ ):
77
+ return _compute_rolling_ols_expr(
78
+ target,
79
+ *features2,
80
+ sample_weights=sample_weights,
81
+ add_intercept=add_intercept,
82
+ null_policy=null_policy,
83
+ window_size=window_size,
84
+ min_periods=min_periods,
85
+ use_woodbury=use_woodbury,
86
+ alpha=alpha,
87
+ )
88
+ if (
89
+ isinstance(target, Series)
90
+ and is_sequence_of(features2, Series)
91
+ and ((sample_weights is None) or isinstance(sample_weights, Series))
92
+ ):
93
+ return concat_series(
94
+ target, *features2, *([] if sample_weights is None else [sample_weights])
95
+ ).with_columns(
96
+ _compute_rolling_ols_expr(
97
+ target.name,
98
+ *(f.name for f in features2),
99
+ sample_weights=None if sample_weights is None else sample_weights.name,
100
+ add_intercept=add_intercept,
101
+ null_policy=null_policy,
102
+ window_size=window_size,
103
+ min_periods=min_periods,
104
+ use_woodbury=use_woodbury,
105
+ alpha=alpha,
106
+ )
107
+ )["ols"]
108
+ raise ImpossibleCaseError( # pragma: no cover
109
+ case=[f"{target=}", f"{features2=}", f"{sample_weights=}"]
110
+ )
111
+
112
+
113
+ def _compute_rolling_ols_expr(
114
+ target: ExprLike,
115
+ *features: ExprLike,
116
+ sample_weights: ExprLike | None = None,
117
+ add_intercept: bool = False,
118
+ null_policy: NullPolicy = "drop_window",
119
+ window_size: int = 1000000,
120
+ min_periods: int | None = None,
121
+ use_woodbury: bool | None = None,
122
+ alpha: float | None = None,
27
123
  ) -> Expr:
28
124
  """Compute a rolling OLS."""
29
125
  target = ensure_expr_or_series(target)
126
+ features2 = tuple(map(ensure_expr_or_series, features))
127
+ sample_weights = (
128
+ None if sample_weights is None else ensure_expr_or_series(sample_weights)
129
+ )
30
130
  rolling_kwargs = RollingKwargs(
31
131
  null_policy=null_policy,
32
132
  window_size=window_size,
@@ -36,7 +136,7 @@ def compute_rolling_ols(
36
136
  )
37
137
  coefficients = compute_rolling_least_squares(
38
138
  target,
39
- *features,
139
+ *features2,
40
140
  sample_weights=sample_weights,
41
141
  add_intercept=add_intercept,
42
142
  mode="coefficients",
@@ -44,7 +144,7 @@ def compute_rolling_ols(
44
144
  ).alias("coefficients")
45
145
  predictions = compute_rolling_least_squares(
46
146
  target,
47
- *features,
147
+ *features2,
48
148
  sample_weights=sample_weights,
49
149
  add_intercept=add_intercept,
50
150
  mode="predictions",
@@ -52,7 +152,7 @@ def compute_rolling_ols(
52
152
  ).alias("predictions")
53
153
  residuals = compute_rolling_least_squares(
54
154
  target,
55
- *features,
155
+ *features2,
56
156
  sample_weights=sample_weights,
57
157
  add_intercept=add_intercept,
58
158
  mode="residuals",