Skip to content

Commit 90a150a

Browse files
committed
Initialize Python reference adapter scaffold
0 parents  commit 90a150a

12 files changed

Lines changed: 251 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
python-version: ["3.11", "3.12", "3.13", "3.14"]
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: actions/setup-python@v5
17+
with:
18+
python-version: ${{ matrix.python-version }}
19+
- run: python -m pip install --upgrade pip
20+
- run: python -m pip install -e ".[dev]"
21+
- run: python -m pytest

‎.gitignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
__pycache__/
2+
.pytest_cache/
3+
*.py[cod]
4+
.venv/
5+
build/
6+
dist/
7+
*.egg-info/

‎README.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# OpenStatSpec Python
2+
3+
The reference Python implementation of the OpenStatSpec specification.
4+
5+
This package implements the specification; it does not define or extend it.
6+
The normative model lives in the `OpenStatSpec/specification` repository.
7+
8+
## Boundaries
9+
10+
For each supported import, one source dataset becomes one dedicated wide SQL
11+
table. Cases are rows and source variables are physical SQL columns. Catalog
12+
metadata is stored separately. The adapter does not reshape data, create EAV
13+
or long-form tables, or harmonize studies or waves.
14+
15+
Unsupported source features, SQL targets, or export paths fail explicitly.
16+
There is no silent truncation, type conversion, metadata loss, or partial
17+
import.
18+
19+
## Package layout
20+
21+
- `openstatspec.core`: pure standard concepts, validation, versions,
22+
capabilities, and loss reports.
23+
- `openstatspec.sql`: database connection and wide-table/catalog operations.
24+
- `openstatspec.spss`: SAV/ZSAV adapter boundary.
25+
26+
## Intended workflow
27+
28+
```python
29+
from openstatspec import export_sav, import_sav
30+
31+
import_sav("responses.sav", database_url="postgresql+psycopg://user:password@server/database", dataset_id="responses-2026")
32+
export_sav(database_url="postgresql+psycopg://user:password@server/database", dataset_id="responses-2026", destination="responses-roundtrip.sav")
33+
```
34+
35+
```text
36+
openstatspec import responses.sav --database-url postgresql+psycopg://... --dataset-id responses-2026
37+
openstatspec export --database-url postgresql+psycopg://... --dataset-id responses-2026 --output responses-roundtrip.sav
38+
```
39+
40+
The initial scaffold deliberately declares no conforming SPSS or SQL profile.
41+
The entry points are stable, but fail explicitly until an implementation has
42+
been proven by OpenStatSpec conformance fixtures.

‎docs/architecture.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Architecture
2+
3+
`openstatspec.core` contains only pure concepts defined by the standard:
4+
versioning, validation concepts, capability declarations, and loss reports.
5+
`openstatspec.sql` owns database-target and strict wide-table/catalog work.
6+
`openstatspec.spss` owns SAV/ZSAV source and export behavior.
7+
8+
The public workflow remains database-connected. A supported import receives an
9+
SPSS source, a database URL or connection, and a dataset identity. It creates
10+
one dedicated wide data table plus separate catalog metadata. Export identifies
11+
the database dataset and writes a supported SPSS file.
12+
13+
No layer may silently substitute EAV, long-form views, JSON, pivots, automatic
14+
harmonization, truncation, or partial import for the standard's strict mapping.

‎pyproject.toml‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "openstatspec"
7+
version = "0.0.0"
8+
description = "Reference adapter for the OpenStatSpec relational contract"
9+
readme = "README.md"
10+
requires-python = ">=3.11"
11+
license = {text = "Apache-2.0"}
12+
authors = [{name = "OpenStatSpec"}]
13+
dependencies = ["SQLAlchemy>=2.0"]
14+
15+
[project.optional-dependencies]
16+
dev = ["pytest>=8"]
17+
18+
[project.scripts]
19+
openstatspec = "openstatspec.cli:main"
20+
21+
[tool.pytest.ini_options]
22+
testpaths = ["tests"]
23+
addopts = "-q"
24+
25+
[tool.hatch.build.targets.wheel]
26+
packages = ["src/openstatspec"]

