dataconvert updates
This commit is contained in:
4
soleprint/station/tools/dataconvert/.gitignore
vendored
Normal file
4
soleprint/station/tools/dataconvert/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Local layouts: how one producer's spreadsheets are shaped is not a fact about
|
||||||
|
# the tool, and the marker values name whoever that is. Copy
|
||||||
|
# dataconvert-example.json to dataconvert.json and edit that; it stays here.
|
||||||
|
dataconvert.json
|
||||||
@@ -7,14 +7,46 @@ plus a `SCHEMA.md` describing every table.
|
|||||||
uv run dataconvert.py --input data/ --out-dir seed/ # full seeds
|
uv run dataconvert.py --input data/ --out-dir seed/ # full seeds
|
||||||
uv run dataconvert.py --input data/ "*.xlsx" --out-dir seed/ # dirs, files, globs, .zip
|
uv run dataconvert.py --input data/ "*.xlsx" --out-dir seed/ # dirs, files, globs, .zip
|
||||||
uv run dataconvert.py --input data/ --out-dir sample/ --max-rows 20 # to understand the data
|
uv run dataconvert.py --input data/ --out-dir sample/ --max-rows 20 # to understand the data
|
||||||
|
uv run dataconvert.py --input export.xlsx --header-row 2 --data-row 6
|
||||||
|
uv run dataconvert.py --input data/ --config their-exports.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Reads `.csv`, `.xlsx`, `.xls` and `.ods`: single files, directories (recursively),
|
Reads `.csv`, `.xlsx`, `.xls` and `.ods`: single files, directories (recursively),
|
||||||
ZIP archives and wildcard patterns. Sheets of a multi-sheet workbook are written as
|
ZIP archives and wildcard patterns. Sheets of a multi-sheet workbook are written as
|
||||||
`<workbook>_<sheet>.sql` unless the sheet is already `drm_` or `study_`. Several
|
`<workbook>_<sheet>.sql` (see `bare_sheet_prefixes` below). Several
|
||||||
sources feeding the same table accumulate in one file, and so does re-running into
|
sources feeding the same table accumulate in one file, and so does re-running into
|
||||||
the same `--out-dir`, so point a fresh run at an empty directory.
|
the same `--out-dir`, so point a fresh run at an empty directory.
|
||||||
|
|
||||||
|
## Layouts: config, not code
|
||||||
|
|
||||||
|
By default row 1 holds the column names and the data starts on row 2. Exports that
|
||||||
|
put a title above the header, or description rows between it and the data, are
|
||||||
|
described in `dataconvert.json` beside the script (or `--config FILE`). It is
|
||||||
|
gitignored because the marker values name whoever produced the files; start from
|
||||||
|
`dataconvert-example.json`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"layouts": [
|
||||||
|
{ "name": "catalogue",
|
||||||
|
"match": { "row": 2, "column": 1, "in": ["CODE", "ITEM_CODE"] },
|
||||||
|
"header_row": 2, "data_row": 6 }
|
||||||
|
],
|
||||||
|
"bare_sheet_prefixes": ["ref_"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **layouts** are checked against every sheet and CSV, first match wins. A layout
|
||||||
|
matches when the cell at `match.row`/`match.column` (counted from 1, trimmed) is one
|
||||||
|
of `match.in`; then the names come from `header_row` and the data from `data_row`
|
||||||
|
on. The run prints which layout each sheet got. A sheet nothing matches is read
|
||||||
|
normally.
|
||||||
|
- **bare_sheet_prefixes**: sheets whose name starts with one of these are written as
|
||||||
|
`<sheet>.sql` instead of `<workbook>_<sheet>.sql`.
|
||||||
|
|
||||||
|
For a one-off, `--header-row 2 --data-row 6` applies one layout to every file in the
|
||||||
|
run and skips detection.
|
||||||
|
|
||||||
## Sampling for a web LLM
|
## Sampling for a web LLM
|
||||||
|
|
||||||
Full seed files get large fast, and a model only needs to see the shape of the data.
|
Full seed files get large fast, and a model only needs to see the shape of the data.
|
||||||
@@ -35,6 +67,7 @@ what pandas read: a starting point, not DDL. `--no-schema` skips the file.
|
|||||||
| file | does |
|
| file | does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `dataconvert.py` | command line |
|
| `dataconvert.py` | command line |
|
||||||
|
| `config.py` | `dataconvert.json`: layouts and sheet naming |
|
||||||
| `readers.py` | files, directories, ZIPs and globs into DataFrames |
|
| `readers.py` | files, directories, ZIPs and globs into DataFrames |
|
||||||
| `sqlgen.py` | DataFrames into INSERT statements, with the row cap |
|
| `sqlgen.py` | DataFrames into INSERT statements, with the row cap |
|
||||||
| `output.py` | file naming and writing |
|
| `output.py` | file naming and writing |
|
||||||
|
|||||||
117
soleprint/station/tools/dataconvert/config.py
Normal file
117
soleprint/station/tools/dataconvert/config.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Config: what a particular set of spreadsheets looks like.
|
||||||
|
|
||||||
|
The tool knows nothing about any one exporter. Where a header sits, which
|
||||||
|
sheets are recognised by a marker cell, and which sheet names are already
|
||||||
|
unique enough to keep as they are, are facts about whoever produced the files,
|
||||||
|
so they live in dataconvert.json beside this script. That file is gitignored:
|
||||||
|
copy dataconvert-example.json and edit it. Without one, every file is read with
|
||||||
|
its header on row 1.
|
||||||
|
|
||||||
|
{
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"name": "catalogue",
|
||||||
|
"match": {"row": 2, "column": 1, "in": ["CODE", "ITEM"]},
|
||||||
|
"header_row": 2,
|
||||||
|
"data_row": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bare_sheet_prefixes": ["ref_"]
|
||||||
|
}
|
||||||
|
|
||||||
|
Rows and columns are counted as the spreadsheet shows them, from 1. A layout
|
||||||
|
applies to a sheet (or CSV) when the cell at match.row/match.column, trimmed,
|
||||||
|
is one of match.in. The first layout that matches wins.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_PATH = Path(__file__).resolve().parent / "dataconvert.json"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Layout:
|
||||||
|
"""Where the column names are and where the data starts, from row 1."""
|
||||||
|
|
||||||
|
def __init__(self, header_row=1, data_row=None, name="default"):
|
||||||
|
self.name = name
|
||||||
|
self.header_row = int(header_row)
|
||||||
|
self.data_row = int(data_row) if data_row is not None else self.header_row + 1
|
||||||
|
if self.header_row < 1:
|
||||||
|
raise ConfigError(f"layout '{name}': header_row must be 1 or more")
|
||||||
|
if self.data_row <= self.header_row:
|
||||||
|
raise ConfigError(f"layout '{name}': the data has to start below the header row")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_default(self):
|
||||||
|
return self.header_row == 1 and self.data_row == 2
|
||||||
|
|
||||||
|
|
||||||
|
class Rule:
|
||||||
|
def __init__(self, raw, index):
|
||||||
|
name = raw.get("name") or f"layout {index + 1}"
|
||||||
|
match = raw.get("match") or {}
|
||||||
|
values = match.get("in")
|
||||||
|
if not isinstance(values, list) or not values:
|
||||||
|
raise ConfigError(f"layout '{name}': match.in must be a non-empty list")
|
||||||
|
self.row = int(match.get("row", 1))
|
||||||
|
self.column = int(match.get("column", 1))
|
||||||
|
if self.row < 1 or self.column < 1:
|
||||||
|
raise ConfigError(f"layout '{name}': match.row and match.column count from 1")
|
||||||
|
self.values = {str(v).strip() for v in values}
|
||||||
|
self.layout = Layout(raw.get("header_row", 1), raw.get("data_row"), name)
|
||||||
|
|
||||||
|
def matches(self, raw_df):
|
||||||
|
r, c = self.row - 1, self.column - 1
|
||||||
|
if len(raw_df) <= r or raw_df.shape[1] <= c:
|
||||||
|
return False
|
||||||
|
return str(raw_df.iloc[r, c]).strip() in self.values
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, rules=(), bare_sheet_prefixes=(), source=None, forced=None):
|
||||||
|
self.rules = list(rules)
|
||||||
|
self.bare_sheet_prefixes = tuple(bare_sheet_prefixes)
|
||||||
|
self.source = source
|
||||||
|
# --header-row / --data-row on the command line: one layout for every
|
||||||
|
# file, no detection.
|
||||||
|
self.forced = forced
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detects(self):
|
||||||
|
return self.forced is None and bool(self.rules)
|
||||||
|
|
||||||
|
def layout_for(self, raw_df):
|
||||||
|
"""The layout for a sheet read without a header, or None for the default read."""
|
||||||
|
if self.forced is not None:
|
||||||
|
return None if self.forced.is_default else self.forced
|
||||||
|
for rule in self.rules:
|
||||||
|
if rule.matches(raw_df):
|
||||||
|
return rule.layout
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load(path=None, forced=None):
|
||||||
|
"""Read the given config, else dataconvert.json beside this script if there is one."""
|
||||||
|
explicit = path is not None
|
||||||
|
path = Path(path) if explicit else DEFAULT_PATH
|
||||||
|
if not path.exists():
|
||||||
|
if explicit:
|
||||||
|
raise ConfigError(f"no such config file: {path}")
|
||||||
|
return Config(forced=forced)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ConfigError(f"{path} is not valid JSON: {e}")
|
||||||
|
|
||||||
|
rules = [Rule(r, i) for i, r in enumerate(raw.get("layouts", []))]
|
||||||
|
prefixes = raw.get("bare_sheet_prefixes", [])
|
||||||
|
if not isinstance(prefixes, list):
|
||||||
|
raise ConfigError(f"{path}: bare_sheet_prefixes must be a list")
|
||||||
|
return Config(rules, [str(p) for p in prefixes], source=path, forced=forced)
|
||||||
16
soleprint/station/tools/dataconvert/dataconvert-example.json
Normal file
16
soleprint/station/tools/dataconvert/dataconvert-example.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Template for dataconvert.json, which dataconvert.py reads from beside itself (or --config FILE). Copy this to dataconvert.json (gitignored) and describe the files you actually convert there. Rows and columns count from 1, as the spreadsheet shows them.",
|
||||||
|
|
||||||
|
"_layouts": "Checked in order against every sheet and CSV; the first match wins, and a sheet nothing matches is read with its header on row 1. A layout matches when the cell at match.row / match.column, trimmed, is one of match.in. header_row holds the column names; data starts at data_row, and anything in between is skipped.",
|
||||||
|
"layouts": [
|
||||||
|
{
|
||||||
|
"name": "catalogue",
|
||||||
|
"match": { "row": 2, "column": 1, "in": ["CODE", "ITEM_CODE"] },
|
||||||
|
"header_row": 2,
|
||||||
|
"data_row": 6
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"_bare_sheet_prefixes": "Sheets of a multi-sheet workbook are written as <workbook>_<sheet>.sql. A sheet whose name starts with one of these is written as <sheet>.sql instead, because its name is already unique.",
|
||||||
|
"bare_sheet_prefixes": ["ref_"]
|
||||||
|
}
|
||||||
@@ -9,6 +9,11 @@ Usage:
|
|||||||
python3 dataconvert.py --input "data/*" "*.xlsx" --out-dir seed/
|
python3 dataconvert.py --input "data/*" "*.xlsx" --out-dir seed/
|
||||||
python3 dataconvert.py --input path/to/folder/ --out-dir seed/
|
python3 dataconvert.py --input path/to/folder/ --out-dir seed/
|
||||||
python3 dataconvert.py --input path/to/folder/ --out-dir sample/ --max-rows 20
|
python3 dataconvert.py --input path/to/folder/ --out-dir sample/ --max-rows 20
|
||||||
|
python3 dataconvert.py --input export.xlsx --header-row 2 --data-row 6
|
||||||
|
python3 dataconvert.py --input data/ --config their-exports.json
|
||||||
|
|
||||||
|
How a given producer lays out its sheets is not built in: see config.py and
|
||||||
|
dataconvert-example.json.
|
||||||
|
|
||||||
--max-rows is for understanding the data rather than loading it: every table
|
--max-rows is for understanding the data rather than loading it: every table
|
||||||
still gets its file and its SCHEMA.md entry, with only the first N rows, and a
|
still gets its file and its SCHEMA.md entry, with only the first N rows, and a
|
||||||
@@ -22,6 +27,7 @@ import argparse
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from output import write_tables
|
from output import write_tables
|
||||||
|
import config as cfg
|
||||||
from readers import expand_inputs, iter_sources
|
from readers import expand_inputs, iter_sources
|
||||||
from schema import SchemaReport
|
from schema import SchemaReport
|
||||||
|
|
||||||
@@ -40,15 +46,31 @@ def main():
|
|||||||
parser.add_argument("--max-rows", type=positive_int, default=None,
|
parser.add_argument("--max-rows", type=positive_int, default=None,
|
||||||
help="Write at most N rows per table; the header and SCHEMA.md note the full row count and size")
|
help="Write at most N rows per table; the header and SCHEMA.md note the full row count and size")
|
||||||
parser.add_argument("--no-schema", action="store_true", help="Do not write SCHEMA.md")
|
parser.add_argument("--no-schema", action="store_true", help="Do not write SCHEMA.md")
|
||||||
|
parser.add_argument("--header-row", type=positive_int, default=1,
|
||||||
|
help="Spreadsheet row holding the column names, for every file; overrides the config layouts (default 1)")
|
||||||
|
parser.add_argument("--data-row", type=positive_int, default=None,
|
||||||
|
help="First row of data, when rows sit between it and the header (default: the row after the header)")
|
||||||
|
parser.add_argument("--config", default=None,
|
||||||
|
help="Layouts and naming for these files (default: dataconvert.json beside this script, if present)")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
forced = None
|
||||||
|
if args.header_row != 1 or args.data_row is not None:
|
||||||
|
forced = cfg.Layout(args.header_row, args.data_row, "command line")
|
||||||
|
config = cfg.load(args.config, forced)
|
||||||
|
except cfg.ConfigError as e:
|
||||||
|
parser.error(str(e))
|
||||||
|
if config.source is not None:
|
||||||
|
print(f"[dataconvert] config: {config.source}")
|
||||||
|
|
||||||
out_dir = Path(args.out_dir)
|
out_dir = Path(args.out_dir)
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
report = None if args.no_schema else SchemaReport()
|
report = None if args.no_schema else SchemaReport()
|
||||||
for in_path in expand_inputs(args.input):
|
for in_path in expand_inputs(args.input):
|
||||||
for source_name, dfs in iter_sources(in_path):
|
for source_name, dfs in iter_sources(in_path, config):
|
||||||
write_tables(dfs, out_dir, source_name, args.max_rows, report)
|
write_tables(dfs, out_dir, source_name, args.max_rows, report, config.bare_sheet_prefixes)
|
||||||
|
|
||||||
if report is not None and report.entries:
|
if report is not None and report.entries:
|
||||||
print(f"[dataconvert] Generated: {report.write(out_dir, args.max_rows)}")
|
print(f"[dataconvert] Generated: {report.write(out_dir, args.max_rows)}")
|
||||||
|
|||||||
@@ -7,22 +7,26 @@ from pathlib import Path
|
|||||||
from sqlgen import render_table, sanitize_identifier
|
from sqlgen import render_table, sanitize_identifier
|
||||||
|
|
||||||
|
|
||||||
def table_filename(raw_name: str, source_name: str, sheet_count: int) -> str:
|
def table_filename(raw_name: str, source_name: str, sheet_count: int, bare_prefixes=()) -> str:
|
||||||
"""Sheets of a multi-sheet workbook are prefixed with the workbook, unless already namespaced."""
|
"""
|
||||||
|
Sheets of a multi-sheet workbook are prefixed with the workbook, so two
|
||||||
|
workbooks cannot collide, unless the config names the sheet as already
|
||||||
|
unique (bare_sheet_prefixes).
|
||||||
|
"""
|
||||||
clean = sanitize_identifier(raw_name)
|
clean = sanitize_identifier(raw_name)
|
||||||
if sheet_count > 1 and not clean.startswith("drm_") and not clean.startswith("study_"):
|
if sheet_count > 1 and not clean.startswith(tuple(bare_prefixes)):
|
||||||
return f"{sanitize_identifier(source_name)}_{clean}.sql"
|
return f"{sanitize_identifier(source_name)}_{clean}.sql"
|
||||||
return f"{clean}.sql"
|
return f"{clean}.sql"
|
||||||
|
|
||||||
|
|
||||||
def write_tables(dfs: dict, out_dir: Path, source_name: str, max_rows=None, report=None):
|
def write_tables(dfs: dict, out_dir: Path, source_name: str, max_rows=None, report=None, bare_prefixes=()):
|
||||||
"""Write individual .sql files per table/sheet into the output directory."""
|
"""Write individual .sql files per table/sheet into the output directory."""
|
||||||
for raw_name, df in dfs.items():
|
for raw_name, df in dfs.items():
|
||||||
if df.empty:
|
if df.empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
table = sanitize_identifier(raw_name)
|
table = sanitize_identifier(raw_name)
|
||||||
filename = table_filename(raw_name, source_name, len(dfs))
|
filename = table_filename(raw_name, source_name, len(dfs), bare_prefixes)
|
||||||
sql, total, full_bytes, exact = render_table(df, table, max_rows)
|
sql, total, full_bytes, exact = render_table(df, table, max_rows)
|
||||||
out_file = out_dir / filename
|
out_file = out_dir / filename
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,6 @@ import pandas as pd
|
|||||||
|
|
||||||
SUPPORTED = {".csv", ".xlsx", ".xls", ".ods"}
|
SUPPORTED = {".csv", ".xlsx", ".xls", ".ods"}
|
||||||
|
|
||||||
# DRM catalogue sheets put the real column names on row 1 and start the data
|
|
||||||
# on row 5; the rows in between are descriptions. Recognised by the first cell
|
|
||||||
# of the header row.
|
|
||||||
DRM_HEADER_MARKERS = {
|
|
||||||
"CONTROLLED_TERMINOLOGY", "DATA_ELEMENT", "DATA_DOMAIN", "DATA_STATE", "DATA_CATEGORY",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def expand_inputs(inputs):
|
def expand_inputs(inputs):
|
||||||
"""Wildcard patterns become the paths they match; everything else passes through."""
|
"""Wildcard patterns become the paths they match; everything else passes through."""
|
||||||
@@ -37,49 +30,66 @@ def expand_inputs(inputs):
|
|||||||
return paths
|
return paths
|
||||||
|
|
||||||
|
|
||||||
def load_dataframes_from_file(file_path: Path) -> dict:
|
def read_sheet(read, config):
|
||||||
|
"""
|
||||||
|
One sheet or CSV, laid out as the config says.
|
||||||
|
|
||||||
|
With nothing to detect, a plain read, exactly as before. Otherwise the sheet
|
||||||
|
is read once without a header, checked against the layouts, and when one
|
||||||
|
matches the header and data rows are sliced out of that same read.
|
||||||
|
"""
|
||||||
|
if not config.detects and config.forced is None:
|
||||||
|
return read(), None
|
||||||
|
raw = read(header=None)
|
||||||
|
layout = config.layout_for(raw)
|
||||||
|
if layout is None:
|
||||||
|
return read(), None
|
||||||
|
data = raw.iloc[layout.data_row - 1:].copy()
|
||||||
|
data.columns = [str(c).strip() for c in raw.iloc[layout.header_row - 1]]
|
||||||
|
return data, layout
|
||||||
|
|
||||||
|
|
||||||
|
def load_dataframes_from_file(file_path: Path, config) -> dict:
|
||||||
"""Load a file (.csv, .xlsx, .xls, .ods) into {entity_name: DataFrame}."""
|
"""Load a file (.csv, .xlsx, .xls, .ods) into {entity_name: DataFrame}."""
|
||||||
ext = file_path.suffix.lower()
|
ext = file_path.suffix.lower()
|
||||||
dfs = {}
|
dfs = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if ext == ".csv":
|
if ext == ".csv":
|
||||||
dfs[file_path.stem] = pd.read_csv(file_path)
|
dfs[file_path.stem], layout = read_sheet(lambda **kw: pd.read_csv(file_path, **kw), config)
|
||||||
elif ext in [".xlsx", ".xls"]:
|
note_layout(file_path.name, None, layout)
|
||||||
xls = pd.ExcelFile(file_path)
|
elif ext in [".xlsx", ".xls", ".ods"]:
|
||||||
|
xls = pd.ExcelFile(file_path, engine="odf") if ext == ".ods" else pd.ExcelFile(file_path)
|
||||||
for sheet in xls.sheet_names:
|
for sheet in xls.sheet_names:
|
||||||
df_raw = pd.read_excel(xls, sheet_name=sheet, header=None)
|
dfs[sheet], layout = read_sheet(
|
||||||
if len(df_raw) > 1 and str(df_raw.iloc[1, 0]).strip() in DRM_HEADER_MARKERS:
|
lambda sheet=sheet, **kw: pd.read_excel(xls, sheet_name=sheet, **kw), config)
|
||||||
cols = [str(c).strip() for c in df_raw.iloc[1]]
|
note_layout(file_path.name, sheet, layout)
|
||||||
data_df = df_raw.iloc[5:].copy()
|
|
||||||
data_df.columns = cols
|
|
||||||
dfs[sheet] = data_df
|
|
||||||
else:
|
|
||||||
dfs[sheet] = pd.read_excel(xls, sheet_name=sheet)
|
|
||||||
elif ext == ".ods":
|
|
||||||
xls = pd.ExcelFile(file_path, engine="odf")
|
|
||||||
for sheet in xls.sheet_names:
|
|
||||||
dfs[sheet] = pd.read_excel(xls, sheet_name=sheet)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[Warning] Could not read '{file_path.name}': {e}")
|
print(f"[Warning] Could not read '{file_path.name}': {e}")
|
||||||
|
|
||||||
return dfs
|
return dfs
|
||||||
|
|
||||||
|
|
||||||
def iter_sources(path: Path):
|
def note_layout(file_name, sheet, layout):
|
||||||
|
if layout is not None:
|
||||||
|
where = f"{file_name} [{sheet}]" if sheet else file_name
|
||||||
|
print(f"[dataconvert] {where}: layout '{layout.name}', header row {layout.header_row}, data from row {layout.data_row}")
|
||||||
|
|
||||||
|
|
||||||
|
def iter_sources(path: Path, config):
|
||||||
"""Yield (source_name, dfs) for a file, a directory (recursively) or a ZIP archive."""
|
"""Yield (source_name, dfs) for a file, a directory (recursively) or a ZIP archive."""
|
||||||
if path.is_file() and path.suffix.lower() == ".zip":
|
if path.is_file() and path.suffix.lower() == ".zip":
|
||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
with zipfile.ZipFile(path, "r") as zip_ref:
|
with zipfile.ZipFile(path, "r") as zip_ref:
|
||||||
zip_ref.extractall(tmp_dir)
|
zip_ref.extractall(tmp_dir)
|
||||||
yield from iter_sources(Path(tmp_dir))
|
yield from iter_sources(Path(tmp_dir), config)
|
||||||
|
|
||||||
elif path.is_dir():
|
elif path.is_dir():
|
||||||
for root, _, files in os.walk(path):
|
for root, _, files in os.walk(path):
|
||||||
for f in sorted(files):
|
for f in sorted(files):
|
||||||
f_path = Path(root) / f
|
f_path = Path(root) / f
|
||||||
if f_path.suffix.lower() in SUPPORTED:
|
if f_path.suffix.lower() in SUPPORTED:
|
||||||
yield f_path.stem, load_dataframes_from_file(f_path)
|
yield f_path.stem, load_dataframes_from_file(f_path, config)
|
||||||
|
|
||||||
elif path.is_file() and path.suffix.lower() in SUPPORTED:
|
elif path.is_file() and path.suffix.lower() in SUPPORTED:
|
||||||
yield path.stem, load_dataframes_from_file(path)
|
yield path.stem, load_dataframes_from_file(path, config)
|
||||||
|
|||||||
Reference in New Issue
Block a user