Skip to content

Commit 4066e26

Browse files
committed
Publish specification companion package
1 parent bc8fa36 commit 4066e26

6 files changed

Lines changed: 144 additions & 16 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: Release
22

33
env:
44
OPENSTATSPEC_SPECIFICATION_DIR: ${{ github.workspace }}/openstatspec-specification
5+
OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR: ${{ github.workspace }}/openstatspec-specification-package
6+
OPENSTATSPEC_SPECIFICATION_PACKAGE_REF: 2778b784afcb63868ce3c3235a5f1fa190100f9d
7+
OPENSTATSPEC_SPECIFICATION_PACKAGE_VERSION: 0.1.0
58

69
on:
710
push:
@@ -21,11 +24,11 @@ jobs:
2124
build:
2225
runs-on: ubuntu-latest
2326
steps:
24-
- uses: actions/checkout@v7
27+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2528
with:
2629
ref: refs/tags/${{ inputs.release_ref || github.ref_name }}
2730
fetch-depth: 0
28-
- uses: actions/setup-python@v7
31+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
2932
with:
3033
python-version: "3.13"
3134
- name: Verify release tag exists and targets this package version
@@ -35,22 +38,110 @@ jobs:
3538
set -eu
3639
git fetch --force --no-tags origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
3740
python .github/verify_release_ref.py
38-
- run: python -m pip install --upgrade pip build
41+
- run: python -m pip install --upgrade pip build twine pytest
3942
- name: Verify required SPSS engine is publicly installable
4043
run: >-
4144
python -m pip download --only-binary=:all: --no-deps
4245
--dest /tmp/openstatspec-engine-release
4346
openstatspec-pyspssio==0.5.1.post2
44-
- name: Build clean release distributions
45-
run: python -m build
4647
- name: Checkout normative specification fixtures
47-
uses: actions/checkout@v7
48+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4849
with:
4950
repository: OpenStatSpec/specification
5051
ref: f2fdf687d8cb32b944ca55a3e9e7215ffc603019
5152
path: openstatspec-specification
53+
- name: Checkout specification package source
54+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
55+
with:
56+
repository: OpenStatSpec/specification
57+
ref: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_REF }}
58+
path: openstatspec-specification-package
59+
- name: Verify specification package source
60+
env:
61+
SPECIFICATION_PACKAGE_DIR: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR }}
62+
SPECIFICATION_PACKAGE_REF: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_REF }}
63+
SPECIFICATION_PACKAGE_VERSION: ${{ env.OPENSTATSPEC_SPECIFICATION_PACKAGE_VERSION }}
64+
run: |
65+
python - <<'PY'
66+
import os
67+
import subprocess
68+
import tomllib
69+
from pathlib import Path
70+
71+
source = Path(os.environ["SPECIFICATION_PACKAGE_DIR"])
72+
expected_ref = os.environ["SPECIFICATION_PACKAGE_REF"]
73+
actual_ref = subprocess.check_output(
74+
["git", "rev-parse", "HEAD"], cwd=source, text=True,
75+
).strip()
76+
if actual_ref != expected_ref:
77+
raise SystemExit(
78+
f"specification package ref {actual_ref!r} does not match "
79+
f"expected {expected_ref!r}"
80+
)
81+
project = tomllib.loads(
82+
(source / "pyproject.toml").read_text(encoding="utf-8")
83+
)["project"]
84+
if project["name"] != "openstatspec-specification":
85+
raise SystemExit("unexpected specification package name")
86+
expected_version = os.environ["SPECIFICATION_PACKAGE_VERSION"]
87+
if project["version"] != expected_version:
88+
raise SystemExit(
89+
f"specification package version {project['version']!r} does "
90+
f"not match expected {expected_version!r}"
91+
)
92+
for required in (
93+
"pyproject.toml",
94+
"src/openstatspec_specification/dolt.py",
95+
"sql/dolt-adapter-declaration-schema.json",
96+
):
97+
if not (source / required).is_file():
98+
raise SystemExit(f"missing specification package resource: {required}")
99+
PY
100+
- name: Verify specification package tests and repository
101+
run: |
102+
python -m pip install -e "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR"
103+
python -m pytest -q "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR/tests/test_dolt_declarations.py"
104+
python "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR/tools/validate_repository.py"
105+
- name: Build clean release distributions
106+
run: |
107+
rm -rf dist /tmp/openstatspec-release-dists
108+
mkdir -p dist /tmp/openstatspec-release-dists/openstatspec /tmp/openstatspec-release-dists/specification
109+
python -m build --outdir /tmp/openstatspec-release-dists/openstatspec .
110+
python -m build --outdir /tmp/openstatspec-release-dists/specification "$OPENSTATSPEC_SPECIFICATION_PACKAGE_DIR"
111+
python -m twine check \
112+
/tmp/openstatspec-release-dists/openstatspec/* \
113+
/tmp/openstatspec-release-dists/specification/*
114+
python - <<'PY'
115+
from pathlib import Path
116+
from shutil import copy2
117+
118+
sources = {
119+
"openstatspec": Path("/tmp/openstatspec-release-dists/openstatspec"),
120+
"openstatspec_specification": Path("/tmp/openstatspec-release-dists/specification"),
121+
}
122+
copied = []
123+
for normalized_name, source in sources.items():
124+
files = sorted(path for path in source.iterdir() if path.is_file())
125+
if len(files) != 2 or {path.suffix for path in files} != {".whl", ".gz"}:
126+
raise SystemExit(
127+
f"expected one wheel and one sdist for {normalized_name}, got "
128+
f"{[path.name for path in files]}"
129+
)
130+
for path in files:
131+
normalized_artifact = path.name.replace("-", "_").lower()
132+
if not normalized_artifact.startswith(normalized_name + "_"):
133+
raise SystemExit(
134+
f"unexpected artifact name for {normalized_name}: {path.name}"
135+
)
136+
destination = Path("dist") / path.name
137+
copy2(path, destination)
138+
copied.append(destination)
139+
if len(copied) != 4:
140+
raise SystemExit(f"expected four release artifacts, got {len(copied)}")
141+
print("release artifacts:", ", ".join(path.name for path in copied))
142+
PY
52143
- name: Checkout required SPSS engine
53-
uses: actions/checkout@v7
144+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
54145
with:
55146
repository: TonisOrmisson/pyspssio
56147
ref: e069adf33c70bcd9e8e6ee495106479463a84fa2
@@ -60,9 +151,21 @@ jobs:
60151
- run: python -m pytest -m "not services"
61152
- run: python -m venv /tmp/openstatspec-release-smoke
62153
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install ./openstatspec-pyspssio
63-
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install dist/*.whl
154+
- run: /tmp/openstatspec-release-smoke/bin/python -m pip install dist/openstatspec-*.whl
64155
- run: /tmp/openstatspec-release-smoke/bin/openstatspec capabilities
65-
- uses: actions/upload-artifact@v7
156+
- name: Smoke-test specification wheel
157+
run: |
158+
python -m venv /tmp/openstatspec-specification-release-smoke
159+
/tmp/openstatspec-specification-release-smoke/bin/python -m pip install dist/openstatspec_specification-*.whl
160+
/tmp/openstatspec-specification-release-smoke/bin/python - <<'PY'
161+
from openstatspec_specification import DoltDeclarationSource
162+
163+
source = DoltDeclarationSource.packaged()
164+
schema = source.read_json("sql/dolt-adapter-declaration-schema.json")
165+
if schema.get("schema_id") != "openstatspec-dolt-adapter-declaration-v1":
166+
raise SystemExit("packaged specification schema has the wrong identifier")
167+
PY
168+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
66169
with:
67170
name: python-distributions
68171
path: dist/
@@ -76,10 +179,10 @@ jobs:
76179
permissions:
77180
id-token: write
78181
steps:
79-
- uses: actions/download-artifact@v8
182+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
80183
with:
81184
name: python-distributions
82185
path: dist/
83-
- uses: pypa/gh-action-pypi-publish@release/v1
186+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
84187
with:
85188
skip-existing: true

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ implemented capability boundary.
6868
## SPSS-like transformation frontend
6969

7070
The bounded SPSS-like frontend lowers `RECODE`, sequential `COMPUTE` and `IF`,
71+
`STRING` declarations, and `DELETE VARIABLES`,
7172
`VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`, `VARIABLE LEVEL`, and
7273
`EXECUTE` into a language-neutral typed canonical plan. Conditions support
7374
parentheses, variable and numeric literal operands, comparisons, `AND`, and

‎docs/release-readiness.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# 0.5.0 release readiness
1+
# 0.6.0 release readiness
22

33
This page records the expected release contract, not a publication event.
44
Creating a version tag remains a separate maintainer action.
@@ -69,6 +69,8 @@ The gate must prove that:
6969
plan hash and in-place result;
7070
- the exact bounded `COMPUTE`/`IF` program compiles to all seven ordered
7171
operations without dropping `FORMATS`, `VARIABLE LEVEL`, or `EXECUTE`;
72+
- bounded `STRING` declarations and `DELETE VARIABLES` operations preserve
73+
the resulting schema and physical-column identity on supported profiles;
7274
- boolean data results match the equivalent expression and the target's label,
7375
0/1 value labels, `F1.0` print/write format, and nominal level exist in the
7476
normative catalog;

‎docs/transformations.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ OpenStatSpec separates transformation syntax, canonical meaning, and database
44
mutation. This lets multiple language frontends produce the same plan without
55
coupling the executor to any one language.
66

7-
The implemented bounded SPSS-like frontend accepts `RECODE`, sequential
8-
`COMPUTE` and `IF`, `VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`,
9-
`VARIABLE LEVEL`, and `EXECUTE`. Predicates support typed variable/literal
7+
The implemented bounded SPSS-like frontend accepts bounded `STRING`
8+
declarations, `RECODE`, sequential `COMPUTE` and `IF`, `DELETE VARIABLES`,
9+
`VARIABLE LABELS`, `VALUE LABELS`, numeric `FORMATS`, `VARIABLE LEVEL`, and
10+
`EXECUTE`. Predicates support typed variable/literal
1011
operands, parentheses, numeric comparisons, `AND`, and `OR`. String comparison
1112
and v0.2 string assignment fail closed until exact profile-independent
1213
collation and explicit-width semantics are available; arbitrary SPSS, Python,

‎src/openstatspec/transform/validation.py‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ def _bind_conditional_assign(
309309
variable=target.name,
310310
)
311311

312+
313+
def _creates_variable(operation: object) -> bool:
314+
return (
315+
isinstance(operation, CreateVariableOperation)
316+
or (
317+
isinstance(operation, AssignOperation)
318+
and operation.target_mode == "create"
319+
)
320+
)
321+
322+
312323
def bind_transformation_plan(
313324
plan: TransformationPlan, schema: VariableSchema
314325
) -> BoundTransformation:
@@ -320,7 +331,7 @@ def bind_transformation_plan(
320331
variables = list(schema.variables)
321332
for operation_index, operation in enumerate(plan.operations):
322333
later_create = any(
323-
isinstance(later_operation, CreateVariableOperation)
334+
_creates_variable(later_operation)
324335
for later_operation in plan.operations[operation_index + 1:]
325336
)
326337
if isinstance(operation, CreateVariableOperation):

‎tests/test_transform_frontend.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,16 @@ def test_delete_final_variable_requires_a_later_explicit_create() -> None:
652652
)
653653

654654

655+
def test_delete_final_variable_allows_a_later_compute_create() -> None:
656+
schema = _schema(VariableDefinition("only", "numeric"))
657+
658+
bound = _compile("DELETE VARIABLES only. COMPUTE replacement = 1.", schema)
659+
660+
assert bound.output_schema.variables == (
661+
VariableDefinition("replacement", "numeric"),
662+
)
663+
664+
655665
def test_recode_string_literals_respect_declared_width() -> None:
656666
schema = _schema(VariableDefinition(
657667
"note", "string", declared_string_width=3,

0 commit comments

Comments
 (0)