openseries 2.1.4__tar.gz → 2.1.5__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.
- {openseries-2.1.4 → openseries-2.1.5}/PKG-INFO +1 -1
- {openseries-2.1.4 → openseries-2.1.5}/openseries/__init__.py +2 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/_common_model.py +106 -108
- openseries-2.1.5/openseries/html_utils.py +314 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/plotly_layouts.json +11 -7
- {openseries-2.1.4 → openseries-2.1.5}/openseries/portfoliotools.py +1 -1
- {openseries-2.1.4 → openseries-2.1.5}/openseries/report.py +32 -33
- {openseries-2.1.4 → openseries-2.1.5}/openseries/series.py +1 -1
- {openseries-2.1.4 → openseries-2.1.5}/pyproject.toml +2 -2
- {openseries-2.1.4 → openseries-2.1.5}/LICENSE.md +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/README.md +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/_risk.py +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/datefixer.py +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/frame.py +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/load_plotly.py +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/owntypes.py +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/plotly_captor_logo.json +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/py.typed +0 -0
- {openseries-2.1.4 → openseries-2.1.5}/openseries/simulation.py +0 -0
|
@@ -11,6 +11,7 @@ from .datefixer import (
|
|
|
11
11
|
offset_business_days,
|
|
12
12
|
)
|
|
13
13
|
from .frame import OpenFrame
|
|
14
|
+
from .html_utils import export_plotly_figure
|
|
14
15
|
from .load_plotly import load_plotly_dict
|
|
15
16
|
from .owntypes import ValueType
|
|
16
17
|
from .portfoliotools import (
|
|
@@ -33,6 +34,7 @@ __all__ = [
|
|
|
33
34
|
"date_fix",
|
|
34
35
|
"date_offset_foll",
|
|
35
36
|
"efficient_frontier",
|
|
37
|
+
"export_plotly_figure",
|
|
36
38
|
"generate_calendar_date_range",
|
|
37
39
|
"get_previous_business_day_before_today",
|
|
38
40
|
"holiday_calendar",
|
|
@@ -21,7 +21,6 @@ from .owntypes import (
|
|
|
21
21
|
DateAlignmentError,
|
|
22
22
|
InitialValueZeroError,
|
|
23
23
|
NumberOfItemsAndLabelsNotSameError,
|
|
24
|
-
PlotlyConfigType,
|
|
25
24
|
ResampleDataLossError,
|
|
26
25
|
SeriesOrFloat_co,
|
|
27
26
|
ValueType,
|
|
@@ -61,9 +60,7 @@ from pandas import (
|
|
|
61
60
|
from pandas.tseries.offsets import CustomBusinessDay
|
|
62
61
|
from plotly.figure_factory import create_distplot # type: ignore[import-untyped]
|
|
63
62
|
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
64
|
-
from
|
|
65
|
-
from plotly.offline import plot # type: ignore[import-untyped]
|
|
66
|
-
from pydantic import BaseModel, ConfigDict, DirectoryPath, ValidationError
|
|
63
|
+
from pydantic import BaseModel, ConfigDict, DirectoryPath
|
|
67
64
|
from scipy.stats import (
|
|
68
65
|
kurtosis,
|
|
69
66
|
norm,
|
|
@@ -79,6 +76,7 @@ from .datefixer import (
|
|
|
79
76
|
date_offset_foll,
|
|
80
77
|
holiday_calendar,
|
|
81
78
|
)
|
|
79
|
+
from .html_utils import export_plotly_figure
|
|
82
80
|
from .load_plotly import load_plotly_dict
|
|
83
81
|
|
|
84
82
|
|
|
@@ -506,17 +504,19 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
506
504
|
"""
|
|
507
505
|
mdddf = self.tsdf.copy()
|
|
508
506
|
mdddf.index = DatetimeIndex(mdddf.index)
|
|
509
|
-
|
|
507
|
+
idxmin_result = cast(
|
|
508
|
+
"Series[Timestamp]",
|
|
509
|
+
(mdddf / mdddf.expanding(min_periods=1).max()).idxmin(),
|
|
510
|
+
)
|
|
511
|
+
result = idxmin_result.dt.date
|
|
510
512
|
|
|
511
513
|
if self.tsdf.shape[1] == 1:
|
|
512
|
-
return
|
|
513
|
-
|
|
514
|
+
return result.iloc[0]
|
|
515
|
+
return Series(
|
|
514
516
|
data=result,
|
|
515
517
|
index=self.tsdf.columns,
|
|
516
518
|
name="Max drawdown date",
|
|
517
|
-
|
|
518
|
-
).dt.date # type: ignore[attr-defined]
|
|
519
|
-
return cast("Series[dt.date]", date_series)
|
|
519
|
+
)
|
|
520
520
|
|
|
521
521
|
@property
|
|
522
522
|
def worst(self: Self) -> SeriesOrFloat_co:
|
|
@@ -547,12 +547,12 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
547
547
|
"""
|
|
548
548
|
method: LiteralPandasReindexMethod = "nearest"
|
|
549
549
|
|
|
550
|
-
|
|
550
|
+
if hasattr(self, "constituents"):
|
|
551
|
+
countries = self.constituents[0].countries
|
|
552
|
+
markets = self.constituents[0].markets
|
|
553
|
+
else:
|
|
551
554
|
countries = self.countries
|
|
552
555
|
markets = self.markets
|
|
553
|
-
except AttributeError:
|
|
554
|
-
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
555
|
-
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
556
556
|
|
|
557
557
|
wmdf = self.tsdf.copy()
|
|
558
558
|
|
|
@@ -733,6 +733,54 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
733
733
|
|
|
734
734
|
return earlier, later
|
|
735
735
|
|
|
736
|
+
def _get_or_set_countries(
|
|
737
|
+
self: Self, countries: CountriesType | None
|
|
738
|
+
) -> CountriesType | None:
|
|
739
|
+
"""Get or set countries attribute, handling both OpenTimeSeries and OpenFrame.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
countries: Country code(s) to set, or None to get existing value.
|
|
743
|
+
|
|
744
|
+
Returns:
|
|
745
|
+
The countries value after getting or setting.
|
|
746
|
+
"""
|
|
747
|
+
if countries:
|
|
748
|
+
if hasattr(self, "countries"):
|
|
749
|
+
self.countries = countries
|
|
750
|
+
else:
|
|
751
|
+
for serie in self.constituents: # type: ignore[attr-defined]
|
|
752
|
+
serie.countries = countries
|
|
753
|
+
elif hasattr(self, "countries"):
|
|
754
|
+
countries = self.countries
|
|
755
|
+
else:
|
|
756
|
+
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
757
|
+
|
|
758
|
+
return countries
|
|
759
|
+
|
|
760
|
+
def _get_or_set_markets(
|
|
761
|
+
self: Self, markets: list[str] | str | None
|
|
762
|
+
) -> list[str] | str | None:
|
|
763
|
+
"""Get or set markets attribute, handling both OpenTimeSeries and OpenFrame.
|
|
764
|
+
|
|
765
|
+
Args:
|
|
766
|
+
markets: Market code(s) to set, or None to get existing value.
|
|
767
|
+
|
|
768
|
+
Returns:
|
|
769
|
+
The markets value after getting or setting.
|
|
770
|
+
"""
|
|
771
|
+
if markets:
|
|
772
|
+
if hasattr(self, "markets"):
|
|
773
|
+
self.markets = markets
|
|
774
|
+
else:
|
|
775
|
+
for serie in self.constituents: # type: ignore[attr-defined]
|
|
776
|
+
serie.markets = markets
|
|
777
|
+
elif hasattr(self, "markets"):
|
|
778
|
+
markets = self.markets
|
|
779
|
+
else:
|
|
780
|
+
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
781
|
+
|
|
782
|
+
return markets
|
|
783
|
+
|
|
736
784
|
def align_index_to_local_cdays(
|
|
737
785
|
self: Self,
|
|
738
786
|
countries: CountriesType | None = None,
|
|
@@ -754,29 +802,8 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
754
802
|
startyear = cast("int", to_datetime(self.tsdf.index[0]).year)
|
|
755
803
|
endyear = cast("int", to_datetime(self.tsdf.index[-1]).year)
|
|
756
804
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
self.countries = countries
|
|
760
|
-
except ValidationError:
|
|
761
|
-
for serie in self.constituents: # type: ignore[attr-defined]
|
|
762
|
-
serie.countries = countries
|
|
763
|
-
else:
|
|
764
|
-
try:
|
|
765
|
-
countries = self.countries
|
|
766
|
-
except AttributeError:
|
|
767
|
-
countries = self.constituents[0].countries # type: ignore[attr-defined]
|
|
768
|
-
|
|
769
|
-
if markets:
|
|
770
|
-
try:
|
|
771
|
-
self.markets = markets
|
|
772
|
-
except ValidationError:
|
|
773
|
-
for serie in self.constituents: # type: ignore[attr-defined]
|
|
774
|
-
serie.markets = markets
|
|
775
|
-
else:
|
|
776
|
-
try:
|
|
777
|
-
markets = self.markets
|
|
778
|
-
except AttributeError:
|
|
779
|
-
markets = self.constituents[0].markets # type: ignore[attr-defined]
|
|
805
|
+
countries = self._get_or_set_countries(countries)
|
|
806
|
+
markets = self._get_or_set_markets(markets)
|
|
780
807
|
|
|
781
808
|
calendar = holiday_calendar(
|
|
782
809
|
startyear=startyear,
|
|
@@ -1046,76 +1073,44 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1046
1073
|
def _apply_title_logo(
|
|
1047
1074
|
figure: Figure,
|
|
1048
1075
|
logo: CaptorLogoType,
|
|
1049
|
-
title: str | None,
|
|
1050
1076
|
*,
|
|
1051
1077
|
add_logo: bool,
|
|
1052
|
-
) -> None:
|
|
1053
|
-
"""Apply optional
|
|
1078
|
+
) -> str | None:
|
|
1079
|
+
"""Apply optional logo to a Plotly Figure.
|
|
1054
1080
|
|
|
1055
1081
|
Args:
|
|
1056
1082
|
figure: Plotly figure to update.
|
|
1057
1083
|
logo: Plotly layout image dict.
|
|
1058
|
-
title: Optional plot title.
|
|
1059
1084
|
add_logo: Whether to add the logo to the figure.
|
|
1085
|
+
|
|
1086
|
+
Returns:
|
|
1087
|
+
Logo source URL if logo should be displayed, None otherwise.
|
|
1060
1088
|
"""
|
|
1089
|
+
logo_url: str | None = None
|
|
1061
1090
|
if add_logo:
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
figure.
|
|
1065
|
-
{
|
|
1091
|
+
source = logo.get("source", "")
|
|
1092
|
+
logo_url = str(source) if source else None
|
|
1093
|
+
figure.add_layout_image(
|
|
1094
|
+
{
|
|
1095
|
+
"source": "",
|
|
1096
|
+
"x": 0,
|
|
1097
|
+
"y": 1,
|
|
1098
|
+
"xanchor": "left",
|
|
1099
|
+
"yanchor": "top",
|
|
1100
|
+
"xref": "paper",
|
|
1101
|
+
"yref": "paper",
|
|
1102
|
+
"sizex": 0,
|
|
1103
|
+
"sizey": 0,
|
|
1104
|
+
"opacity": 0,
|
|
1105
|
+
}
|
|
1066
1106
|
)
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
output_type: LiteralPlotlyOutput,
|
|
1073
|
-
plotfile: Path,
|
|
1074
|
-
filename: str,
|
|
1075
|
-
*,
|
|
1076
|
-
include_plotlyjs_bool: LiteralPlotlyJSlib,
|
|
1077
|
-
auto_open: bool,
|
|
1078
|
-
) -> str:
|
|
1079
|
-
"""Write a file or return inline HTML string from a Plotly Figure.
|
|
1080
|
-
|
|
1081
|
-
Args:
|
|
1082
|
-
figure: Plotly figure to render.
|
|
1083
|
-
fig_config: Plotly config dict.
|
|
1084
|
-
output_type: Output type: ``"file"`` or ``"div"``.
|
|
1085
|
-
plotfile: Full path to the output html file.
|
|
1086
|
-
filename: Output filename used for the ``div_id`` when inline.
|
|
1087
|
-
include_plotlyjs_bool: How plotly.js is included.
|
|
1088
|
-
auto_open: Whether to auto-open the file in a browser.
|
|
1089
|
-
|
|
1090
|
-
Returns:
|
|
1091
|
-
If ``output_type`` is ``"file"``, the path to the file; otherwise an
|
|
1092
|
-
inline HTML string (div).
|
|
1093
|
-
"""
|
|
1094
|
-
if output_type == "file":
|
|
1095
|
-
plot(
|
|
1096
|
-
figure_or_data=figure,
|
|
1097
|
-
filename=str(plotfile),
|
|
1098
|
-
auto_open=auto_open,
|
|
1099
|
-
auto_play=False,
|
|
1100
|
-
link_text="",
|
|
1101
|
-
include_plotlyjs=include_plotlyjs_bool,
|
|
1102
|
-
config=fig_config,
|
|
1103
|
-
output_type=output_type,
|
|
1104
|
-
)
|
|
1105
|
-
return str(plotfile)
|
|
1106
|
-
|
|
1107
|
-
div_id = filename.rsplit(".", 1)[0]
|
|
1108
|
-
return cast(
|
|
1109
|
-
"str",
|
|
1110
|
-
to_html(
|
|
1111
|
-
fig=figure,
|
|
1112
|
-
config=fig_config,
|
|
1113
|
-
auto_play=False,
|
|
1114
|
-
include_plotlyjs=include_plotlyjs_bool,
|
|
1115
|
-
full_html=False,
|
|
1116
|
-
div_id=div_id,
|
|
1117
|
-
),
|
|
1107
|
+
figure.update_layout(
|
|
1108
|
+
{
|
|
1109
|
+
"margin": {"t": 20, "b": 60, "l": 60, "r": 60, "pad": 4},
|
|
1110
|
+
"autosize": True,
|
|
1111
|
+
},
|
|
1118
1112
|
)
|
|
1113
|
+
return logo_url
|
|
1119
1114
|
|
|
1120
1115
|
def plot_bars(
|
|
1121
1116
|
self: Self,
|
|
@@ -1176,21 +1171,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1176
1171
|
)
|
|
1177
1172
|
figure.update_layout(barmode=mode, yaxis={"tickformat": tick_fmt})
|
|
1178
1173
|
|
|
1179
|
-
self._apply_title_logo(
|
|
1174
|
+
logo_url = self._apply_title_logo(
|
|
1180
1175
|
figure=figure,
|
|
1181
|
-
title=title,
|
|
1182
1176
|
add_logo=add_logo,
|
|
1183
1177
|
logo=logo,
|
|
1184
1178
|
)
|
|
1185
1179
|
|
|
1186
|
-
string_output =
|
|
1180
|
+
string_output = export_plotly_figure(
|
|
1187
1181
|
figure=figure,
|
|
1188
1182
|
fig_config=fig["config"],
|
|
1189
|
-
|
|
1183
|
+
include_plotlyjs=include_plotlyjs,
|
|
1190
1184
|
output_type=output_type,
|
|
1191
1185
|
auto_open=auto_open,
|
|
1192
1186
|
plotfile=plotfile,
|
|
1193
1187
|
filename=filename,
|
|
1188
|
+
title=title,
|
|
1189
|
+
logo_url=logo_url,
|
|
1194
1190
|
)
|
|
1195
1191
|
|
|
1196
1192
|
return figure, string_output
|
|
@@ -1271,21 +1267,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1271
1267
|
textposition="top center",
|
|
1272
1268
|
)
|
|
1273
1269
|
|
|
1274
|
-
self._apply_title_logo(
|
|
1270
|
+
logo_url = self._apply_title_logo(
|
|
1275
1271
|
figure=figure,
|
|
1276
|
-
title=title,
|
|
1277
1272
|
add_logo=add_logo,
|
|
1278
1273
|
logo=logo,
|
|
1279
1274
|
)
|
|
1280
1275
|
|
|
1281
|
-
string_output =
|
|
1276
|
+
string_output = export_plotly_figure(
|
|
1282
1277
|
figure=figure,
|
|
1283
1278
|
fig_config=fig["config"],
|
|
1284
|
-
|
|
1279
|
+
include_plotlyjs=include_plotlyjs,
|
|
1285
1280
|
output_type=output_type,
|
|
1286
1281
|
auto_open=auto_open,
|
|
1287
1282
|
plotfile=plotfile,
|
|
1288
1283
|
filename=filename,
|
|
1284
|
+
title=title,
|
|
1285
|
+
logo_url=logo_url,
|
|
1289
1286
|
)
|
|
1290
1287
|
|
|
1291
1288
|
return figure, string_output
|
|
@@ -1393,21 +1390,22 @@ class _CommonModel(BaseModel, Generic[SeriesOrFloat_co]):
|
|
|
1393
1390
|
figure.update_xaxes(zeroline=True, zerolinewidth=2, zerolinecolor="lightgrey")
|
|
1394
1391
|
figure.update_yaxes(zeroline=True, zerolinewidth=2, zerolinecolor="lightgrey")
|
|
1395
1392
|
|
|
1396
|
-
self._apply_title_logo(
|
|
1393
|
+
logo_url = self._apply_title_logo(
|
|
1397
1394
|
figure=figure,
|
|
1398
|
-
title=title,
|
|
1399
1395
|
add_logo=add_logo,
|
|
1400
1396
|
logo=logo,
|
|
1401
1397
|
)
|
|
1402
1398
|
|
|
1403
|
-
string_output =
|
|
1399
|
+
string_output = export_plotly_figure(
|
|
1404
1400
|
figure=figure,
|
|
1405
1401
|
fig_config=fig_dict["config"],
|
|
1406
|
-
|
|
1402
|
+
include_plotlyjs=include_plotlyjs,
|
|
1407
1403
|
output_type=output_type,
|
|
1408
1404
|
auto_open=auto_open,
|
|
1409
1405
|
plotfile=plotfile,
|
|
1410
1406
|
filename=filename,
|
|
1407
|
+
title=title,
|
|
1408
|
+
logo_url=logo_url,
|
|
1411
1409
|
)
|
|
1412
1410
|
|
|
1413
1411
|
return figure, string_output
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Shared HTML utilities for responsive Plotly outputs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, cast
|
|
7
|
+
from webbrowser import open as webbrowser_open
|
|
8
|
+
|
|
9
|
+
from plotly.io import to_html # type: ignore[import-untyped]
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
12
|
+
from plotly.graph_objs import Figure # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
from .owntypes import LiteralPlotlyJSlib, LiteralPlotlyOutput, PlotlyConfigType
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"_generate_responsive_plot_html",
|
|
18
|
+
"_get_base_css",
|
|
19
|
+
"_get_plot_css",
|
|
20
|
+
"_get_plotly_script",
|
|
21
|
+
"export_plotly_figure",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_base_css() -> str:
|
|
26
|
+
"""Get base CSS styles for responsive HTML reports."""
|
|
27
|
+
return """
|
|
28
|
+
:root{--ink:#1f2a44;--muted:#6b778c;--header:#4a4a4a;--header2:#6a6a6a;--cell:#f3f3f3;--cell2:#e6e6e6;--paper:#ffffff;}
|
|
29
|
+
html,body{margin:0;padding:0;background:var(--paper);color:var(--ink);
|
|
30
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;}
|
|
31
|
+
.page{max-width:calc(100% - 64px);margin:0 auto;padding:32px;
|
|
32
|
+
padding-bottom:48px;}
|
|
33
|
+
.header{display:grid;grid-template-columns:140px 1fr 140px;gap:12px;
|
|
34
|
+
align-items:start;}
|
|
35
|
+
h1{margin:0;text-align:center;font-size:45px;font-weight:800;}
|
|
36
|
+
.layout{display:grid;grid-template-columns:1.2fr .9fr;
|
|
37
|
+
grid-template-areas:"charts table";gap:22px;align-items:start;margin-top:12px;}
|
|
38
|
+
.charts{grid-area:charts;display:grid;grid-template-rows:auto auto;gap:18px;}
|
|
39
|
+
.table{grid-area:table;}
|
|
40
|
+
.plot{width:100%;height:380px;}
|
|
41
|
+
.plot.bar{height:300px;}
|
|
42
|
+
@media (max-width:980px){
|
|
43
|
+
.page{padding:24px;padding-bottom:24px;}
|
|
44
|
+
.header{grid-template-columns:120px 1fr;}
|
|
45
|
+
h1{font-size:36px;}
|
|
46
|
+
.layout{grid-template-columns:1fr;grid-template-areas:"table" "charts";gap:16px;}
|
|
47
|
+
.plot{height:380px;}
|
|
48
|
+
.plot.bar{height:300px;}
|
|
49
|
+
.table{overflow-x:auto;}
|
|
50
|
+
table.metrics{table-layout:auto;font-size:14px;}
|
|
51
|
+
}
|
|
52
|
+
table.metrics{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;
|
|
53
|
+
border-radius:4px;overflow:hidden;table-layout:fixed;}
|
|
54
|
+
table.metrics thead th{background:var(--header);color:white;padding:8px 10px;
|
|
55
|
+
font-weight:700;text-align:center;word-wrap:break-word;word-break:break-word;}
|
|
56
|
+
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
57
|
+
width:180px;}
|
|
58
|
+
table.metrics tbody td{padding:7px 10px;border-bottom:1px solid white;
|
|
59
|
+
border-right:1px solid white;text-align:center;background:var(--paper);}
|
|
60
|
+
table.metrics tbody td:first-child{text-align:left;font-weight:600;color:white;
|
|
61
|
+
background:var(--header);width:180px;}
|
|
62
|
+
table.metrics tbody td:last-child{background:var(--cell2);}
|
|
63
|
+
.legend-container{margin-top:24px;padding-top:20px;padding-bottom:16px;
|
|
64
|
+
display:flex;justify-content:center;flex-wrap:wrap;gap:24px;flex-shrink:0;}
|
|
65
|
+
.legend-item{display:flex;align-items:center;gap:8px;font-size:14px;}
|
|
66
|
+
.legend-color{width:20px;height:3px;border-radius:2px;}
|
|
67
|
+
@media (min-width:981px){
|
|
68
|
+
html,body{overflow-y:auto;}
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_plot_css() -> str:
|
|
74
|
+
"""Get CSS styles for full-screen responsive plots.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
CSS string for responsive plots.
|
|
78
|
+
"""
|
|
79
|
+
return """
|
|
80
|
+
*{box-sizing:border-box;}
|
|
81
|
+
html,body{margin:0;padding:0;width:100%;height:100%;
|
|
82
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;
|
|
83
|
+
overflow-x:hidden;}
|
|
84
|
+
.page{width:100%;height:100vh;display:flex;flex-direction:column;}
|
|
85
|
+
.title-container{width:100%;flex-shrink:0;padding:15px 20px 10px 20px;
|
|
86
|
+
display:flex;align-items:center;justify-content:center;background:white;z-index:10;
|
|
87
|
+
gap:15px;}
|
|
88
|
+
.title-logo{height:60px;width:auto;flex-shrink:0;}
|
|
89
|
+
.title-container h1{margin:0;padding:0;font-size:36px;font-weight:bold;
|
|
90
|
+
color:#253551;word-wrap:break-word;word-break:break-word;
|
|
91
|
+
white-space:normal;line-height:1.2;flex:1;text-align:center;}
|
|
92
|
+
.plot-container{width:100%;flex:1;position:relative;overflow:hidden;
|
|
93
|
+
min-height:0;}
|
|
94
|
+
.plot{width:100%;height:100%;display:block;position:absolute;top:0;left:0;}
|
|
95
|
+
.plot > div{width:100% !important;height:100% !important;}
|
|
96
|
+
.plot .js-plotly-plot{width:100% !important;height:100% !important;}
|
|
97
|
+
.plot .js-plotly-plot .plotly{width:100% !important;height:100% !important;}
|
|
98
|
+
@media (max-width: 980px) {
|
|
99
|
+
.title-container{padding:12px 15px 8px 15px;gap:12px;}
|
|
100
|
+
.title-container h1{font-size:24px;line-height:1.3;}
|
|
101
|
+
.title-logo{height:32px;}
|
|
102
|
+
}
|
|
103
|
+
@media (max-width: 480px) {
|
|
104
|
+
.title-container{padding:10px 12px 6px 12px;gap:10px;}
|
|
105
|
+
.title-container h1{font-size:18px;line-height:1.2;}
|
|
106
|
+
.title-logo{height:24px;}
|
|
107
|
+
}
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_plotly_script(include_plotlyjs: LiteralPlotlyJSlib) -> str:
|
|
112
|
+
"""Get plotly script tag."""
|
|
113
|
+
if include_plotlyjs == "cdn":
|
|
114
|
+
return '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _generate_responsive_plot_html(
|
|
119
|
+
title: str | None,
|
|
120
|
+
plot_div: str,
|
|
121
|
+
include_plotlyjs: LiteralPlotlyJSlib,
|
|
122
|
+
plot_id: str,
|
|
123
|
+
logo_url: str | None = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""Generate responsive HTML for a single Plotly plot."""
|
|
126
|
+
css = _get_plot_css()
|
|
127
|
+
plotly_script = _get_plotly_script(include_plotlyjs)
|
|
128
|
+
|
|
129
|
+
plot_div = plot_div.replace(
|
|
130
|
+
f'<div id="{plot_id}"', f'<div id="{plot_id}" class="plot"'
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
title_html = ""
|
|
134
|
+
if (title is not None and title) or logo_url:
|
|
135
|
+
logo_html = ""
|
|
136
|
+
if logo_url:
|
|
137
|
+
logo_html = f'<img src="{logo_url}" alt="Logo" class="title-logo" />'
|
|
138
|
+
title_text = f"<h1><b>{title}</b></h1>" if title else ""
|
|
139
|
+
title_html = f'<div class="title-container">{logo_html}{title_text}</div>'
|
|
140
|
+
|
|
141
|
+
return f"""<!doctype html>
|
|
142
|
+
<html lang="en">
|
|
143
|
+
<head>
|
|
144
|
+
<meta charset="utf-8" />
|
|
145
|
+
<meta name="viewport" content="width=device-width,initial-scale=1,
|
|
146
|
+
maximum-scale=5,user-scalable=yes" />
|
|
147
|
+
<title>{title or ""}</title>
|
|
148
|
+
<style>{css}</style>
|
|
149
|
+
{plotly_script}
|
|
150
|
+
</head>
|
|
151
|
+
<body>
|
|
152
|
+
<div class="page">
|
|
153
|
+
{title_html}
|
|
154
|
+
<div class="plot-container">
|
|
155
|
+
{plot_div}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<script>
|
|
159
|
+
(function() {{
|
|
160
|
+
var plotDiv = document.getElementById("{plot_id}");
|
|
161
|
+
var container = plotDiv ? plotDiv.closest('.plot-container') : null;
|
|
162
|
+
|
|
163
|
+
function getContainerSize() {{
|
|
164
|
+
if (!container) return {{width: window.innerWidth, height: window.innerHeight}};
|
|
165
|
+
var rect = container.getBoundingClientRect();
|
|
166
|
+
return {{width: rect.width, height: rect.height}};
|
|
167
|
+
}}
|
|
168
|
+
|
|
169
|
+
function resizePlot() {{
|
|
170
|
+
if (typeof Plotly === 'undefined' || !Plotly.Plots || !plotDiv) return;
|
|
171
|
+
|
|
172
|
+
var size = getContainerSize();
|
|
173
|
+
if (size.width > 0 && size.height > 0) {{
|
|
174
|
+
Plotly.Plots.resize(plotDiv);
|
|
175
|
+
}}
|
|
176
|
+
}}
|
|
177
|
+
|
|
178
|
+
function debounceResize() {{
|
|
179
|
+
clearTimeout(window._resizeTimeout);
|
|
180
|
+
window._resizeTimeout = setTimeout(resizePlot, 150);
|
|
181
|
+
}}
|
|
182
|
+
|
|
183
|
+
window.addEventListener("resize", debounceResize);
|
|
184
|
+
|
|
185
|
+
window.addEventListener("orientationchange", function() {{
|
|
186
|
+
setTimeout(function() {{
|
|
187
|
+
resizePlot();
|
|
188
|
+
}}, 300);
|
|
189
|
+
}});
|
|
190
|
+
|
|
191
|
+
function initResize() {{
|
|
192
|
+
if (typeof Plotly !== 'undefined' && plotDiv) {{
|
|
193
|
+
setTimeout(resizePlot, 100);
|
|
194
|
+
setTimeout(resizePlot, 500);
|
|
195
|
+
setTimeout(resizePlot, 1000);
|
|
196
|
+
}} else {{
|
|
197
|
+
setTimeout(initResize, 100);
|
|
198
|
+
}}
|
|
199
|
+
}}
|
|
200
|
+
|
|
201
|
+
if (document.readyState === 'loading') {{
|
|
202
|
+
document.addEventListener('DOMContentLoaded', initResize);
|
|
203
|
+
}} else {{
|
|
204
|
+
initResize();
|
|
205
|
+
}}
|
|
206
|
+
|
|
207
|
+
window.addEventListener('load', function() {{
|
|
208
|
+
setTimeout(resizePlot, 200);
|
|
209
|
+
}});
|
|
210
|
+
}})();
|
|
211
|
+
</script>
|
|
212
|
+
</body>
|
|
213
|
+
</html>
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def export_plotly_figure(
|
|
218
|
+
figure: Figure,
|
|
219
|
+
fig_config: PlotlyConfigType,
|
|
220
|
+
output_type: LiteralPlotlyOutput,
|
|
221
|
+
filename: str,
|
|
222
|
+
*,
|
|
223
|
+
include_plotlyjs: LiteralPlotlyJSlib,
|
|
224
|
+
auto_open: bool = False,
|
|
225
|
+
plotfile: Path | str,
|
|
226
|
+
title: str | None = None,
|
|
227
|
+
logo_url: str | None = None,
|
|
228
|
+
) -> str:
|
|
229
|
+
"""Export a Plotly figure to a mobile-responsive HTML file or inline div.
|
|
230
|
+
|
|
231
|
+
This function converts a Plotly figure to HTML output, with support for
|
|
232
|
+
mobile-responsive standalone HTML files or inline HTML divs. When exporting
|
|
233
|
+
to a file, the output includes proper viewport settings, responsive CSS,
|
|
234
|
+
and JavaScript for handling window resizing and orientation changes.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
figure: Plotly figure to render.
|
|
238
|
+
fig_config: Plotly config dictionary.
|
|
239
|
+
output_type: Output type: ``"file"`` or ``"div"``.
|
|
240
|
+
filename: Output filename used for the ``div_id`` when inline, or to
|
|
241
|
+
derive the div_id when exporting to file.
|
|
242
|
+
include_plotlyjs: How plotly.js is included. Can be ``True`` (inline),
|
|
243
|
+
``False`` (not included), or ``"cdn"`` (CDN link).
|
|
244
|
+
auto_open: Whether to auto-open the file in a browser (only used when
|
|
245
|
+
``output_type="file"``). Defaults to ``False``.
|
|
246
|
+
plotfile: Full path to the output HTML file. Required when
|
|
247
|
+
``output_type="file"``, ignored when ``output_type="div"``.
|
|
248
|
+
title: Title for the HTML page (used for file output). Defaults to
|
|
249
|
+
``None``.
|
|
250
|
+
logo_url: Optional logo URL to display in the title container.
|
|
251
|
+
Defaults to ``None``.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
If ``output_type`` is ``"file"``, the path to the file as a string;
|
|
255
|
+
otherwise an inline HTML string (div).
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
Export a Plotly figure to a responsive HTML file:
|
|
260
|
+
|
|
261
|
+
>>> import plotly.graph_objects as go
|
|
262
|
+
>>> from openseries.html_utils import export_plotly_figure
|
|
263
|
+
>>> from pathlib import Path
|
|
264
|
+
>>>
|
|
265
|
+
>>> fig = go.Figure(data=go.Scatter(x=[1, 2, 3], y=[4, 5, 6]))
|
|
266
|
+
>>> output_path = export_plotly_figure(
|
|
267
|
+
... figure=fig,
|
|
268
|
+
... fig_config={},
|
|
269
|
+
... output_type="file",
|
|
270
|
+
... filename="my_plot.html",
|
|
271
|
+
... include_plotlyjs="cdn",
|
|
272
|
+
... plotfile=Path("output/my_plot.html"),
|
|
273
|
+
... title="My Plot",
|
|
274
|
+
... auto_open=True,
|
|
275
|
+
... )
|
|
276
|
+
"""
|
|
277
|
+
if output_type == "file":
|
|
278
|
+
plotfile_path = Path(plotfile) if isinstance(plotfile, str) else plotfile
|
|
279
|
+
div_id = filename.rsplit(".", 1)[0]
|
|
280
|
+
plot_div = cast(
|
|
281
|
+
"str",
|
|
282
|
+
to_html(
|
|
283
|
+
fig=figure,
|
|
284
|
+
config=fig_config,
|
|
285
|
+
auto_play=False,
|
|
286
|
+
include_plotlyjs=False,
|
|
287
|
+
full_html=False,
|
|
288
|
+
div_id=div_id,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
html_content = _generate_responsive_plot_html(
|
|
292
|
+
title=title,
|
|
293
|
+
plot_div=plot_div,
|
|
294
|
+
include_plotlyjs=include_plotlyjs,
|
|
295
|
+
plot_id=div_id,
|
|
296
|
+
logo_url=logo_url,
|
|
297
|
+
)
|
|
298
|
+
plotfile_path.write_text(html_content, encoding="utf-8")
|
|
299
|
+
if auto_open:
|
|
300
|
+
webbrowser_open(plotfile_path.as_uri())
|
|
301
|
+
return str(plotfile_path)
|
|
302
|
+
|
|
303
|
+
div_id = filename.rsplit(".", 1)[0]
|
|
304
|
+
return cast(
|
|
305
|
+
"str",
|
|
306
|
+
to_html(
|
|
307
|
+
fig=figure,
|
|
308
|
+
config=fig_config,
|
|
309
|
+
auto_play=False,
|
|
310
|
+
include_plotlyjs=include_plotlyjs,
|
|
311
|
+
full_html=False,
|
|
312
|
+
div_id=div_id,
|
|
313
|
+
),
|
|
314
|
+
)
|
|
@@ -41,22 +41,26 @@
|
|
|
41
41
|
"legend": {
|
|
42
42
|
"bgcolor": "rgba(0,0,0,0)",
|
|
43
43
|
"orientation": "h",
|
|
44
|
-
"x": 0.
|
|
45
|
-
"xanchor": "
|
|
46
|
-
"
|
|
47
|
-
"
|
|
44
|
+
"x": 0.5,
|
|
45
|
+
"xanchor": "center",
|
|
46
|
+
"xref": "container",
|
|
47
|
+
"y": 1.05,
|
|
48
|
+
"yanchor": "bottom",
|
|
49
|
+
"yref": "container"
|
|
48
50
|
},
|
|
49
51
|
"paper_bgcolor": "rgba(255,255,255,1)",
|
|
50
52
|
"plot_bgcolor": "rgba(255,255,255,1)",
|
|
51
53
|
"showlegend": true,
|
|
52
54
|
"title": {
|
|
53
55
|
"font": {
|
|
54
|
-
"size":
|
|
56
|
+
"size": 36
|
|
55
57
|
},
|
|
56
58
|
"x": 0.5,
|
|
57
59
|
"xanchor": "center",
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
+
"xref": "container",
|
|
61
|
+
"y": 0.93,
|
|
62
|
+
"yanchor": "bottom",
|
|
63
|
+
"yref": "container"
|
|
60
64
|
},
|
|
61
65
|
"xaxis": {
|
|
62
66
|
"gridcolor": "#EEEEEE",
|
|
@@ -738,7 +738,7 @@ def _configure_figure_layout(
|
|
|
738
738
|
if title:
|
|
739
739
|
if titletext is None:
|
|
740
740
|
titletext = "<b>Risk and Return</b><br>"
|
|
741
|
-
figure.update_layout(title={"text": titletext, "font": {"size":
|
|
741
|
+
figure.update_layout(title={"text": titletext, "font": {"size": 36}})
|
|
742
742
|
|
|
743
743
|
if add_logo:
|
|
744
744
|
figure.add_layout_image(logo)
|
|
@@ -17,6 +17,7 @@ from pandas import DataFrame, Index, Series, Timestamp, concat, isna
|
|
|
17
17
|
from plotly.graph_objs import Bar, Figure, Scatter # type: ignore[import-untyped]
|
|
18
18
|
from plotly.utils import PlotlyJSONEncoder # type: ignore[import-untyped]
|
|
19
19
|
|
|
20
|
+
from .html_utils import _get_base_css, _get_plotly_script
|
|
20
21
|
from .load_plotly import load_plotly_dict
|
|
21
22
|
from .owntypes import (
|
|
22
23
|
CaptorLogoType,
|
|
@@ -408,12 +409,10 @@ def _get_legend_html(line_traces: list[Scatter], colorway: list[str]) -> str:
|
|
|
408
409
|
|
|
409
410
|
def _get_css() -> str:
|
|
410
411
|
"""Get CSS styles for the HTML report."""
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.page{max-width:calc(100% - 64px);margin:0 auto;padding:32px;
|
|
416
|
-
padding-bottom:48px;}
|
|
412
|
+
base_css = _get_base_css()
|
|
413
|
+
return (
|
|
414
|
+
base_css
|
|
415
|
+
+ """
|
|
417
416
|
.header{display:grid;grid-template-columns:140px 1fr 140px;gap:12px;
|
|
418
417
|
align-items:start;}
|
|
419
418
|
h1{margin:0;text-align:center;font-size:45px;font-weight:800;}
|
|
@@ -430,16 +429,22 @@ def _get_css() -> str:
|
|
|
430
429
|
.layout{grid-template-columns:1fr;grid-template-areas:"table" "charts";gap:16px;}
|
|
431
430
|
.plot{height:380px;}
|
|
432
431
|
.plot.bar{height:300px;}
|
|
432
|
+
table.metrics{table-layout:fixed;width:auto;}
|
|
433
|
+
table.metrics thead th{min-width:120px;width:120px;white-space:nowrap;}
|
|
434
|
+
table.metrics thead th:first-child{width:180px;}
|
|
435
|
+
table.metrics tbody td{min-width:120px;width:120px;}
|
|
436
|
+
table.metrics tbody td:first-child{width:180px;}
|
|
433
437
|
}
|
|
434
438
|
table.metrics{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;
|
|
435
|
-
border-radius:4px;overflow:hidden;}
|
|
439
|
+
border-radius:4px;overflow:hidden;table-layout:fixed;}
|
|
436
440
|
table.metrics thead th{background:var(--header);color:white;padding:8px 10px;
|
|
437
|
-
font-weight:700;text-align:center;
|
|
438
|
-
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
441
|
+
font-weight:700;text-align:center;word-wrap:break-word;word-break:break-word;}
|
|
442
|
+
table.metrics thead th:first-child{background:var(--header2);text-align:left;
|
|
443
|
+
width:180px;}
|
|
439
444
|
table.metrics tbody td{padding:7px 10px;border-bottom:1px solid white;
|
|
440
445
|
border-right:1px solid white;text-align:center;background:var(--paper);}
|
|
441
446
|
table.metrics tbody td:first-child{text-align:left;font-weight:600;color:white;
|
|
442
|
-
background:var(--header);width:
|
|
447
|
+
background:var(--header);width:180px;}
|
|
443
448
|
table.metrics tbody td:last-child{background:var(--cell2);}
|
|
444
449
|
.legend-container{margin-top:24px;padding-top:20px;padding-bottom:16px;
|
|
445
450
|
display:flex;justify-content:center;flex-wrap:wrap;gap:24px;flex-shrink:0;}
|
|
@@ -449,13 +454,7 @@ def _get_css() -> str:
|
|
|
449
454
|
html,body{overflow-y:auto;}
|
|
450
455
|
}
|
|
451
456
|
"""
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
def _get_plotly_script(include_plotlyjs: LiteralPlotlyJSlib) -> str:
|
|
455
|
-
"""Get plotly script tag."""
|
|
456
|
-
if include_plotlyjs == "cdn":
|
|
457
|
-
return '<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>'
|
|
458
|
-
return ""
|
|
457
|
+
)
|
|
459
458
|
|
|
460
459
|
|
|
461
460
|
def _write_html_file(
|
|
@@ -611,7 +610,7 @@ def report_html(
|
|
|
611
610
|
rpt_df.columns = colmns
|
|
612
611
|
table_html = _metrics_table_html(rpt_df)
|
|
613
612
|
|
|
614
|
-
dirpath = _get_output_directory(directory)
|
|
613
|
+
dirpath = _get_output_directory(directory=directory)
|
|
615
614
|
|
|
616
615
|
if not filename:
|
|
617
616
|
filename = "".join(choice(ascii_letters) for _ in range(6)) + ".html"
|
|
@@ -625,16 +624,16 @@ def report_html(
|
|
|
625
624
|
)
|
|
626
625
|
|
|
627
626
|
line_layout, bar_layout = _get_plotly_layouts(
|
|
628
|
-
layout_theme,
|
|
629
|
-
colorway,
|
|
630
|
-
copied.item_count,
|
|
627
|
+
layout_theme=layout_theme,
|
|
628
|
+
colorway=colorway,
|
|
629
|
+
item_count=copied.item_count,
|
|
631
630
|
)
|
|
632
631
|
|
|
633
632
|
config = cast("dict[str, Any]", fig_theme.get("config", {})) or {}
|
|
634
633
|
config = {**config, "responsive": True, "displayModeBar": False}
|
|
635
634
|
|
|
636
|
-
plotly_script = _get_plotly_script(include_plotlyjs)
|
|
637
|
-
logo_html = _get_logo_html(logo, add_logo=add_logo)
|
|
635
|
+
plotly_script = _get_plotly_script(include_plotlyjs=include_plotlyjs)
|
|
636
|
+
logo_html = _get_logo_html(logo=logo, add_logo=add_logo)
|
|
638
637
|
css = _get_css()
|
|
639
638
|
|
|
640
639
|
line_payload = {
|
|
@@ -648,21 +647,21 @@ def report_html(
|
|
|
648
647
|
"config": config,
|
|
649
648
|
}
|
|
650
649
|
|
|
651
|
-
legend_html = _get_legend_html(line_traces, colorway)
|
|
650
|
+
legend_html = _get_legend_html(line_traces=line_traces, colorway=colorway)
|
|
652
651
|
|
|
653
652
|
html = _generate_html(
|
|
654
|
-
title,
|
|
655
|
-
css,
|
|
656
|
-
plotly_script,
|
|
657
|
-
logo_html,
|
|
658
|
-
table_html,
|
|
659
|
-
line_payload,
|
|
660
|
-
bar_payload,
|
|
661
|
-
legend_html,
|
|
653
|
+
title=title,
|
|
654
|
+
css=css,
|
|
655
|
+
plotly_script=plotly_script,
|
|
656
|
+
logo_html=logo_html,
|
|
657
|
+
table_html=table_html,
|
|
658
|
+
line_payload=line_payload,
|
|
659
|
+
bar_payload=bar_payload,
|
|
660
|
+
legend_html=legend_html,
|
|
662
661
|
)
|
|
663
662
|
|
|
664
663
|
if output_type == "file":
|
|
665
|
-
output = _write_html_file(plotfile, html, auto_open=auto_open)
|
|
664
|
+
output = _write_html_file(plotfile=plotfile, html=html, auto_open=auto_open)
|
|
666
665
|
else:
|
|
667
666
|
output = html
|
|
668
667
|
|
|
@@ -625,7 +625,7 @@ class OpenTimeSeries(_CommonModel[float]):
|
|
|
625
625
|
cast("Timestamp", earlier) : cast("Timestamp", later)
|
|
626
626
|
].copy()
|
|
627
627
|
|
|
628
|
-
data.loc[:, (self.label, ValueType.RTRN)] = log(
|
|
628
|
+
data.loc[:, (self.label, ValueType.RTRN)] = log(
|
|
629
629
|
data.loc[:, self.tsdf.columns.to_numpy()[0]],
|
|
630
630
|
).diff()
|
|
631
631
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "openseries"
|
|
3
|
-
version = "2.1.
|
|
3
|
+
version = "2.1.5"
|
|
4
4
|
description = "Tools for analyzing financial timeseries."
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "Martin Karrin", email = "martin.karrin@captor.se" },
|
|
@@ -67,7 +67,7 @@ pre-commit = ">=4.5.1"
|
|
|
67
67
|
pytest = ">=9.0.2"
|
|
68
68
|
pytest-cov = ">=7.0.0"
|
|
69
69
|
pytest-xdist = ">=3.8.0"
|
|
70
|
-
ruff = "0.14.
|
|
70
|
+
ruff = "0.14.10"
|
|
71
71
|
types-openpyxl = ">=3.1.2"
|
|
72
72
|
scipy-stubs = ">=1.14.1.0"
|
|
73
73
|
types-python-dateutil = ">=2.8.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|