lazy-visual 0.1.0__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.
@@ -0,0 +1,82 @@
1
+ """lazy-visual: one-line statistical plots with built-in stats."""
2
+ from __future__ import annotations
3
+
4
+ from ._errors import (
5
+ LvArgumentError,
6
+ LvDataError,
7
+ LvDataWarning,
8
+ LvDependencyError,
9
+ LvError,
10
+ LvExportError,
11
+ LvPerformanceWarning,
12
+ )
13
+ from ._options import Options, get_options, set_options
14
+
15
+ __version__ = "0.1.0"
16
+
17
+ __all__ = [
18
+ "LvArgumentError",
19
+ "LvDataError",
20
+ "LvDataWarning",
21
+ "LvDependencyError",
22
+ "LvError",
23
+ "LvExportError",
24
+ "LvPerformanceWarning",
25
+ "LvResult",
26
+ "Options",
27
+ "Theme",
28
+ "__version__",
29
+ "box",
30
+ "distribution",
31
+ "get_options",
32
+ "heatmap",
33
+ "load",
34
+ "raincloud",
35
+ "roc",
36
+ "scatterplot",
37
+ "set_options",
38
+ "theme",
39
+ "upset",
40
+ "violin",
41
+ ]
42
+
43
+
44
+ def __getattr__(name: str):
45
+ """Lazy imports so `import lazy_visual` stays fast (NFR-1)."""
46
+ if name == "load":
47
+ from .io.loader import load
48
+ return load
49
+ if name == "theme":
50
+ from .core.style import theme
51
+ return theme
52
+ if name == "Theme":
53
+ from .core.style import Theme
54
+ return Theme
55
+ if name == "LvResult":
56
+ from .core.result import LvResult
57
+ return LvResult
58
+ if name == "scatterplot":
59
+ from .plots.scatter import scatterplot
60
+ return scatterplot
61
+ if name == "distribution":
62
+ from .plots.distribution import distribution
63
+ return distribution
64
+ if name == "box":
65
+ from .plots.box import box
66
+ return box
67
+ if name == "violin":
68
+ from .plots.violin import violin
69
+ return violin
70
+ if name == "raincloud":
71
+ from .plots.raincloud import raincloud
72
+ return raincloud
73
+ if name == "heatmap":
74
+ from .plots.heatmap import heatmap
75
+ return heatmap
76
+ if name == "upset":
77
+ from .plots.upset import upset
78
+ return upset
79
+ if name == "roc":
80
+ from .plots.roc import roc
81
+ return roc
82
+ raise AttributeError(f"module 'lazy_visual' has no attribute {name!r}")
lazy_visual/_errors.py ADDED
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from collections.abc import Iterable
5
+
6
+
7
+ class LvError(Exception):
8
+ code: str = "LV-E000"
9
+
10
+ def __init__(self, message: str, *, code: str | None = None,
11
+ hint: str | None = None) -> None:
12
+ self.code = code or self.code
13
+ self.message = message
14
+ self.hint = hint
15
+ super().__init__(self._format())
16
+
17
+ def _format(self) -> str:
18
+ base = f"[{self.code}] {self.message}"
19
+ if self.hint:
20
+ base += f" Hint: {self.hint}"
21
+ return base
22
+
23
+ def __str__(self) -> str:
24
+ return self._format()
25
+
26
+
27
+ class LvDataError(LvError, ValueError):
28
+ code = "LV-E100"
29
+
30
+
31
+ class LvArgumentError(LvError, ValueError):
32
+ code = "LV-E200"
33
+
34
+
35
+ class LvExportError(LvError, OSError):
36
+ code = "LV-E300"
37
+
38
+
39
+ class LvDependencyError(LvError, ImportError):
40
+ code = "LV-E401"
41
+
42
+
43
+ class LvDataWarning(UserWarning):
44
+ pass
45
+
46
+
47
+ class LvPerformanceWarning(UserWarning):
48
+ pass
49
+
50
+
51
+ def _suggest(name: str, available: Iterable[str]) -> str | None:
52
+ m = difflib.get_close_matches(name, list(available), n=1, cutoff=0.6)
53
+ return m[0] if m else None
54
+
55
+
56
+ def err_column_not_found(name: str, available: list) -> LvDataError:
57
+ hint = "column names are case-sensitive"
58
+ s = _suggest(name, available)
59
+ if s:
60
+ hint = f"did you mean '{s}'? {hint}"
61
+ return LvDataError(
62
+ f"Column '{name}' not found. Available: {', '.join(available)}.",
63
+ code="LV-E101", hint=hint)
64
+
65
+
66
+ def err_wrong_dtype(name: str, expected: str, found: str) -> LvDataError:
67
+ return LvDataError(
68
+ f"Column '{name}' has invalid dtype '{found}', expected {expected}.",
69
+ code="LV-E102")
70
+
71
+
72
+ def err_empty_after_nan(n_before: int, n_dropped: int) -> LvDataError:
73
+ return LvDataError(
74
+ f"No rows left after dropping NaNs "
75
+ f"(had {n_before}, dropped {n_dropped}).", code="LV-E103")
76
+
77
+
78
+ def err_duplicate_columns(dupes: list) -> LvDataError:
79
+ return LvDataError(
80
+ f"Duplicate column names: {', '.join(dupes)}.",
81
+ code="LV-E104", hint="rename columns to be unique")
82
+
83
+
84
+ def err_y_true_not_binary(classes: list) -> LvDataError:
85
+ return LvDataError(
86
+ f"y_true must have exactly 2 classes, found {len(classes)}: "
87
+ f"{classes}.", code="LV-E106")
88
+
89
+
90
+ def err_upset_sets_count(n: int) -> LvDataError:
91
+ return LvDataError(f"upset requires 2-20 sets, got {n}.",
92
+ code="LV-E107")
93
+
94
+
95
+ def err_file_not_found(path: str) -> LvDataError:
96
+ return LvDataError(f"File not found or unreadable: {path}.",
97
+ code="LV-E108")
98
+
99
+
100
+ def err_unsupported_format(fmt: str) -> LvDataError:
101
+ return LvDataError(f"Unsupported file format: '{fmt}'.",
102
+ code="LV-E109")
103
+
104
+
105
+ def err_url_unreachable(url: str, reason: str) -> LvDataError:
106
+ return LvDataError(
107
+ f"URL unreachable or timed out: {url} ({reason}).",
108
+ code="LV-E110")
109
+
110
+
111
+ def err_out_of_range(name: str, value: object,
112
+ allowed: str) -> LvArgumentError:
113
+ return LvArgumentError(
114
+ f"Argument '{name}' = {value!r} is out of range. "
115
+ f"Allowed: {allowed}.", code="LV-E201")
116
+
117
+
118
+ def err_wrong_arg_type(name: str, expected: str,
119
+ got: str) -> LvArgumentError:
120
+ return LvArgumentError(
121
+ f"Argument '{name}' must be {expected}, got {got}.",
122
+ code="LV-E202")
123
+
124
+
125
+ def err_unknown_theme(name: str, available: list) -> LvArgumentError:
126
+ return LvArgumentError(
127
+ f"Unknown theme/palette '{name}'. "
128
+ f"Available: {', '.join(available)}.", code="LV-E203")
129
+
130
+
131
+ def err_incompatible_args(message: str) -> LvArgumentError:
132
+ return LvArgumentError(message, code="LV-E204")
133
+
134
+
135
+ def err_too_many_groups(n: int, limit: int) -> LvDataError:
136
+ return LvDataError(
137
+ f"Too many groups on axis ({n} > {limit}).",
138
+ code="LV-E205", hint="reduce the number of categories")
139
+
140
+
141
+ def err_export_format(fmt: str) -> LvExportError:
142
+ return LvExportError(
143
+ f"Unsupported export format '{fmt}'. Use png/svg/pdf.",
144
+ code="LV-E301")
145
+
146
+
147
+ def err_no_write_permission(path: str) -> LvExportError:
148
+ return LvExportError(f"Cannot write to {path}.", code="LV-E302")
149
+
150
+
151
+ def err_backend_unavailable(backend: str) -> LvArgumentError:
152
+ return LvArgumentError(
153
+ f"Backend '{backend}' is not available in this version.",
154
+ code="LV-E310", hint="only 'matplotlib' is supported in 0.1.0")
155
+
156
+
157
+ def err_missing_dependency(pkg: str, extra: str) -> LvDependencyError:
158
+ return LvDependencyError(
159
+ f"Optional dependency '{pkg}' is missing. "
160
+ f"Install with: pip install lazy-visual[{extra}]",
161
+ code="LV-E401")
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from . import defaults
6
+ from ._errors import err_out_of_range
7
+
8
+
9
+ @dataclass
10
+ class Options:
11
+ active_theme: str = "default"
12
+ random_state: int | None = defaults.RANDOM_STATE
13
+ max_points: int = defaults.MAX_POINTS
14
+ n_boot: int = defaults.N_BOOT
15
+
16
+ _OPTIONS = Options()
17
+
18
+ def get_options():
19
+ return _OPTIONS
20
+
21
+ def set_options(*, random_state=defaults.RANDOM_STATE,
22
+ max_points=defaults.MAX_POINTS,
23
+ n_boot=defaults.N_BOOT, active_theme=None):
24
+ if max_points < 1:
25
+ raise err_out_of_range("max_points", max_points, ">= 1")
26
+ if not (defaults.N_BOOT_MIN <= n_boot <= defaults.N_BOOT_MAX):
27
+ raise err_out_of_range("n_boot", n_boot,
28
+ f"{defaults.N_BOOT_MIN}-{defaults.N_BOOT_MAX}")
29
+ _OPTIONS.random_state = random_state
30
+ _OPTIONS.max_points = int(max_points)
31
+ _OPTIONS.n_boot = int(n_boot)
32
+ if active_theme is not None:
33
+ _OPTIONS.active_theme = active_theme
File without changes
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import pandas as pd
8
+
9
+ from .spec import PlotSpec
10
+
11
+
12
+ @dataclass
13
+ class LvResult:
14
+ fig: Any
15
+ ax: Any
16
+ spec: PlotSpec
17
+ stats: list[Any] = field(default_factory=list)
18
+ tables: dict[str, pd.DataFrame] = field(default_factory=dict)
19
+ files: list[Path] = field(default_factory=list)
20
+
21
+ def save(self, path: Any, *, fmt: Any = None,
22
+ dpi: Any = None) -> list[Path]:
23
+ from ..export import save_result
24
+ self.files = save_result(self.fig, path, fmt=fmt, dpi=dpi)
25
+ return self.files
26
+
27
+ def show(self) -> None:
28
+ import matplotlib.pyplot as plt
29
+ plt.show()
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ return {
33
+ "kind": self.spec.kind,
34
+ "stats": [
35
+ s.to_dict() if hasattr(s, "to_dict") else s.__dict__
36
+ for s in self.stats
37
+ ],
38
+ "files": [str(p) for p in self.files],
39
+ }
40
+
41
+ def __repr__(self) -> str:
42
+ head = f"<LvResult {self.spec.kind}"
43
+ if self.stats:
44
+ s = self.stats[0]
45
+ p = getattr(s, "p_value", None)
46
+ if p is not None:
47
+ ps = "p < 0.001" if p < 0.001 else f"p = {p:.3f}"
48
+ head += f", {getattr(s, 'method', '')} {ps}"
49
+ if self.files:
50
+ head += f", saved: {', '.join(p.name for p in self.files)}"
51
+ return head + ">"
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Layer:
9
+ geom: str
10
+ columns: dict[str, Any] = field(default_factory=dict)
11
+ style_overrides: dict[str, Any] = field(default_factory=dict)
12
+
13
+
14
+ @dataclass
15
+ class Annotation:
16
+ kind: str
17
+ text: str = ""
18
+ position: tuple[float, float] = (0.0, 0.0)
19
+ groups: tuple[str, ...] = ()
20
+ stat_ref: int | None = None
21
+
22
+
23
+ @dataclass
24
+ class AxesSpec:
25
+ xlabel: str | None = None
26
+ ylabel: str | None = None
27
+ title: str | None = None
28
+ xlim: tuple[float, float] | None = None
29
+ ylim: tuple[float, float] | None = None
30
+
31
+
32
+ @dataclass
33
+ class PlotSpec:
34
+ kind: str
35
+ panels: list[str] = field(default_factory=list)
36
+ layers: list[Layer] = field(default_factory=list)
37
+ annotations: list[Annotation] = field(default_factory=list)
38
+ axes: AxesSpec = field(default_factory=AxesSpec)
39
+ legend: bool = True
40
+ style: dict[str, Any] = field(default_factory=dict)
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass, replace
5
+ from importlib import resources
6
+ from typing import Any
7
+
8
+ import matplotlib as mpl
9
+
10
+ from .. import defaults
11
+ from .._errors import (
12
+ err_out_of_range,
13
+ err_unknown_theme,
14
+ err_wrong_arg_type,
15
+ )
16
+
17
+ PALETTES: dict[str, list[Any]] = {
18
+ "okabe": ["#000000", "#E69F00", "#56B4E9", "#009E73",
19
+ "#F0E442", "#0072B2", "#D55E00", "#CC79A7"],
20
+ "tab10": [mpl.colormaps["tab10"](i / 10)
21
+ for i in range(10)],
22
+ "viridis": [mpl.colormaps["viridis"](x)
23
+ for x in (0.0, 0.15, 0.3, 0.45, 0.6, 0.75, 0.9)],
24
+ "gray": ["#111111", "#444444", "#777777", "#999999", "#bbbbbb"],
25
+ }
26
+ KNOWN_THEMES: tuple[str, ...] = ("default", "nature", "apa", "dark")
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class Theme:
31
+ name: str = "default"
32
+ font_family: tuple[str, ...] = ("DejaVu Sans",)
33
+ font_size: float = 10.0
34
+ palette: str = "tab10"
35
+ linewidth: float = 1.0
36
+ alpha: float = 0.85
37
+ grid: bool = True
38
+ spines: tuple[str, ...] = ("left", "bottom", "top", "right")
39
+ background: str = "white"
40
+ figsize: tuple[float, float] = (6.0, 4.0)
41
+ dpi: int = defaults.DPI_DEFAULT
42
+
43
+ def palette_colors(self) -> list[Any]:
44
+ p = self.palette
45
+ if isinstance(p, str) and p in PALETTES:
46
+ return list(PALETTES[p])
47
+ if isinstance(p, str):
48
+ try:
49
+ cmap = mpl.colormaps[p]
50
+ except KeyError:
51
+ return list(PALETTES["tab10"])
52
+ return [cmap(x) for x in (0.0, 0.2, 0.4, 0.6, 0.8)]
53
+ return list(PALETTES["tab10"])
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class StyleArgs:
58
+ color: str | None = None
59
+ palette: Any | None = None
60
+ alpha: float | None = None
61
+ linewidth: float | None = None
62
+ size: float | None = None
63
+ title: str | None = None
64
+ xlabel: str | None = None
65
+ ylabel: str | None = None
66
+ show_legend: bool = True
67
+ show_grid: bool | None = None
68
+ annotate: bool = True
69
+ figsize: tuple[float, float] | None = None
70
+ dpi: int | None = None
71
+ save: Any | None = None
72
+ fmt: Any | None = None
73
+ theme: Any | None = None
74
+ ax: Any | None = None
75
+ backend: str = "matplotlib"
76
+
77
+ def validate(self) -> None:
78
+ if self.alpha is not None and not (0.0 <= self.alpha <= 1.0):
79
+ raise err_out_of_range("alpha", self.alpha, "0-1")
80
+ if self.linewidth is not None and not (0.0 < self.linewidth <= 10.0):
81
+ raise err_out_of_range(
82
+ "linewidth", self.linewidth, "0 < lw <= 10")
83
+ if self.size is not None and not (self.size > 0):
84
+ raise err_out_of_range("size", self.size, "> 0")
85
+ if self.figsize is not None:
86
+ if len(self.figsize) != 2:
87
+ raise err_wrong_arg_type(
88
+ "figsize", "tuple of 2 floats", str(self.figsize))
89
+ for v in self.figsize:
90
+ if not (1 <= v <= 30):
91
+ raise err_out_of_range(
92
+ "figsize", self.figsize, "1-30 per side")
93
+ if self.dpi is not None and not (
94
+ defaults.DPI_MIN <= self.dpi <= defaults.DPI_MAX
95
+ ):
96
+ raise err_out_of_range(
97
+ "dpi", self.dpi,
98
+ f"{defaults.DPI_MIN}-{defaults.DPI_MAX}")
99
+ if self.backend != "matplotlib":
100
+ from .._errors import err_backend_unavailable
101
+ raise err_backend_unavailable(self.backend)
102
+
103
+
104
+ def _theme_from_dict(d: dict[str, Any]) -> Theme:
105
+ known = set(Theme.__dataclass_fields__)
106
+ kwargs: dict[str, Any] = {k: v for k, v in d.items() if k in known}
107
+ if "font_family" in kwargs:
108
+ kwargs["font_family"] = tuple(kwargs["font_family"])
109
+ if "spines" in kwargs:
110
+ kwargs["spines"] = tuple(kwargs["spines"])
111
+ if "figsize" in kwargs:
112
+ kwargs["figsize"] = tuple(kwargs["figsize"])
113
+ return Theme(**kwargs)
114
+
115
+
116
+ def _load_builtin(name: str) -> Theme:
117
+ if name not in KNOWN_THEMES:
118
+ raise err_unknown_theme(name, list(KNOWN_THEMES))
119
+ try:
120
+ data = resources.files("lazy_visual.themes").joinpath(
121
+ f"{name}.json").read_text("utf-8")
122
+ return _theme_from_dict(json.loads(data))
123
+ except (FileNotFoundError, ModuleNotFoundError):
124
+ return Theme(name=name)
125
+
126
+
127
+ _BUILTIN_CACHE: dict[str, Theme] = {}
128
+
129
+
130
+ def get_theme(name: str) -> Theme:
131
+ if name not in _BUILTIN_CACHE:
132
+ _BUILTIN_CACHE[name] = _load_builtin(name)
133
+ return _BUILTIN_CACHE[name]
134
+
135
+
136
+ def theme(name: str = "default", **overrides: Any) -> Theme:
137
+ base = get_theme(name)
138
+ if overrides:
139
+ base = replace(base, **{
140
+ k: v for k, v in overrides.items()
141
+ if k in Theme.__dataclass_fields__
142
+ })
143
+ return base
144
+
145
+
146
+ def resolve(style: StyleArgs) -> tuple[Theme, StyleArgs]:
147
+ from .._options import get_options
148
+ if style.theme is None:
149
+ t = get_theme(get_options().active_theme)
150
+ elif isinstance(style.theme, str):
151
+ t = get_theme(style.theme)
152
+ elif isinstance(style.theme, dict):
153
+ base_name = style.theme.get("name", get_options().active_theme)
154
+ base = get_theme(base_name)
155
+ merged = {**base.__dict__, **style.theme}
156
+ t = _theme_from_dict(merged)
157
+ elif isinstance(style.theme, Theme):
158
+ t = style.theme
159
+ else:
160
+ raise err_wrong_arg_type(
161
+ "theme", "str, dict or Theme",
162
+ type(style.theme).__name__)
163
+ return t, style
164
+
165
+
166
+ def apply_mpl_theme(t: Theme) -> None:
167
+ available = {f.name for f in mpl.font_manager.fontManager.ttflist}
168
+ fams = [f for f in t.font_family if f in available] or ["DejaVu Sans"]
169
+ mpl.rcParams["font.family"] = fams
170
+ mpl.rcParams["font.size"] = t.font_size
171
+ mpl.rcParams["axes.grid"] = t.grid
172
+ mpl.rcParams["axes.facecolor"] = t.background
173
+ mpl.rcParams["figure.facecolor"] = t.background
174
+ for side in ("left", "bottom", "top", "right"):
175
+ mpl.rcParams[f"axes.spines.{side}"] = side in t.spines
176
+ mpl.rcParams["svg.fonttype"] = "none"
177
+ mpl.rcParams["pdf.fonttype"] = 42
@@ -0,0 +1,14 @@
1
+ RANDOM_STATE = 0
2
+ MAX_POINTS = 5000
3
+ N_BOOT = 1000
4
+ MAX_GROUPS = 30
5
+ UPSET_TOP_N = 30
6
+ MAX_SETS = 20
7
+ MAX_MODELS = 8
8
+ HEATMAP_ANNOT_LIMIT = 30
9
+ URL_TIMEOUT_S = 30
10
+ PERF_WARN_ROWS = 1_000_000
11
+ DPI_DEFAULT = 150
12
+ DPI_SAVE = 300
13
+ DPI_MIN, DPI_MAX = 72, 1200
14
+ N_BOOT_MIN, N_BOOT_MAX = 100, 10_000
lazy_visual/export.py ADDED
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from ._errors import err_export_format, err_no_write_permission
8
+
9
+ VALID_FMTS = {"png", "svg", "pdf"}
10
+
11
+ def _resolve_targets(path, fmt):
12
+ items = list(path) if isinstance(path, (list, tuple)) else [path]
13
+ out = []
14
+ for p in items:
15
+ p = Path(p)
16
+ f = (fmt or p.suffix.lstrip(".") or "png").lower()
17
+ if f not in VALID_FMTS:
18
+ raise err_export_format(f)
19
+ if p.suffix.lower() != f".{f}":
20
+ p = p.with_suffix(f".{f}")
21
+ out.append((p, f))
22
+ return out
23
+
24
+ def _atomic_save(fig, target, fmt, dpi):
25
+ target.parent.mkdir(parents=True, exist_ok=True)
26
+ fd, tmp = tempfile.mkstemp(suffix=f".{fmt}", dir=str(target.parent))
27
+ os.close(fd)
28
+ try:
29
+ extra = [a.get_legend() for a in fig.axes if a.get_legend()]
30
+ if extra:
31
+ fig.savefig(tmp, format=fmt, dpi=dpi,
32
+ bbox_inches="tight",
33
+ bbox_extra_artists=extra)
34
+ else:
35
+ fig.savefig(tmp, format=fmt, dpi=dpi, bbox_inches="tight")
36
+ os.replace(tmp, target)
37
+ except PermissionError as e:
38
+ try: os.unlink(tmp)
39
+ except OSError: pass
40
+ raise err_no_write_permission(str(target)) from e
41
+ except Exception:
42
+ try: os.unlink(tmp)
43
+ except OSError: pass
44
+ raise
45
+
46
+ def save_result(fig, path, *, fmt=None, dpi=None):
47
+ targets = _resolve_targets(path, fmt)
48
+ files = []
49
+ for target, f in targets:
50
+ _atomic_save(fig, target, f, dpi)
51
+ files.append(target)
52
+ return files
File without changes
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import io as _io
4
+ import urllib.request
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+
9
+ from .. import defaults
10
+ from .._errors import (
11
+ LvPerformanceWarning,
12
+ err_file_not_found,
13
+ err_missing_dependency,
14
+ err_unsupported_format,
15
+ err_url_unreachable,
16
+ )
17
+
18
+
19
+ def _ext(path):
20
+ return Path(path).suffix.lower().lstrip(".")
21
+
22
+ def _read_bytes(payload, fmt, read_kwargs):
23
+ buf = _io.BytesIO(payload)
24
+ if fmt == "csv":
25
+ return pd.read_csv(buf, **read_kwargs)
26
+ if fmt in ("tsv", "tab"):
27
+ return pd.read_csv(buf, sep="\t", **read_kwargs)
28
+ if fmt in ("parquet", "pq"):
29
+ try:
30
+ return pd.read_parquet(buf, **read_kwargs)
31
+ except ImportError as e:
32
+ raise err_missing_dependency("pyarrow", "parquet") from e
33
+ if fmt in ("xlsx", "xls"):
34
+ try:
35
+ return pd.read_excel(buf, **read_kwargs)
36
+ except ImportError as e:
37
+ raise err_missing_dependency("openpyxl", "excel") from e
38
+ if fmt == "json":
39
+ return pd.read_json(buf, **read_kwargs)
40
+ raise err_unsupported_format(fmt)
41
+
42
+ def load(source, *, fmt=None, **read_kwargs):
43
+ if isinstance(source, pd.DataFrame):
44
+ return source
45
+ s = str(source)
46
+ if s.startswith(("http://", "https://")):
47
+ if fmt is None:
48
+ fmt = _ext(s.split("?")[0]) or "csv"
49
+ try:
50
+ with urllib.request.urlopen(s, timeout=defaults.URL_TIMEOUT_S) as r:
51
+ payload = r.read()
52
+ except Exception as e:
53
+ raise err_url_unreachable(s, str(e)) from e
54
+ return _read_bytes(payload, fmt, read_kwargs)
55
+ path = Path(s)
56
+ if not path.exists() or not path.is_file():
57
+ raise err_file_not_found(s)
58
+ size_mb = path.stat().st_size / (1024 * 1024)
59
+ if size_mb > 500:
60
+ import warnings
61
+ warnings.warn(f"[LV-W503] File is {size_mb:.0f} MB.",
62
+ LvPerformanceWarning, stacklevel=2)
63
+ fmt = fmt or _ext(s)
64
+ if fmt == "csv":
65
+ return pd.read_csv(path, **read_kwargs)
66
+ if fmt in ("tsv", "tab"):
67
+ return pd.read_csv(path, sep="\t", **read_kwargs)
68
+ if fmt in ("parquet", "pq"):
69
+ try:
70
+ return pd.read_parquet(path, **read_kwargs)
71
+ except ImportError as e:
72
+ raise err_missing_dependency("pyarrow", "parquet") from e
73
+ if fmt in ("xlsx", "xls"):
74
+ try:
75
+ return pd.read_excel(path, **read_kwargs)
76
+ except ImportError as e:
77
+ raise err_missing_dependency("openpyxl", "excel") from e
78
+ if fmt == "json":
79
+ return pd.read_json(path, **read_kwargs)
80
+ raise err_unsupported_format(fmt)