‎src/openstatspec/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Public API for the OpenStatSpec Python reference implementation."""
2+
3+
from .api import capabilities, export_sav, import_sav, inspect, validate
4+
from .core import CapabilityDeclaration, LossReport, UnsupportedOperationError
5+
6+
__all__ = ["CapabilityDeclaration", "LossReport", "UnsupportedOperationError", "capabilities", "export_sav", "import_sav", "inspect", "validate"]

‎src/openstatspec/api.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Stable, database-connected public API."""
2+
3+
from collections.abc import Mapping
4+
from pathlib import Path
5+
from typing import Any
6+
7+
from .core import CapabilityDeclaration
8+
from .spss import export_dataset, import_dataset, inspect_source
9+
from .sql import validate_dataset
10+
11+
12+
def capabilities() -> Mapping[str, Any]:
13+
return CapabilityDeclaration.empty().as_dict()
14+
15+
16+
def inspect(source: str | Path, /, **options: Any) -> Mapping[str, Any]:
17+
return inspect_source(source, **options)
18+
19+
20+
def import_sav(source: str | Path, /, *, database_url: Any, dataset_id: str, **options: Any) -> Mapping[str, Any]:
21+
"""Import one source file into one dedicated wide SQL table."""
22+
return import_dataset(source, database_url=database_url, dataset_id=dataset_id, **options)
23+
24+
25+
def export_sav(*, database_url: Any, dataset_id: str, destination: str | Path, **options: Any) -> Mapping[str, Any]:
26+
"""Export one database-resident conforming dataset to SAV/ZSAV."""
27+
return export_dataset(database_url=database_url, dataset_id=dataset_id, destination=destination, **options)
28+
29+
30+
def validate(*, database_url: Any, dataset_id: str, **options: Any) -> Mapping[str, Any]:
31+
return validate_dataset(database_url=database_url, dataset_id=dataset_id, **options)

‎src/openstatspec/cli.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Command-line entry points for the database-connected workflow."""
2+
3+
import argparse
4+
from collections.abc import Sequence
5+
6+
from .api import export_sav, import_sav
7+
8+
9+
def main(argv: Sequence[str] | None = None) -> int:
10+
parser = argparse.ArgumentParser(prog="openstatspec")
11+
commands = parser.add_subparsers(dest="command", required=True)
12+
importer = commands.add_parser("import", help="import one SAV/ZSAV file")
13+
importer.add_argument("source")
14+
importer.add_argument("--database-url", required=True)
15+
importer.add_argument("--dataset-id", required=True)
16+
exporter = commands.add_parser("export", help="export one dataset to SAV/ZSAV")
17+
exporter.add_argument("--database-url", required=True)
18+
exporter.add_argument("--dataset-id", required=True)
19+
exporter.add_argument("--output", required=True)
20+
args = parser.parse_args(argv)
21+
if args.command == "import":
22+
import_sav(args.source, database_url=args.database_url, dataset_id=args.dataset_id)
23+
else:
24+
export_sav(database_url=args.database_url, dataset_id=args.dataset_id, destination=args.output)
25+
return 0

‎src/openstatspec/core/__init__.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Pure OpenStatSpec concepts; no file or database adapter code."""
2+
3+
from dataclasses import dataclass, field
4+
from typing import Any
5+
6+
7+
class UnsupportedOperationError(NotImplementedError):
8+
"""Raised when faithful support for a requested operation is unavailable."""
9+
10+
11+
@dataclass(frozen=True)
12+
class LossReport:
13+
"""Machine-readable fidelity outcome for a completed future operation."""
14+
15+
events: tuple[dict[str, Any], ...] = ()
16+
17+
18+
@dataclass(frozen=True)
19+
class CapabilityDeclaration:
20+
specification: str | None = None
21+
formats: dict[str, Any] = field(default_factory=dict)
22+
database_profiles: dict[str, Any] = field(default_factory=dict)
23+
24+
@classmethod
25+
def empty(cls) -> "CapabilityDeclaration":
26+
return cls()
27+
28+
def as_dict(self) -> dict[str, Any]:
29+
return {"specification": self.specification, "formats": self.formats, "database_profiles": self.database_profiles, "operations": {"inspect": False, "import_sav": False, "export_sav": False, "validate": False}}

‎src/openstatspec/spss/__init__.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"""SAV/ZSAV adapter boundary; never a data-model transformation layer."""
2+
3+
from pathlib import Path
4+
from typing import Any
5+
6+
from ..core import UnsupportedOperationError
7+
8+
9+
def inspect_source(source: str | Path, **options: Any) -> dict[str, Any]:
10+
raise UnsupportedOperationError("No SAV/ZSAV profile is implemented; source metadata cannot be guessed.")
11+
12+
13+
def import_dataset(source: str | Path, *, database_url: Any, dataset_id: str, **options: Any) -> dict[str, Any]:
14+
raise UnsupportedOperationError("No SAV/ZSAV-to-SQL profile is implemented; no partial import was attempted.")
15+
16+
17+
def export_dataset(*, database_url: Any, dataset_id: str, destination: str | Path, **options: Any) -> dict[str, Any]:
18+
raise UnsupportedOperationError("No SQL-to-SAV/ZSAV profile is implemented; no lossy export was attempted.")

0 commit comments

Comments
 (0)