Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 130 additions & 11 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Release

env:
OPENSTATSPEC_SPECIFICATION_DIR: ${{ github.workspace }}/openstatspec-specification
OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR: ${{ github.workspace }}/openstatspec-specification-package
OPENSTATSPEC_SPECIFICATION_PACKAGE_REF: 2778b784afcb63868ce3c3235a5f1fa190100f9d
OPENSTATSPEC_SPECIFICATION_PACKAGE_VERSION: 0.1.0

on:
push:
Expand All @@ -21,11 +24,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: refs/tags/${{ inputs.release_ref || github.ref_name }}
fetch-depth: 0
- uses: actions/setup-python@v7
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Verify release tag exists and targets this package version
Expand All @@ -35,22 +38,119 @@ jobs:
set -eu
git fetch --force --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
python .github/verify_release_ref.py
- run: python -m pip install --upgrade pip build
- run: python -m pip install --upgrade pip build twine pytest
- name: Verify required SPSS engine is publicly installable
run: >-
python -m pip download --only-binary=:all: --no-deps
--dest /tmp/openstatspec-engine-release
openstatspec-pyspssio==0.5.1.post2
- name: Build clean release distributions
run: python -m build
- name: Checkout normative specification fixtures
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: OpenStatSpec/specification
ref: f2fdf687d8cb32b944ca55a3e9e7215ffc603019
path: openstatspec-specification
- name: Checkout specification package source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: OpenStatSpec/specification
ref: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_REF }}
path: openstatspec-specification-package
- name: Verify specification package source
env:
SPECIFICATION_PACKAGE_DIR: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR }}
SPECIFICATION_PACKAGE_REF: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_REF }}
SPECIFICATION_PACKAGE_VERSION: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_VERSION }}
run: |
python - <<'PY'
import os
import subprocess
import tomllib
from pathlib import Path

