Coverage for src / superset_io / utils.py: 100%
53 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-02 13:09 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-02 13:09 +0100
1import io
2import logging
3import zipfile
4from pathlib import Path
6log = logging.getLogger("superset_io")
9def get_version():
10 from importlib import metadata
12 try:
13 return metadata.version("superset-io")
14 except metadata.PackageNotFoundError:
15 return "[not found] Use `uv sync` when developing!"
18def zipfile_buffer_from_folder(folder: Path | str) -> io.BytesIO:
19 """Create a zipfile.ZipFile object from a folder path in memory.
20 Args:
21 folder (Path | str): Path to the folder to be zipped.
22 Returns:
23 zipfile.ZipFile: In-memory zipfile object.
24 """
26 folder = Path(folder)
27 if not folder.exists() or not folder.is_dir():
28 raise ValueError(f"Not a folder: {folder}")
30 root_prefix = folder.name
32 buffer = io.BytesIO()
33 with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as zf:
34 for path in folder.rglob("*"):
35 if path.is_file():
36 arcname = Path(root_prefix) / path.relative_to(folder)
37 zf.write(path, arcname.as_posix())
39 buffer.seek(0)
40 return buffer
43def zipfile_buffer_from_zipfile(zip_path: Path | str) -> io.BytesIO:
44 """Create an in-memory copy of an existing .zip file.
45 Args:
46 zip_path (Path | str): Path to the existing .zip file.
47 Returns:
48 io.BytesIO: In-memory buffer containing the zip file contents.
49 """
50 zip_path = Path(zip_path)
51 buffer = io.BytesIO()
53 # Read the original zip file and write its contents into the buffer
54 with Path(zip_path).open("rb") as f:
55 buffer.write(f.read())
57 buffer.seek(0) # Reset buffer pointer to the beginning for reading
58 return buffer
61def validate_assets_bundle_structure(zip_buffer: io.BytesIO | bytes | Path) -> None:
62 """
63 Check that a zip files structure will be accepted by /api/v1/assets/import.
65 Superset assets bundles typically look like:
66 assets_export_<timestamp>/metadata.yaml
67 assets_export_<timestamp>/charts/area_54.yaml
68 assets_export_<timestamp>/dashboards/world_banks_data_1.yaml
69 assets_export_<timestamp>/databases/examples.yaml
70 assets_export_<timestamp>/datasets/bart_lines_7.yaml
72 IMPORTANT: metadata.yaml is not at the zip root, but under a
73 single top-level folder.
74 """
76 zip_io: io.BytesIO | str
77 if isinstance(zip_buffer, (bytes, memoryview, bytearray)):
78 zip_io = io.BytesIO(zip_buffer)
79 elif isinstance(zip_buffer, Path):
80 zip_io = str(zip_buffer)
81 else:
82 zip_io = zip_buffer
84 with zipfile.ZipFile(zip_io, "r") as zf:
85 names = [n for n in zf.namelist() if not n.endswith("/")]
87 try:
88 # Find metadata.yaml anywhere
89 metadata_candidates = [n for n in names if n.endswith("metadata.yaml")]
90 if not metadata_candidates:
91 raise ValueError("Missing metadata.yaml in ZIP.")
93 # Enforce the expected structure: exactly one top-level folder,
94 # and metadata.yaml under it e.g. "assets_export_20260212T092634/metadata.yaml"
95 roots = {n.split("/", 1)[0] for n in names if "/" in n}
96 if len(roots) != 1:
97 raise ValueError(
98 "Expected exactly one top-level folder in assets bundle, but found: "
99 f"{sorted(roots)}."
100 )
102 root = next(iter(roots))
103 expected_path = f"{root}/metadata.yaml"
104 if expected_path not in names:
105 raise ValueError(
106 f"metadata.yaml not found at expected path '{expected_path}'. "
107 f"Candidates were: {metadata_candidates}"
108 )
109 except ValueError:
110 log.debug("Invalid ZIP contents:\n" + " \n".join(names))
111 raise