meds-summary-stats 0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meds_summary_stats-0/.github/actions/setup/action.yaml +30 -0
- meds_summary_stats-0/.github/dependabot.yml +44 -0
- meds_summary_stats-0/.github/workflows/action-selftest.yaml +124 -0
- meds_summary_stats-0/.github/workflows/code-quality-main.yaml +28 -0
- meds_summary_stats-0/.github/workflows/code-quality-pr.yaml +46 -0
- meds_summary_stats-0/.github/workflows/meds-stats-regression.yaml +184 -0
- meds_summary_stats-0/.github/workflows/python-build.yaml +112 -0
- meds_summary_stats-0/.github/workflows/tests.yaml +71 -0
- meds_summary_stats-0/.github/zizmor.yml +29 -0
- meds_summary_stats-0/.gitignore +216 -0
- meds_summary_stats-0/.pre-commit-config.yaml +155 -0
- meds_summary_stats-0/.python-version +1 -0
- meds_summary_stats-0/AGENTS.md +111 -0
- meds_summary_stats-0/CLAUDE.md +111 -0
- meds_summary_stats-0/CONTRIBUTORS.md +286 -0
- meds_summary_stats-0/LICENSE +21 -0
- meds_summary_stats-0/PKG-INFO +443 -0
- meds_summary_stats-0/README.md +425 -0
- meds_summary_stats-0/action.yml +192 -0
- meds_summary_stats-0/conftest.py +45 -0
- meds_summary_stats-0/docs/github-action.md +250 -0
- meds_summary_stats-0/docs/stats-surface.md +337 -0
- meds_summary_stats-0/examples/build_datasets.py +294 -0
- meds_summary_stats-0/examples/build_stats.py +61 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/data/held_out/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/data/train/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/data/tuning/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/metadata/codes.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/metadata/dataset.json +7 -0
- meds_summary_stats-0/examples/datasets/v1_baseline/metadata/subject_splits.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/data/held_out/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/data/train/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/data/tuning/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/codes.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/dataset.json +7 -0
- meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/subject_splits.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/data/held_out/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/data/train/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/data/tuning/0.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/codes.parquet +0 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/dataset.json +7 -0
- meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/subject_splits.parquet +0 -0
- meds_summary_stats-0/examples/policy.yaml +121 -0
- meds_summary_stats-0/examples/stats/v1_baseline.json +318 -0
- meds_summary_stats-0/examples/stats/v1_etl_regression.json +318 -0
- meds_summary_stats-0/examples/stats/v2_data_refresh.json +318 -0
- meds_summary_stats-0/pyproject.toml +106 -0
- meds_summary_stats-0/setup.cfg +4 -0
- meds_summary_stats-0/src/meds_summary_stats/__init__.py +10 -0
- meds_summary_stats-0/src/meds_summary_stats/__main__.py +10 -0
- meds_summary_stats-0/src/meds_summary_stats/cli.py +415 -0
- meds_summary_stats-0/src/meds_summary_stats/compare.py +644 -0
- meds_summary_stats-0/src/meds_summary_stats/config.py +204 -0
- meds_summary_stats-0/src/meds_summary_stats/digests.py +156 -0
- meds_summary_stats-0/src/meds_summary_stats/extract.py +1484 -0
- meds_summary_stats-0/src/meds_summary_stats/flatten.py +186 -0
- meds_summary_stats-0/src/meds_summary_stats/ontology.py +193 -0
- meds_summary_stats-0/src/meds_summary_stats/policy.py +459 -0
- meds_summary_stats-0/src/meds_summary_stats/report.py +249 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/PKG-INFO +443 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/SOURCES.txt +70 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/dependency_links.txt +1 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/entry_points.txt +3 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/requires.txt +3 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/scm_file_list.json +66 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/scm_version.json +8 -0
- meds_summary_stats-0/src/meds_summary_stats.egg-info/top_level.txt +1 -0
- meds_summary_stats-0/tests/.gitkeep +0 -0
- meds_summary_stats-0/tests/test_examples.py +142 -0
- meds_summary_stats-0/tests/test_integration.py +441 -0
- meds_summary_stats-0/tests/test_properties.py +395 -0
- meds_summary_stats-0/uv.lock +785 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Setup Package
|
|
2
|
+
description: This workflow sets up the package for other workflows.
|
|
3
|
+
inputs:
|
|
4
|
+
python-version:
|
|
5
|
+
description: The Python version to use for the setup.
|
|
6
|
+
required: true
|
|
7
|
+
default: "3.11"
|
|
8
|
+
group:
|
|
9
|
+
description: The dependency group to install (benchmarks, dev, docs)
|
|
10
|
+
required: false
|
|
11
|
+
default: "dev"
|
|
12
|
+
runs:
|
|
13
|
+
using: "composite"
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/setup-python@v7.0.0
|
|
16
|
+
with:
|
|
17
|
+
python-version: "${{ inputs.python-version }}"
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
cache-suffix: "py${{ inputs.python-version }}"
|
|
24
|
+
|
|
25
|
+
- name: Install packages
|
|
26
|
+
shell: bash
|
|
27
|
+
env:
|
|
28
|
+
UV_GROUP: ${{ inputs.group }}
|
|
29
|
+
run: |
|
|
30
|
+
uv sync --locked --group "$UV_GROUP"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# GitHub Actions: track action versions across workflows.
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "monthly"
|
|
8
|
+
# Wait 7 days after a release before opening an update PR. Mitigates
|
|
9
|
+
# supply-chain risk by giving time for malicious releases to be flagged
|
|
10
|
+
# and yanked before they propagate here.
|
|
11
|
+
cooldown:
|
|
12
|
+
default-days: 7
|
|
13
|
+
# Skip patch-level Action updates — they're almost always non-functional
|
|
14
|
+
# changes to the action's own README or CI. Security advisories fire on
|
|
15
|
+
# a separate channel regardless of this filter.
|
|
16
|
+
ignore:
|
|
17
|
+
- dependency-name: "*"
|
|
18
|
+
update-types: ["version-update:semver-patch"]
|
|
19
|
+
groups:
|
|
20
|
+
actions:
|
|
21
|
+
patterns: ["*"]
|
|
22
|
+
commit-message:
|
|
23
|
+
prefix: "ci"
|
|
24
|
+
labels:
|
|
25
|
+
- "dependencies"
|
|
26
|
+
- "github-actions"
|
|
27
|
+
open-pull-requests-limit: 5
|
|
28
|
+
|
|
29
|
+
# Python dependencies via uv (pyproject.toml + uv.lock).
|
|
30
|
+
- package-ecosystem: "uv"
|
|
31
|
+
directory: "/"
|
|
32
|
+
schedule:
|
|
33
|
+
interval: "monthly"
|
|
34
|
+
cooldown:
|
|
35
|
+
default-days: 7
|
|
36
|
+
groups:
|
|
37
|
+
python:
|
|
38
|
+
patterns: ["*"]
|
|
39
|
+
commit-message:
|
|
40
|
+
prefix: "deps"
|
|
41
|
+
labels:
|
|
42
|
+
- "dependencies"
|
|
43
|
+
- "python"
|
|
44
|
+
open-pull-requests-limit: 5
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Exercises `action.yml` against itself.
|
|
2
|
+
#
|
|
3
|
+
# A composite action is not covered by the Python test suite: its shell, its argument threading, and
|
|
4
|
+
# its output wiring only run in Actions. This workflow builds a throwaway MEDS dataset, runs the
|
|
5
|
+
# action in extract-only mode to produce a baseline, then runs it again in comparison mode -- once
|
|
6
|
+
# where it must pass and once, against a deliberately corrupted baseline, where it must fail.
|
|
7
|
+
|
|
8
|
+
name: Action self-test
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [main]
|
|
13
|
+
pull_request:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
selftest:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 20
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7.0.1
|
|
25
|
+
with:
|
|
26
|
+
persist-credentials: false
|
|
27
|
+
|
|
28
|
+
- name: Setup package
|
|
29
|
+
uses: ./.github/actions/setup
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
|
|
33
|
+
- name: Build a throwaway MEDS dataset
|
|
34
|
+
run: |
|
|
35
|
+
uv run build_sample_MEDS_dataset \
|
|
36
|
+
seed=1 N_subjects=200 do_overwrite=False \
|
|
37
|
+
output_dir=sample_dataset dataset_spec/data_generator=sample
|
|
38
|
+
|
|
39
|
+
- name: Extract a baseline
|
|
40
|
+
id: baseline
|
|
41
|
+
uses: ./
|
|
42
|
+
with:
|
|
43
|
+
meds-dir: sample_dataset
|
|
44
|
+
output: baseline.json
|
|
45
|
+
package-spec: .
|
|
46
|
+
upload-artifacts: "false"
|
|
47
|
+
|
|
48
|
+
- name: Baseline was produced
|
|
49
|
+
env:
|
|
50
|
+
STATS_PATH: ${{ steps.baseline.outputs.stats-path }}
|
|
51
|
+
run: test -s "$STATS_PATH"
|
|
52
|
+
|
|
53
|
+
- name: Compare against the baseline (must pass)
|
|
54
|
+
id: clean
|
|
55
|
+
uses: ./
|
|
56
|
+
with:
|
|
57
|
+
meds-dir: sample_dataset
|
|
58
|
+
baseline: baseline.json
|
|
59
|
+
output: current.json
|
|
60
|
+
package-spec: .
|
|
61
|
+
artifact-name: selftest-clean
|
|
62
|
+
|
|
63
|
+
- name: Comparison reported a pass
|
|
64
|
+
env:
|
|
65
|
+
STATUS: ${{ steps.clean.outputs.status }}
|
|
66
|
+
FAILED: ${{ steps.clean.outputs.failed }}
|
|
67
|
+
run: |
|
|
68
|
+
echo "status=$STATUS failed=$FAILED"
|
|
69
|
+
test "$STATUS" = "pass"
|
|
70
|
+
test "$FAILED" = "false"
|
|
71
|
+
|
|
72
|
+
- name: Corrupt the baseline
|
|
73
|
+
run: |
|
|
74
|
+
uv run python -c "
|
|
75
|
+
import json, pathlib
|
|
76
|
+
p = pathlib.Path('corrupted.json')
|
|
77
|
+
stats = json.loads(pathlib.Path('baseline.json').read_text())
|
|
78
|
+
stats['counts']['n_subjects'] = int(stats['counts']['n_subjects'] * 0.5)
|
|
79
|
+
p.write_text(json.dumps(stats))
|
|
80
|
+
"
|
|
81
|
+
|
|
82
|
+
- name: Compare against the corrupted baseline (must fail)
|
|
83
|
+
id: dirty
|
|
84
|
+
continue-on-error: true
|
|
85
|
+
uses: ./
|
|
86
|
+
with:
|
|
87
|
+
meds-dir: sample_dataset
|
|
88
|
+
baseline: corrupted.json
|
|
89
|
+
output: current2.json
|
|
90
|
+
report: report2.json
|
|
91
|
+
markdown: report2.md
|
|
92
|
+
package-spec: .
|
|
93
|
+
artifact-name: selftest-dirty
|
|
94
|
+
|
|
95
|
+
- name: Regression was detected and failed the step
|
|
96
|
+
env:
|
|
97
|
+
OUTCOME: ${{ steps.dirty.outcome }}
|
|
98
|
+
STATUS: ${{ steps.dirty.outputs.status }}
|
|
99
|
+
run: |
|
|
100
|
+
echo "outcome=$OUTCOME status=$STATUS"
|
|
101
|
+
test "$OUTCOME" = "failure"
|
|
102
|
+
test "$STATUS" = "fail"
|
|
103
|
+
grep -q "counts.n_subjects" report2.json
|
|
104
|
+
|
|
105
|
+
- name: fail-on never reports the regression without failing
|
|
106
|
+
id: advisory
|
|
107
|
+
uses: ./
|
|
108
|
+
with:
|
|
109
|
+
meds-dir: sample_dataset
|
|
110
|
+
baseline: corrupted.json
|
|
111
|
+
output: current3.json
|
|
112
|
+
report: report3.json
|
|
113
|
+
markdown: report3.md
|
|
114
|
+
fail-on: never
|
|
115
|
+
package-spec: .
|
|
116
|
+
artifact-name: selftest-advisory
|
|
117
|
+
|
|
118
|
+
- name: Advisory mode still reports the failure status
|
|
119
|
+
env:
|
|
120
|
+
STATUS: ${{ steps.advisory.outputs.status }}
|
|
121
|
+
FAILED: ${{ steps.advisory.outputs.failed }}
|
|
122
|
+
run: |
|
|
123
|
+
test "$STATUS" = "fail"
|
|
124
|
+
test "$FAILED" = "false"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Same as `code-quality-pr.yaml` but triggered on commit to main branch
|
|
2
|
+
# and runs on all files (instead of only the changed ones)
|
|
3
|
+
|
|
4
|
+
name: Code Quality Main
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
code-quality:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7.0.1
|
|
19
|
+
with:
|
|
20
|
+
persist-credentials: false
|
|
21
|
+
|
|
22
|
+
- name: Setup package
|
|
23
|
+
uses: ./.github/actions/setup
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
|
|
27
|
+
- name: Run pre-commits
|
|
28
|
+
run: uv run pre-commit run --all-files --show-diff-on-failure
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# This workflow finds which files were changed, prints them,
|
|
2
|
+
# and runs `pre-commit` on those files.
|
|
3
|
+
|
|
4
|
+
# Inspired by the sktime library:
|
|
5
|
+
# https://github.com/alan-turing-institute/sktime/blob/main/.github/workflows/test.yml
|
|
6
|
+
|
|
7
|
+
name: Code Quality PR
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
pull-requests: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
code-quality:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
|
|
25
|
+
- name: Setup package
|
|
26
|
+
uses: ./.github/actions/setup
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Find modified files
|
|
31
|
+
id: file_changes
|
|
32
|
+
uses: tj-actions/changed-files@v47.0.6
|
|
33
|
+
|
|
34
|
+
- name: List all changed files
|
|
35
|
+
env:
|
|
36
|
+
ALL_CHANGED_FILES: ${{ steps.file_changes.outputs.all_changed_files }}
|
|
37
|
+
run: echo "$ALL_CHANGED_FILES"
|
|
38
|
+
|
|
39
|
+
- name: Run pre-commits
|
|
40
|
+
env:
|
|
41
|
+
ALL_CHANGED_FILES: ${{ steps.file_changes.outputs.all_changed_files }}
|
|
42
|
+
run: |
|
|
43
|
+
# Intentional word splitting: the action output is a space-separated
|
|
44
|
+
# list of file paths.
|
|
45
|
+
# shellcheck disable=SC2086
|
|
46
|
+
uv run pre-commit run --show-diff-on-failure --files $ALL_CHANGED_FILES
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Reusable workflow: run the MEDS summary-stats regression check as its own job.
|
|
2
|
+
#
|
|
3
|
+
# Use this when you want the check to have separate logs, its own runner, and its own summary panel.
|
|
4
|
+
# Use the composite action in `action.yml` instead when you want it as a step inside a job that has
|
|
5
|
+
# already built the dataset. See docs/github-action.md.
|
|
6
|
+
#
|
|
7
|
+
# This invokes the CLIs directly rather than reusing `action.yml`. A reusable workflow cannot
|
|
8
|
+
# reference an action from its own repository at the ref it was called with -- it would have to
|
|
9
|
+
# hardcode one -- so calling this at a tag would silently run `main`'s action. Invoking the
|
|
10
|
+
# published package via `package-spec` keeps the version under the caller's control instead.
|
|
11
|
+
|
|
12
|
+
name: MEDS summary-stats regression
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
workflow_call:
|
|
16
|
+
inputs:
|
|
17
|
+
meds-dir:
|
|
18
|
+
description: Root of the MEDS dataset to summarize.
|
|
19
|
+
required: true
|
|
20
|
+
type: string
|
|
21
|
+
baseline:
|
|
22
|
+
description: Path to the baseline stats JSON. Leave empty to extract only.
|
|
23
|
+
required: false
|
|
24
|
+
default: ""
|
|
25
|
+
type: string
|
|
26
|
+
build-command:
|
|
27
|
+
description: >-
|
|
28
|
+
Shell command run before extraction, to produce the dataset (an ETL invocation, a
|
|
29
|
+
download, a decrypt). Leave empty if `meds-dir` already exists in the checkout.
|
|
30
|
+
required: false
|
|
31
|
+
default: ""
|
|
32
|
+
type: string
|
|
33
|
+
policy:
|
|
34
|
+
description: Path to a YAML comparison policy.
|
|
35
|
+
required: false
|
|
36
|
+
default: ""
|
|
37
|
+
type: string
|
|
38
|
+
extract-config:
|
|
39
|
+
description: Path to a YAML extraction config.
|
|
40
|
+
required: false
|
|
41
|
+
default: ""
|
|
42
|
+
type: string
|
|
43
|
+
fail-on:
|
|
44
|
+
description: "Minimum severity that fails the job: error, warning, or never."
|
|
45
|
+
required: false
|
|
46
|
+
default: error
|
|
47
|
+
type: string
|
|
48
|
+
on-dataset-version-change:
|
|
49
|
+
description: "ignore, warn, error, or relax."
|
|
50
|
+
required: false
|
|
51
|
+
default: warn
|
|
52
|
+
type: string
|
|
53
|
+
package-spec:
|
|
54
|
+
description: What to install, e.g. 'meds_summary_stats==0.1.0'. Pin it for reproducible CI.
|
|
55
|
+
required: false
|
|
56
|
+
default: meds_summary_stats
|
|
57
|
+
type: string
|
|
58
|
+
python-version:
|
|
59
|
+
description: Python version used to run the tool.
|
|
60
|
+
required: false
|
|
61
|
+
default: "3.12"
|
|
62
|
+
type: string
|
|
63
|
+
runs-on:
|
|
64
|
+
description: Runner label.
|
|
65
|
+
required: false
|
|
66
|
+
default: ubuntu-latest
|
|
67
|
+
type: string
|
|
68
|
+
timeout-minutes:
|
|
69
|
+
description: Job timeout.
|
|
70
|
+
required: false
|
|
71
|
+
default: 60
|
|
72
|
+
type: number
|
|
73
|
+
outputs:
|
|
74
|
+
status:
|
|
75
|
+
description: "pass, warn, or fail -- what was found."
|
|
76
|
+
value: ${{ jobs.check.outputs.status }}
|
|
77
|
+
failed:
|
|
78
|
+
description: "'true' if the findings breached fail-on -- what the policy makes of it."
|
|
79
|
+
value: ${{ jobs.check.outputs.failed }}
|
|
80
|
+
|
|
81
|
+
permissions:
|
|
82
|
+
contents: read
|
|
83
|
+
|
|
84
|
+
jobs:
|
|
85
|
+
check:
|
|
86
|
+
runs-on: ${{ inputs.runs-on }}
|
|
87
|
+
timeout-minutes: ${{ inputs.timeout-minutes }}
|
|
88
|
+
outputs:
|
|
89
|
+
status: ${{ steps.compare.outputs.status }}
|
|
90
|
+
failed: ${{ steps.compare.outputs.failed }}
|
|
91
|
+
|
|
92
|
+
steps:
|
|
93
|
+
- uses: actions/checkout@v7.0.1
|
|
94
|
+
with:
|
|
95
|
+
persist-credentials: false
|
|
96
|
+
|
|
97
|
+
- uses: actions/setup-python@v7.0.0
|
|
98
|
+
with:
|
|
99
|
+
python-version: ${{ inputs.python-version }}
|
|
100
|
+
|
|
101
|
+
- uses: astral-sh/setup-uv@v9.0.0
|
|
102
|
+
with:
|
|
103
|
+
enable-cache: true
|
|
104
|
+
cache-suffix: meds-summary-stats
|
|
105
|
+
|
|
106
|
+
- name: Build the dataset
|
|
107
|
+
if: inputs.build-command != ''
|
|
108
|
+
shell: bash
|
|
109
|
+
env:
|
|
110
|
+
BUILD_COMMAND: ${{ inputs.build-command }}
|
|
111
|
+
run: bash -euo pipefail -c "$BUILD_COMMAND"
|
|
112
|
+
|
|
113
|
+
# Inputs reach the shell through `env:` rather than `${{ }}` interpolation, so a value
|
|
114
|
+
# containing shell metacharacters is data, not code.
|
|
115
|
+
- name: Extract summary statistics
|
|
116
|
+
shell: bash
|
|
117
|
+
env:
|
|
118
|
+
MEDS_DIR: ${{ inputs.meds-dir }}
|
|
119
|
+
EXTRACT_CONFIG: ${{ inputs.extract-config }}
|
|
120
|
+
PACKAGE_SPEC: ${{ inputs.package-spec }}
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
args=("$MEDS_DIR" --output meds-summary-stats.json)
|
|
124
|
+
if [[ -n "$EXTRACT_CONFIG" ]]; then args+=(--config "$EXTRACT_CONFIG"); fi
|
|
125
|
+
uvx --from "$PACKAGE_SPEC" extract_MEDS_summary_stats "${args[@]}"
|
|
126
|
+
|
|
127
|
+
- name: Compare against baseline
|
|
128
|
+
id: compare
|
|
129
|
+
if: inputs.baseline != ''
|
|
130
|
+
shell: bash
|
|
131
|
+
env:
|
|
132
|
+
BASELINE: ${{ inputs.baseline }}
|
|
133
|
+
POLICY: ${{ inputs.policy }}
|
|
134
|
+
FAIL_ON: ${{ inputs.fail-on }}
|
|
135
|
+
ON_VERSION_CHANGE: ${{ inputs.on-dataset-version-change }}
|
|
136
|
+
PACKAGE_SPEC: ${{ inputs.package-spec }}
|
|
137
|
+
run: |
|
|
138
|
+
set -uo pipefail
|
|
139
|
+
|
|
140
|
+
if [[ ! -f "$BASELINE" ]]; then
|
|
141
|
+
echo "::error::Baseline file not found: $BASELINE"
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
args=("$BASELINE" meds-summary-stats.json
|
|
146
|
+
--output meds-summary-stats-report.json
|
|
147
|
+
--markdown meds-summary-stats-report.md
|
|
148
|
+
--fail-on "$FAIL_ON"
|
|
149
|
+
--on-dataset-version-change "$ON_VERSION_CHANGE")
|
|
150
|
+
if [[ -n "$POLICY" ]]; then args+=(--config "$POLICY"); fi
|
|
151
|
+
|
|
152
|
+
# The tool writes its markdown to $GITHUB_STEP_SUMMARY and its verdict to $GITHUB_OUTPUT
|
|
153
|
+
# itself. `set -e` is off deliberately: exit 1 means "regression found", which must not
|
|
154
|
+
# abort before artifacts upload. Anything above 1 is a genuine tool failure.
|
|
155
|
+
uvx --from "$PACKAGE_SPEC" compare_MEDS_summary_stats "${args[@]}"
|
|
156
|
+
code=$?
|
|
157
|
+
if (( code > 1 )); then
|
|
158
|
+
echo "::error::compare_MEDS_summary_stats exited with code $code"
|
|
159
|
+
exit "$code"
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
- name: Upload artifacts
|
|
163
|
+
if: ${{ !cancelled() }}
|
|
164
|
+
uses: actions/upload-artifact@v7.0.1
|
|
165
|
+
with:
|
|
166
|
+
name: meds-summary-stats
|
|
167
|
+
path: |
|
|
168
|
+
meds-summary-stats.json
|
|
169
|
+
meds-summary-stats-report.json
|
|
170
|
+
meds-summary-stats-report.md
|
|
171
|
+
if-no-files-found: warn
|
|
172
|
+
retention-days: 90
|
|
173
|
+
|
|
174
|
+
- name: Fail on regression
|
|
175
|
+
if: steps.compare.outputs.failed == 'true'
|
|
176
|
+
shell: bash
|
|
177
|
+
env:
|
|
178
|
+
STATUS: ${{ steps.compare.outputs.status }}
|
|
179
|
+
N_ERRORS: ${{ steps.compare.outputs.n_errors }}
|
|
180
|
+
N_WARNINGS: ${{ steps.compare.outputs.n_warnings }}
|
|
181
|
+
run: |
|
|
182
|
+
echo "::error::MEDS summary-stats comparison reported '${STATUS}'" \
|
|
183
|
+
"(${N_ERRORS} errors, ${N_WARNINGS} warnings). See the job summary for details."
|
|
184
|
+
exit 1
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
|
|
2
|
+
|
|
3
|
+
on: push
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Build distribution 📦
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.12"]
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
|
|
25
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
26
|
+
uses: actions/setup-python@v7.0.0
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
32
|
+
with:
|
|
33
|
+
enable-cache: true
|
|
34
|
+
|
|
35
|
+
- name: Build
|
|
36
|
+
run: uv build
|
|
37
|
+
|
|
38
|
+
- name: Store the distribution packages
|
|
39
|
+
uses: actions/upload-artifact@v7.0.1
|
|
40
|
+
with:
|
|
41
|
+
name: python-package-distributions
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
publish-to-pypi:
|
|
45
|
+
name: Publish Python 🐍 distribution 📦 to PyPI
|
|
46
|
+
if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
|
|
47
|
+
needs:
|
|
48
|
+
- build
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
environment:
|
|
51
|
+
name: pypi
|
|
52
|
+
url: https://pypi.org/p/meds_summary_stats
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write # IMPORTANT: mandatory for trusted publishing
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- name: Download all the dists
|
|
58
|
+
uses: actions/download-artifact@v8.0.1
|
|
59
|
+
with:
|
|
60
|
+
name: python-package-distributions
|
|
61
|
+
path: dist/
|
|
62
|
+
|
|
63
|
+
- name: Publish distribution 📦 to PyPI
|
|
64
|
+
uses: pypa/gh-action-pypi-publish@v1.14.0
|
|
65
|
+
|
|
66
|
+
github-release:
|
|
67
|
+
name: >-
|
|
68
|
+
Sign the Python 🐍 distribution 📦 with Sigstore
|
|
69
|
+
and upload them to GitHub Release
|
|
70
|
+
needs:
|
|
71
|
+
- publish-to-pypi
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
|
|
74
|
+
permissions:
|
|
75
|
+
contents: write # IMPORTANT: mandatory for making GitHub Releases
|
|
76
|
+
id-token: write # IMPORTANT: mandatory for sigstore
|
|
77
|
+
|
|
78
|
+
steps:
|
|
79
|
+
- name: Download all the dists
|
|
80
|
+
uses: actions/download-artifact@v8.0.1
|
|
81
|
+
with:
|
|
82
|
+
name: python-package-distributions
|
|
83
|
+
path: dist/
|
|
84
|
+
|
|
85
|
+
- name: Sign the dists with Sigstore
|
|
86
|
+
uses: sigstore/gh-action-sigstore-python@v3.4.0
|
|
87
|
+
with:
|
|
88
|
+
inputs: >-
|
|
89
|
+
./dist/*.tar.gz
|
|
90
|
+
./dist/*.whl
|
|
91
|
+
- name: Create GitHub Release
|
|
92
|
+
env:
|
|
93
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
94
|
+
REF_NAME: ${{ github.ref_name }}
|
|
95
|
+
REPOSITORY: ${{ github.repository }}
|
|
96
|
+
run: >-
|
|
97
|
+
gh release create
|
|
98
|
+
"$REF_NAME"
|
|
99
|
+
--repo "$REPOSITORY"
|
|
100
|
+
--generate-notes
|
|
101
|
+
- name: Upload artifact signatures to GitHub Release
|
|
102
|
+
env:
|
|
103
|
+
GITHUB_TOKEN: ${{ github.token }}
|
|
104
|
+
REF_NAME: ${{ github.ref_name }}
|
|
105
|
+
REPOSITORY: ${{ github.repository }}
|
|
106
|
+
# Upload to GitHub Release using the `gh` CLI.
|
|
107
|
+
# `dist/` contains the built packages, and the
|
|
108
|
+
# sigstore-produced signatures and certificates.
|
|
109
|
+
run: >-
|
|
110
|
+
gh release upload
|
|
111
|
+
"$REF_NAME" dist/**
|
|
112
|
+
--repo "$REPOSITORY"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
run_tests_ubuntu:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
|
|
18
|
+
timeout-minutes: 30
|
|
19
|
+
|
|
20
|
+
# `secrets` is not among the contexts available to a step-level `if`, so the token is hoisted
|
|
21
|
+
# into the job environment where the Codecov steps can test it.
|
|
22
|
+
env:
|
|
23
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7.0.1
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
|
|
30
|
+
- name: Setup package
|
|
31
|
+
uses: ./.github/actions/setup
|
|
32
|
+
with:
|
|
33
|
+
python-version: "3.12"
|
|
34
|
+
|
|
35
|
+
# No `--ignore=docs` here, unlike the template: this project's docs are hand-written and
|
|
36
|
+
# doctested, so skipping them in CI would let a documented example drift from real output.
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: >
|
|
39
|
+
uv run pytest -v
|
|
40
|
+
--cov=src
|
|
41
|
+
--cov-report=xml:coverage.xml
|
|
42
|
+
--cov-report=term
|
|
43
|
+
--junitxml=junit.xml
|
|
44
|
+
|
|
45
|
+
# Skip Codecov when there is no token to use. Two cases: Dependabot runs do not get
|
|
46
|
+
# repository secrets (a deliberate GitHub security policy), and a freshly created repository
|
|
47
|
+
# has not had its token configured yet. In both, uploading is impossible rather than broken,
|
|
48
|
+
# so failing the run would be reporting the wrong problem. `fail_ci_if_error` stays on, so a
|
|
49
|
+
# genuine upload failure with a token present is still loud.
|
|
50
|
+
- name: Upload coverage to Codecov
|
|
51
|
+
if: env.CODECOV_TOKEN != '' && github.actor != 'dependabot[bot]'
|
|
52
|
+
uses: codecov/codecov-action@v7.0.0
|
|
53
|
+
with:
|
|
54
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
55
|
+
files: coverage.xml
|
|
56
|
+
fail_ci_if_error: true
|
|
57
|
+
verbose: true
|
|
58
|
+
# Install the Codecov CLI from PyPI rather than downloading the prebuilt binary. The
|
|
59
|
+
# binary path GPG-verifies the download against a key fetched from a keyserver, and that
|
|
60
|
+
# fetch fails on GitHub-hosted runners ("gpg: no valid OpenPGP data found" ->
|
|
61
|
+
# "Can't check signature: No public key"), taking the whole job down with it. PyPI
|
|
62
|
+
# installation is verified by the index instead and does not depend on keyserver
|
|
63
|
+
# reachability.
|
|
64
|
+
use_pypi: true
|
|
65
|
+
|
|
66
|
+
- name: Upload test results to Codecov
|
|
67
|
+
if: ${{ !cancelled() && env.CODECOV_TOKEN != '' && github.actor != 'dependabot[bot]' }}
|
|
68
|
+
uses: codecov/test-results-action@v1.2.1
|
|
69
|
+
with:
|
|
70
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
71
|
+
files: junit.xml
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# zizmor configuration. See https://docs.zizmor.sh/configuration/
|
|
2
|
+
#
|
|
3
|
+
# `unpinned-uses`: zizmor's default policy requires every action reference to be
|
|
4
|
+
# a 40-char commit SHA. That is the strictest stance but high friction even with
|
|
5
|
+
# Dependabot. We relax it to allow tag-pinning (`ref-pin`) for publishers we
|
|
6
|
+
# explicitly trust, and require commit-hash-pinning (`hash-pin`) for everything
|
|
7
|
+
# else. Dependabot (.github/dependabot.yml) keeps the tag pins current.
|
|
8
|
+
rules:
|
|
9
|
+
unpinned-uses:
|
|
10
|
+
config:
|
|
11
|
+
policies:
|
|
12
|
+
# First-party GitHub-owned actions.
|
|
13
|
+
actions/*: ref-pin
|
|
14
|
+
github/*: ref-pin
|
|
15
|
+
# Astral (uv, ruff, ty).
|
|
16
|
+
astral-sh/*: ref-pin
|
|
17
|
+
# Codecov (coverage + test results upload).
|
|
18
|
+
codecov/*: ref-pin
|
|
19
|
+
# PyPA (Python Packaging Authority).
|
|
20
|
+
pypa/*: ref-pin
|
|
21
|
+
# Sigstore (Linux Foundation).
|
|
22
|
+
sigstore/*: ref-pin
|
|
23
|
+
# Pre-commit hook authors used in this repo's workflows.
|
|
24
|
+
rhysd/*: ref-pin
|
|
25
|
+
# tj-actions/changed-files (had a March-2025 supply-chain compromise;
|
|
26
|
+
# consider tightening to hash-pin if you want stricter security).
|
|
27
|
+
tj-actions/*: ref-pin
|
|
28
|
+
# Default for everything else: require commit-hash pinning.
|
|
29
|
+
"*": hash-pin
|