source = Path(os.environ["SPECIFICATION_PACKAGE_DIR"])
expected_ref = os.environ["SPECIFICATION_PACKAGE_REF"]
actual_ref = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=source, text=True,
).strip()
if actual_ref != expected_ref:
raise SystemExit(
f"specification package ref {actual_ref!r} does not match "
f"expected {expected_ref!r}"
)
project = tomllib.loads(
(source / "pyproject.toml").read_text(encoding="utf-8")
)["project"]
if project["name"] != "openstatspec-specification":
raise SystemExit("unexpected specification package name")
expected_version = os.environ["SPECIFICATION_PACKAGE_VERSION"]
if project["version"] != expected_version:
raise SystemExit(
f"specification package version {project['version']!r} does "
f"not match expected {expected_version!r}"
)
for required in (
"pyproject.toml",
"src/openstatspec_specification/dolt.py",
"sql/dolt-adapter-declaration-schema.json",
):
if not (source / required).is_file():
raise SystemExit(f"missing specification package resource: {required}")
PY
- name: Verify specification package tests and repository
run: |
python -m pip install -e "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR"
python -m pytest -q "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR/tests/test_dolt_declarations.py"
python "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR/tools/validate_repository.py"
- name: Build clean release distributions
run: |
rm -rf dist /tmp/openstatspec-release-dists
mkdir -p dist/openstatspec dist/openstatspec-specification \
/tmp/openstatspec-release-dists/openstatspec \
/tmp/openstatspec-release-dists/specification
python -m build --outdir /tmp/openstatspec-release-dists/openstatspec .
python -m build --outdir /tmp/openstatspec-release-dists/specification "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR"
python -m twine check \
/tmp/openstatspec-release-dists/openstatspec/* \
/tmp/openstatspec-release-dists/specification/*
python - <<'PY'
from pathlib import Path
from shutil import copy2

sources = {
"openstatspec": (
Path("/tmp/openstatspec-release-dists/openstatspec"),
Path("dist/openstatspec"),
),
"openstatspec_specification": (
Path("/tmp/openstatspec-release-dists/specification"),
Path("dist/openstatspec-specification"),
),
}
copied = []
for normalized_name, (source, destination_dir) in sources.items():
destination_dir.mkdir(parents=True, exist_ok=True)
files = sorted(path for path in source.iterdir() if path.is_file())
if len(files) != 2 or {path.suffix for path in files} != {".whl", ".gz"}:
raise SystemExit(
f"expected one wheel and one sdist for {normalized_name}, got "
f"{[path.name for path in files]}"
)
for path in files:
normalized_artifact = path.name.replace("-", "_").lower()
if not normalized_artifact.startswith(normalized_name + "_"):
raise SystemExit(
f"unexpected artifact name for {normalized_name}: {path.name}"
)
destination = destination_dir / path.name
copy2(path, destination)
copied.append(destination)
if len(copied) != 4:
raise SystemExit(f"expected four release artifacts, got {len(copied)}")
print("release artifacts:", ", ".join(path.name for path in copied))
PY
- name: Checkout required SPSS engine
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: TonisOrmisson/pyspssio
ref: e069adf33c70bcd9e8e6ee495106479463a84fa2
Expand All @@ -60,9 +160,21 @@ jobs:
- run: python -m pytest -m "not services"
- run: python -m venv /tmp/openstatspec-release-smoke
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install ./openstatspec-pyspssio
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install dist/*.whl
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install dist/openstatspec/openstatspec-*.whl
- run: /tmp/openstatspec-release-smoke/bin/openstatspec capabilities
- uses: actions/upload-artifact@v7
- name: Smoke-test specification wheel
run: |
python -m venv /tmp/openstatspec-specification-release-smoke
/tmp/openstatspec-specification-release-smoke/bin/python -m pip install dist/openstatspec-specification/openstatspec_specification-*.whl
/tmp/openstatspec-specification-release-smoke/bin/python - <<'PY'
from openstatspec_specification import DoltDeclarationSource

source = DoltDeclarationSource.packaged()
schema = source.read_json("sql/dolt-adapter-declaration-schema.json")
if schema.get("schema_id") != "openstatspec-dolt-adapter-declaration-v1":
raise SystemExit("packaged specification schema has the wrong identifier")
PY
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: python-distributions
path: dist/
Expand All @@ -76,10 +188,17 @@ jobs:
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
- name: Publish openstatspec to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/openstatspec/
skip-existing: true
- name: Publish openstatspec-specification to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/openstatspec-specification/
skip-existing: true
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ implemented capability boundary.
## SPSS-like transformation frontend

The bounded SPSS-like frontend lowers `RECODE`, sequential `COMPUTE` and `IF`,
`STRING` declarations, and `DELETE VARIABLES`,
`VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`, `VARIABLE LEVEL`, and
`EXECUTE` into a language-neutral typed canonical plan. Conditions support
parentheses, variable and numeric literal operands, comparisons, `AND`, and
Expand Down
4 changes: 3 additions & 1 deletion docs/release-readiness.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 0.5.0 release readiness
# 0.6.0 release readiness

This page records the expected release contract, not a publication event.
Creating a version tag remains a separate maintainer action.
Expand Down Expand Up @@ -69,6 +69,8 @@ The gate must prove that:
plan hash and in-place result;
- the exact bounded `COMPUTE`/`IF` program compiles to all seven ordered
operations without dropping `FORMATS`, `VARIABLE LEVEL`, or `EXECUTE`;
- bounded `STRING` declarations and `DELETE VARIABLES` operations preserve
the resulting schema and physical-column identity on supported profiles;
- boolean data results match the equivalent expression and the target's label,
0/1 value labels, `F1.0` print/write format, and nominal level exist in the
normative catalog;
Expand Down
7 changes: 4 additions & 3 deletions docs/transformations.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ OpenStatSpec separates transformation syntax, canonical meaning, and database
mutation. This lets multiple language frontends produce the same plan without
coupling the executor to any one language.

The implemented bounded SPSS-like frontend accepts `RECODE`, sequential
`COMPUTE` and `IF`, `VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`,
`VARIABLE LEVEL`, and `EXECUTE`. Predicates support typed variable/literal
The implemented bounded SPSS-like frontend accepts bounded `STRING`
declarations, `RECODE`, sequential `COMPUTE` and `IF`, `DELETE VARIABLES`,
`VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`, `VARIABLE LEVEL`, and
`EXECUTE`. Predicates support typed variable/literal
operands, parentheses, numeric comparisons, `AND`, and `OR`. String comparison
and v0.2 string assignment fail closed until exact profile-independent
collation and explicit-width semantics are available; arbitrary SPSS, Python,
Expand Down
13 changes: 12 additions & 1 deletion src/openstatspec/transform/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,17 @@ def _bind_conditional_assign(
variable=target.name,
)


def _creates_variable(operation: object) -> bool:
return (
isinstance(operation, CreateVariableOperation)
or (
isinstance(operation, AssignOperation)
and operation.target_mode == "create"
)
)


def bind_transformation_plan(
plan: TransformationPlan, schema: VariableSchema
) -> BoundTransformation:
Expand All @@ -320,7 +331,7 @@ def bind_transformation_plan(
variables = list(schema.variables)
for operation_index, operation in enumerate(plan.operations):
later_create = any(
isinstance(later_operation, CreateVariableOperation)
_creates_variable(later_operation)
for later_operation in plan.operations[operation_index + 1:]
)
if isinstance(operation, CreateVariableOperation):
Expand Down
10 changes: 10 additions & 0 deletions tests/test_transform_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,16 @@ def test_delete_final_variable_requires_a_later_explicit_create() -> None:
)


def test_delete_final_variable_allows_a_later_compute_create() -> None:
schema = _schema(VariableDefinition("only", "numeric"))

bound = _compile("DELETE VARIABLES only. COMPUTE replacement = 1.", schema)

assert bound.output_schema.variables == (
VariableDefinition("replacement", "numeric"),
)


def test_recode_string_literals_respect_declared_width() -> None:
schema = _schema(VariableDefinition(
"note", "string", declared_string_width=3,
Expand Down