ml-data-access 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,84 @@
1
+ """Read side of the malatium data store.
2
+
3
+ import ml_data_access
4
+
5
+ db = ml_data_access.connect()
6
+ reference_df = ml_data_access.load_reference_returns(db, start, end)
7
+ chain_df = ml_data_access.load_option_greeks(db, "AAPL", start, end)
8
+
9
+ One `load_*` per table `ml-data-pipelines` writes, every one taking the
10
+ database and an inclusive `[start, end]` window and returning a collected
11
+ DataFrame. Nothing is screened, joined or derived: the store already holds
12
+ canonical tables, and the two screens a multi-year study needs are named
13
+ functions, `usable_symbol_years` and `in_universe`.
14
+ """
15
+
16
+ from ml_data_access.loaders import (
17
+ in_universe,
18
+ load_calendar,
19
+ load_corporate_actions,
20
+ load_earnings,
21
+ load_factor_covariances,
22
+ load_factor_loadings,
23
+ load_factor_returns,
24
+ load_forecast,
25
+ load_idio_vol,
26
+ load_index_greeks,
27
+ load_indices,
28
+ load_open_interest,
29
+ load_option_greeks,
30
+ load_rates,
31
+ load_realized_vol,
32
+ load_reference_returns,
33
+ load_sectors,
34
+ load_sessions,
35
+ load_signals,
36
+ load_stock_features,
37
+ load_surface,
38
+ load_symbology_check,
39
+ load_underlying,
40
+ load_universe,
41
+ load_yields,
42
+ usable_symbol_years,
43
+ )
44
+ from ml_data_access.store import (
45
+ available_symbols,
46
+ available_years,
47
+ connect,
48
+ describe,
49
+ scan,
50
+ )
51
+
52
+ __all__ = [
53
+ "available_symbols",
54
+ "available_years",
55
+ "connect",
56
+ "describe",
57
+ "in_universe",
58
+ "load_calendar",
59
+ "load_corporate_actions",
60
+ "load_earnings",
61
+ "load_factor_covariances",
62
+ "load_factor_loadings",
63
+ "load_factor_returns",
64
+ "load_forecast",
65
+ "load_idio_vol",
66
+ "load_index_greeks",
67
+ "load_indices",
68
+ "load_open_interest",
69
+ "load_option_greeks",
70
+ "load_rates",
71
+ "load_realized_vol",
72
+ "load_reference_returns",
73
+ "load_sectors",
74
+ "load_sessions",
75
+ "load_signals",
76
+ "load_stock_features",
77
+ "load_surface",
78
+ "load_symbology_check",
79
+ "load_underlying",
80
+ "load_universe",
81
+ "load_yields",
82
+ "scan",
83
+ "usable_symbol_years",
84
+ ]
@@ -0,0 +1,239 @@
1
+ """One loader per table. Every loader is `load_x(db, start=None, end=None)`.
2
+
3
+ The per-symbol chain loaders take the symbol first, since it selects the
4
+ partition rather than filtering rows. `db` is the connected bear-lake
5
+ database; it is passed for the same reason at-research passes it, so a
6
+ study can hold two stores open.
7
+ """
8
+
9
+ import datetime as dt
10
+
11
+ import bear_lake as bl
12
+ import polars as pl
13
+
14
+ from ml_data_access.store import in_window, scan, years_in
15
+
16
+
17
+ def load_window(
18
+ db: bl.Database, name: str, start: dt.date | None, end: dt.date | None, sort: list[str]
19
+ ) -> pl.DataFrame:
20
+ frame = scan(name, years=years_in(start, end, name))
21
+ return db.query(in_window(frame, start, end).sort(sort))
22
+
23
+
24
+ # --- raw ---------------------------------------------------------------------
25
+
26
+
27
+ def load_calendar(
28
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
29
+ ) -> pl.DataFrame:
30
+ """Exchange sessions, one `date` per row."""
31
+ return load_window(db, "calendar", start, end, ["date"])
32
+
33
+
34
+ def load_sessions(
35
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
36
+ ) -> list[dt.date]:
37
+ """The calendar as a list of dates, the shape `malatium.providers.TradingCalendar` takes."""
38
+ return load_calendar(db, start, end)["date"].to_list()
39
+
40
+
41
+ def load_universe(
42
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
43
+ ) -> pl.DataFrame:
44
+ """Point-in-time membership: `date`, `ticker` (Wikipedia), `symbol` (option root)."""
45
+ return load_window(db, "universe", start, end, ["date", "ticker"])
46
+
47
+
48
+ def load_sectors(db: bl.Database) -> pl.DataFrame:
49
+ """`ticker`, `symbol`, `sector`, `sub_industry`; a snapshot of today's constituents."""
50
+ return db.query(scan("sectors").sort("ticker"))
51
+
52
+
53
+ def load_indices(
54
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
55
+ ) -> pl.DataFrame:
56
+ """EOD levels for SPX, RUT, OEX, XSP and the VIX complex, 2024 on."""
57
+ return load_window(db, "indices", start, end, ["date", "symbol"])
58
+
59
+
60
+ def load_yields(
61
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
62
+ ) -> pl.DataFrame:
63
+ """CBOE treasury yield indices (`13w`, `5y`, `10y`, `30y`) as decimals."""
64
+ return load_window(db, "yields", start, end, ["date", "tenor"])
65
+
66
+
67
+ def load_rates(
68
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
69
+ ) -> pl.DataFrame:
70
+ """Overnight SOFR as a decimal, 2024 on."""
71
+ return load_window(db, "rates", start, end, ["date", "symbol"])
72
+
73
+
74
+ def load_corporate_actions(
75
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
76
+ ) -> pl.DataFrame:
77
+ """Splits (ex-date ratios) and dividends, one row per (symbol, date, action)."""
78
+ return load_window(db, "corporate_actions", start, end, ["symbol", "date", "action"])
79
+
80
+
81
+ def load_earnings(
82
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
83
+ ) -> pl.DataFrame:
84
+ """Announcement dates with a `bmo` / `amc` / `unknown` session."""
85
+ return load_window(db, "earnings", start, end, ["symbol", "date"])
86
+
87
+
88
+ def load_underlying(
89
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
90
+ ) -> pl.DataFrame:
91
+ """EOD stock OHLCV, 2023-06 on, `symbol` in option-root spelling."""
92
+ return load_window(db, "underlying", start, end, ["date", "symbol"])
93
+
94
+
95
+ def load_chain(
96
+ db: bl.Database, name: str, symbol: str, start: dt.date | None, end: dt.date | None
97
+ ) -> pl.DataFrame:
98
+ frame = scan(name, years=years_in(start, end, name), symbols=[symbol.upper()])
99
+ return db.query(in_window(frame, start, end).sort("date", "expiration", "strike", "right"))
100
+
101
+
102
+ def load_option_greeks(
103
+ db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
104
+ ) -> pl.DataFrame:
105
+ """One name's EOD chain: quotes, `iv` (null where the inversion failed), greeks with
106
+ `vega` per vol point, and `underlying`, the spot the greeks were struck against."""
107
+ return load_chain(db, "option_greeks", symbol, start, end)
108
+
109
+
110
+ def load_index_greeks(
111
+ db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
112
+ ) -> pl.DataFrame:
113
+ """The same for an index root (SPX, SPXW, XSP, VIX); repaired sessions carry null gamma."""
114
+ return load_chain(db, "index_greeks", symbol, start, end)
115
+
116
+
117
+ def load_open_interest(
118
+ db: bl.Database, symbol: str, start: dt.date | None = None, end: dt.date | None = None
119
+ ) -> pl.DataFrame:
120
+ """One name's EOD open interest; stamped pre-open, so it joins the same session's chain."""
121
+ return load_chain(db, "open_interest", symbol, start, end)
122
+
123
+
124
+ def load_symbology_check(db: bl.Database) -> pl.DataFrame:
125
+ """Per symbol-year: is the stored chain the company the universe names?"""
126
+ return db.query(scan("symbology_check").sort("year", "symbol"))
127
+
128
+
129
+ # --- derived -------------------------------------------------------------------
130
+
131
+
132
+ def load_reference_returns(
133
+ db: bl.Database,
134
+ start: dt.date | None = None,
135
+ end: dt.date | None = None,
136
+ symbols: list[str] | None = None,
137
+ ) -> pl.DataFrame:
138
+ """The reference straddle's per-vega P&L per (date, symbol); `SPX` is the market."""
139
+ frame = scan(
140
+ "reference_returns", years=years_in(start, end, "reference_returns"), symbols=symbols
141
+ )
142
+ return db.query(in_window(frame, start, end).sort("date", "symbol"))
143
+
144
+
145
+ def load_factor_returns(
146
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
147
+ ) -> pl.DataFrame:
148
+ """`(date, factor, ret)`: `market` and one GICS sector factor each."""
149
+ return load_window(db, "factor_returns", start, end, ["date", "factor"])
150
+
151
+
152
+ def load_factor_loadings(
153
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
154
+ ) -> pl.DataFrame:
155
+ """`(date, symbol, factor, loading)`, trailing 250 sessions."""
156
+ return load_window(db, "factor_loadings", start, end, ["date", "symbol", "factor"])
157
+
158
+
159
+ def load_factor_covariances(
160
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
161
+ ) -> pl.DataFrame:
162
+ """`(date, factor_1, factor_2, covariance)`, Ledoit-Wolf shrunk."""
163
+ return load_window(db, "factor_covariances", start, end, ["date", "factor_1", "factor_2"])
164
+
165
+
166
+ def load_idio_vol(
167
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
168
+ ) -> pl.DataFrame:
169
+ """`(date, symbol, idio_vol)`, daily residual std per dollar of vega."""
170
+ return load_window(db, "idio_vol", start, end, ["date", "symbol"])
171
+
172
+
173
+ def load_surface(
174
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
175
+ ) -> pl.DataFrame:
176
+ """Constant-maturity ATM IV per (date, symbol): `iv30`, `iv60`, `iv90`."""
177
+ return load_window(db, "surface", start, end, ["date", "symbol"])
178
+
179
+
180
+ def load_realized_vol(
181
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
182
+ ) -> pl.DataFrame:
183
+ """Close-to-close realized vol: trailing `rv_1`, `rv_5`, `rv_22` and the forward `rv_fwd`."""
184
+ return load_window(db, "realized_vol", start, end, ["date", "symbol"])
185
+
186
+
187
+ def load_forecast(
188
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
189
+ ) -> pl.DataFrame:
190
+ """The HAR forecast of the forward 60-session vol, `rv_fcst`."""
191
+ return load_window(db, "forecast", start, end, ["date", "symbol"])
192
+
193
+
194
+ def load_stock_features(
195
+ db: bl.Database, start: dt.date | None = None, end: dt.date | None = None
196
+ ) -> pl.DataFrame:
197
+ """`ret`, `beta`, `idio_vol`, `gap_freq` per (date, symbol)."""
198
+ return load_window(db, "stock_features", start, end, ["date", "symbol"])
199
+
200
+
201
+ def load_signals(
202
+ db: bl.Database,
203
+ signal: str,
204
+ start: dt.date | None = None,
205
+ end: dt.date | None = None,
206
+ ) -> pl.DataFrame:
207
+ """`(date, symbol, score)` for one named signal (`vrp`, `iv_zscore`, `momentum`)."""
208
+ frame = scan("signals", years=years_in(start, end, "signals")).filter(
209
+ pl.col("signal") == signal
210
+ )
211
+ return db.query(
212
+ in_window(frame, start, end).select("date", "symbol", "score").sort("date", "symbol")
213
+ )
214
+
215
+
216
+ # --- screens -------------------------------------------------------------------
217
+
218
+
219
+ def usable_symbol_years(db: bl.Database) -> pl.DataFrame:
220
+ """`(symbol, year)` pairs whose chain is not known to be another company's.
221
+
222
+ Only `wrong_instrument` is excluded. `thin_overlap` means the check could
223
+ not run, and it falls on names later delisted or acquired, so excluding
224
+ it would be a survivorship filter. Semi-join a panel against this.
225
+ """
226
+ return db.query(
227
+ scan("symbology_check")
228
+ .filter(pl.col("status") != "wrong_instrument")
229
+ .select("symbol", "year")
230
+ .unique()
231
+ .sort("symbol", "year")
232
+ )
233
+
234
+
235
+ def in_universe(db: bl.Database, panel_df: pl.DataFrame) -> pl.DataFrame:
236
+ """Keep the (date, symbol) rows that were index members that day."""
237
+ start, end = panel_df["date"].min(), panel_df["date"].max()
238
+ members_df = load_universe(db, start, end).select("date", "symbol").unique()
239
+ return panel_df.join(members_df, on=["date", "symbol"], how="semi")
@@ -0,0 +1,128 @@
1
+ """Connecting to the store and scanning only the partitions a query needs.
2
+
3
+ bear-lake lays a partitioned table out as `<store>/<table>/<year>/<symbol>.parquet`
4
+ with a `metadata.json` beside it. `bl.table()` globs the whole table, which
5
+ is right for the small tables and wrong for a per-symbol read of a 60 GB
6
+ chain table, so `scan` builds the path list from the partition keys in the
7
+ table's metadata and falls back to the glob otherwise.
8
+ """
9
+
10
+ import datetime as dt
11
+ import json
12
+ import os
13
+ from pathlib import Path
14
+
15
+ import bear_lake as bl
16
+ import polars as pl
17
+ from dotenv import load_dotenv
18
+
19
+ load_dotenv()
20
+
21
+
22
+ def connect(path: str | Path | None = None) -> bl.Database:
23
+ """Connect to the store at `path`, or at `ML_DATA_STORE`."""
24
+ path = path or os.getenv("ML_DATA_STORE")
25
+ if not path:
26
+ raise RuntimeError("set ML_DATA_STORE to the bear-lake directory, or pass a path")
27
+ return bl.connect(str(Path(path).expanduser()))
28
+
29
+
30
+ def store_path() -> Path:
31
+ if not bl.CONNECTED:
32
+ raise RuntimeError("not connected: call ml_data_access.connect() first")
33
+ return Path(bl.DATABASE_PATH)
34
+
35
+
36
+ def table_dir(name: str) -> Path:
37
+ return store_path() / name
38
+
39
+
40
+ def partition_keys(name: str) -> list[str]:
41
+ metadata_path = table_dir(name) / "metadata.json"
42
+ if not metadata_path.exists():
43
+ raise FileNotFoundError(
44
+ f"no table {name!r} in {store_path()}; run `uv run pipelines {name.replace('_', '-')}`"
45
+ " in ml-data-pipelines"
46
+ )
47
+ return json.loads(metadata_path.read_text()).get("partition_keys") or []
48
+
49
+
50
+ def available_years(name: str) -> list[int]:
51
+ directory = table_dir(name)
52
+ if not directory.is_dir():
53
+ return []
54
+ return sorted(int(child.stem) for child in directory.iterdir() if child.stem.isdigit())
55
+
56
+
57
+ def available_symbols(name: str, year: int) -> list[str]:
58
+ directory = table_dir(name) / str(year)
59
+ if not directory.is_dir():
60
+ return []
61
+ return sorted(path.stem for path in directory.glob("*.parquet"))
62
+
63
+
64
+ def scan(
65
+ name: str,
66
+ years: list[int] | None = None,
67
+ symbols: list[str] | None = None,
68
+ ) -> pl.LazyFrame:
69
+ """Lazy scan of `name`, opening only the partitions for `years` and `symbols`."""
70
+ keys = partition_keys(name)
71
+ if keys == ["year", "symbol"] and (years is not None or symbols is not None):
72
+ years = years if years is not None else available_years(name)
73
+ paths = []
74
+ for year in years:
75
+ names = symbols if symbols is not None else available_symbols(name, year)
76
+ paths.extend(
77
+ table_dir(name) / str(year) / f"{symbol}.parquet"
78
+ for symbol in names
79
+ if (table_dir(name) / str(year) / f"{symbol}.parquet").exists()
80
+ )
81
+ elif keys == ["year"] and years is not None:
82
+ paths = [
83
+ table_dir(name) / f"{year}.parquet"
84
+ for year in years
85
+ if (table_dir(name) / f"{year}.parquet").exists()
86
+ ]
87
+ else:
88
+ paths = sorted(table_dir(name).glob("**/*.parquet"))
89
+ if not paths:
90
+ raise FileNotFoundError(f"no {name} partitions for years={years} symbols={symbols}")
91
+ frame = pl.scan_parquet(paths)
92
+ if symbols is not None and "symbol" in frame.collect_schema().names():
93
+ frame = frame.filter(pl.col("symbol").is_in(symbols))
94
+ return frame
95
+
96
+
97
+ def years_in(start: dt.date | None, end: dt.date | None, name: str) -> list[int] | None:
98
+ """The years a window touches, clipped to what is on disk; None means every year."""
99
+ if start is None and end is None:
100
+ return None
101
+ on_disk = available_years(name)
102
+ first = start.year if start else (on_disk[0] if on_disk else None)
103
+ last = end.year if end else (on_disk[-1] if on_disk else None)
104
+ if first is None or last is None:
105
+ return None
106
+ return [year for year in on_disk if first <= year <= last]
107
+
108
+
109
+ def in_window(
110
+ frame: pl.LazyFrame, start: dt.date | None, end: dt.date | None, column: str = "date"
111
+ ) -> pl.LazyFrame:
112
+ if start is not None:
113
+ frame = frame.filter(pl.col(column) >= start)
114
+ if end is not None:
115
+ frame = frame.filter(pl.col(column) <= end)
116
+ return frame
117
+
118
+
119
+ def describe() -> str:
120
+ """One line per table: partitions and size."""
121
+ lines = []
122
+ for directory in sorted(store_path().iterdir()):
123
+ if not (directory / "metadata.json").exists():
124
+ continue
125
+ files = list(directory.glob("**/*.parquet"))
126
+ size = sum(file.stat().st_size for file in files)
127
+ lines.append(f"{directory.name:<20} {len(files):>5} files {size / 1e9:7.2f} GB")
128
+ return "\n".join(lines)
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.5
2
+ Name: ml-data-access
3
+ Version: 0.1.0
4
+ Summary: Read side of the malatium data store: one loader per bear-lake table.
5
+ Project-URL: Repository, https://github.com/Atium-Research/ml-data-access
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: bear-lake>=0.1.5
8
+ Requires-Dist: polars>=1.30
9
+ Requires-Dist: python-dotenv>=1.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # ml-data-access
13
+
14
+ Read side of the malatium data store. One `load_*` per table that [ml-data-pipelines](https://github.com/Atium-Research/ml-data-pipelines) writes, over a [bear-lake](https://github.com/andrewhall1124/bear-lake) database, in the shape of `at-research`'s data helpers.
15
+
16
+ ```bash
17
+ pip install ml-data-access
18
+ ```
19
+
20
+ ```python
21
+ import datetime as dt
22
+
23
+ import ml_data_access
24
+
25
+ db = ml_data_access.connect() # ML_DATA_STORE, or connect(path)
26
+ start, end = dt.date(2018, 7, 2), dt.date(2025, 6, 30)
27
+
28
+ reference_df = ml_data_access.load_reference_returns(db, start, end)
29
+ scores_df = ml_data_access.load_signals(db, "vrp", start, end)
30
+ loadings_df = ml_data_access.load_factor_loadings(db, start, end)
31
+ chain_df = ml_data_access.load_option_greeks(db, "AAPL", dt.date(2025, 1, 1), dt.date(2025, 3, 31))
32
+ ```
33
+
34
+ Every loader is `load_x(db, start=None, end=None)` and returns a collected DataFrame for the inclusive window; the three per-symbol chain loaders take the symbol first. Nothing is screened, joined or derived: the store holds canonical tables (`symbol` is the option root, `right` is `C`/`P`, `iv` is null where the vendor's inversion failed, `vega` is per vol point).
35
+
36
+ The frames slot straight into malatium:
37
+
38
+ ```python
39
+ from malatium.providers import PanelProvider, TradingCalendar
40
+
41
+ calendar = TradingCalendar(ml_data_access.load_sessions(db, start, end))
42
+ reference = PanelProvider(reference_df)
43
+ scores = PanelProvider(scores_df)
44
+ ```
45
+
46
+ ## Loaders
47
+
48
+ | loader | table |
49
+ | --- | --- |
50
+ | `load_calendar`, `load_sessions` | exchange sessions, as a frame or a list of dates |
51
+ | `load_universe` | point-in-time S&P 500 membership, `ticker` and `symbol` |
52
+ | `load_sectors` | GICS sector snapshot |
53
+ | `load_indices`, `load_yields`, `load_rates` | index levels (2024 on), the CBOE yield curve, SOFR |
54
+ | `load_corporate_actions`, `load_earnings` | splits and dividends; announcement dates with a session |
55
+ | `load_underlying` | EOD stock OHLCV, 2023-06 on |
56
+ | `load_option_greeks(db, symbol, ...)`, `load_index_greeks`, `load_open_interest` | one name's chain, index chain, open interest |
57
+ | `load_symbology_check`, `usable_symbol_years` | which symbol-years are the right company |
58
+ | `load_reference_returns` | the reference straddle's per-vega P&L, `SPX` included |
59
+ | `load_factor_returns`, `load_factor_loadings`, `load_factor_covariances`, `load_idio_vol` | the vol risk model |
60
+ | `load_surface`, `load_realized_vol`, `load_forecast`, `load_stock_features` | the derived panels |
61
+ | `load_signals(db, name, ...)` | one signal's `(date, symbol, score)` |
62
+ | `in_universe` | semi-join a `(date, symbol)` panel to membership |
63
+
64
+ `ml_data_access.describe()` lists what the store holds. `ml_data_access.scan(name, years, symbols)` is the lazy scan under every loader; it opens only the partitions asked for, using bear-lake's `<table>/<year>/<symbol>.parquet` layout.
65
+
66
+ ## Things to know before trusting a number
67
+
68
+ - **Eighteen symbol-years are another company's chain.** Semi-join against `usable_symbol_years(db)` for any multi-year study.
69
+ - **Open interest is one day stale by construction** and joins on the same `date`.
70
+ - **`iv` is null, not wrong**, on the ~3% of contract-days that failed to invert.
71
+ - **Repaired index sessions** (mostly 2020-21) carry null gamma and a 15:59 underlying.
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ uv sync
77
+ uv run pytest # builds a synthetic store in a temp dir
78
+ uv run ruff check . && uv run ruff format .
79
+ ```
@@ -0,0 +1,6 @@
1
+ ml_data_access/__init__.py,sha256=mE-N1q7pCV1M2u9s_wH3PagH-ThCZKhJIAu8lR9FPUk,2042
2
+ ml_data_access/loaders.py,sha256=KkOYxJ7I2kxCaz87-FAqa4JudWAsfBrV5pkqqub-LAk,9059
3
+ ml_data_access/store.py,sha256=tflQQcJ-1MTyEWSsNuEcyRa9pXtaMB4Y9ztUndttaXk,4617
4
+ ml_data_access-0.1.0.dist-info/METADATA,sha256=NaM2Z9iugDZJ23OMs37uTkuFaIFaiQv66JiB5nx2YJ0,3795
5
+ ml_data_access-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
6
+ ml_data_access-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any