Skip to content

Commit 01cd66a

Browse files
committed
Report unsupported SPSS dictionary fidelity gaps
1 parent 9876694 commit 01cd66a

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎src/openstatspec/spss/sav.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ def import_sav_dataset(*, source: str | Path, database_url: str, dataset_id: str
7373
variables=variables, file_label=getattr(meta, "file_label", "") or "",
7474
source_encoding=getattr(meta, "file_encoding", None),
7575
documents=json.dumps(list(getattr(meta, "notes", []) or [])),
76+
multiple_response_sets=json.dumps(dict(getattr(meta, "mr_sets", {}) or {}), default=str),
7677
)
77-
return {**result, "loss_report": LossReport().events}
78+
return {**result, "loss_report": _import_loss_report(meta)}
7879

7980

8081
def export_sav_dataset(*, database_url: str, dataset_id: str, destination: str | Path) -> dict[str, Any]:
@@ -107,10 +108,26 @@ def export_sav_dataset(*, database_url: str, dataset_id: str, destination: str |
107108
)
108109
return {
109110
"dataset_id": dataset_id, "destination": str(destination_path),
110-
"loss_report": LossReport().events,
111+
"loss_report": _export_loss_report(dataset, variables),
111112
}
112113

113114

115+
116+
117+
def _import_loss_report(meta: Any) -> tuple[dict[str, str], ...]:
118+
if getattr(meta, "mr_sets", {}) or {}:
119+
return ({"code": "multiple-response-sets-not-exportable", "detail": "Multiple-response sets are catalogued but pyreadstat cannot write them back to SAV."},)
120+
return LossReport().events
121+
122+
123+
def _export_loss_report(dataset: dict[str, Any], variables: list[dict[str, Any]]) -> tuple[dict[str, str], ...]:
124+
events = []
125+
if dataset["multiple_response_sets"] != "{}":
126+
events.append({"code": "multiple-response-sets-not-exported", "detail": "The SAV writer has no multiple-response-set output capability."})
127+
if any(item.get("alignment") not in (None, "unknown") for item in variables):
128+
events.append({"code": "variable-alignment-not-exported", "detail": "The SAV writer has no variable-alignment output capability."})
129+
return tuple(events)
130+
114131
def _require_source(source_path: Path) -> None:
115132
if source_path.suffix.lower() not in {".sav", ".zsav"}:
116133
raise UnsupportedOperationError("Only unencrypted SAV and ZSAV sources are supported.")

‎src/openstatspec/sql/wide.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def catalog(metadata: MetaData) -> tuple[Table, Table]:
2121
Column("case_count", BigInteger, nullable=False),
2222
Column("file_label", Text, nullable=False, default=""),
2323
Column("documents", Text, nullable=False, default="[]"),
24+
Column("multiple_response_sets", Text, nullable=False, default="{}"),
2425
)
2526
variables = Table(
2627
"variable_catalog", metadata,
@@ -61,6 +62,7 @@ def create_wide_dataset(
6162
*, database_url: str, dataset_id: str, source_name: str, source_format: str,
6263
rows: Iterable[Mapping[str, Any]], variables: list[dict[str, Any]], file_label: str = "",
6364
source_encoding: str | None = None, documents: str = "[]",
65+
multiple_response_sets: str = "{}",
6466
) -> dict[str, Any]:
6567
profile = profile_for_url(database_url)
6668
preflight(profile, len(variables))
@@ -83,6 +85,7 @@ def create_wide_dataset(
8385
dataset_id=dataset_id, data_table=data_table.name, source_format=source_format,
8486
source_name=source_name, source_encoding=source_encoding, case_count=len(materialized),
8587
file_label=file_label, documents=documents,
88+
multiple_response_sets=multiple_response_sets,
8689
))
8790
connection.execute(insert(variable_catalog), [dict(dataset_id=dataset_id, **item) for item in variables])
8891
if materialized:

‎tests/test_loss_reports.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from openstatspec import export_sav
2+
from openstatspec.sql.wide import create_wide_dataset
3+
4+
5+
def test_export_reports_unwritable_spss_dictionary_features(tmp_path) -> None:
6+
database = f"sqlite:///{tmp_path / 'dataset.sqlite'}"
7+
create_wide_dataset(
8+
database_url=database,
9+
dataset_id="dictionary-gap",
10+
source_name="fixture.sav",
11+
source_format="SAV",
12+
rows=[{"answer": 1.0}],
13+
variables=[{
14+
"ordinal": 1, "source_name": "answer", "physical_name": "answer",
15+
"storage_kind": "numeric", "string_width": None, "label": "",
16+
"format": "F8.0", "measure": "nominal", "alignment": "left",
17+
"display_width": 8, "value_labels": "{}", "missing_ranges": "[]",
18+
}],
19+
multiple_response_sets='{"set1": {"label": "example"}}',
20+
)
21+
22+
result = export_sav(
23+
database_url=database, dataset_id="dictionary-gap", destination=tmp_path / "output.sav"
24+
)
25+
assert {event["code"] for event in result["loss_report"]} == {
26+
"multiple-response-sets-not-exported", "variable-alignment-not-exported"
27+
}

0 commit comments

Comments
 (0)