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.
Files changed (72) hide show
  1. meds_summary_stats-0/.github/actions/setup/action.yaml +30 -0
  2. meds_summary_stats-0/.github/dependabot.yml +44 -0
  3. meds_summary_stats-0/.github/workflows/action-selftest.yaml +124 -0
  4. meds_summary_stats-0/.github/workflows/code-quality-main.yaml +28 -0
  5. meds_summary_stats-0/.github/workflows/code-quality-pr.yaml +46 -0
  6. meds_summary_stats-0/.github/workflows/meds-stats-regression.yaml +184 -0
  7. meds_summary_stats-0/.github/workflows/python-build.yaml +112 -0
  8. meds_summary_stats-0/.github/workflows/tests.yaml +71 -0
  9. meds_summary_stats-0/.github/zizmor.yml +29 -0
  10. meds_summary_stats-0/.gitignore +216 -0
  11. meds_summary_stats-0/.pre-commit-config.yaml +155 -0
  12. meds_summary_stats-0/.python-version +1 -0
  13. meds_summary_stats-0/AGENTS.md +111 -0
  14. meds_summary_stats-0/CLAUDE.md +111 -0
  15. meds_summary_stats-0/CONTRIBUTORS.md +286 -0
  16. meds_summary_stats-0/LICENSE +21 -0
  17. meds_summary_stats-0/PKG-INFO +443 -0
  18. meds_summary_stats-0/README.md +425 -0
  19. meds_summary_stats-0/action.yml +192 -0
  20. meds_summary_stats-0/conftest.py +45 -0
  21. meds_summary_stats-0/docs/github-action.md +250 -0
  22. meds_summary_stats-0/docs/stats-surface.md +337 -0
  23. meds_summary_stats-0/examples/build_datasets.py +294 -0
  24. meds_summary_stats-0/examples/build_stats.py +61 -0
  25. meds_summary_stats-0/examples/datasets/v1_baseline/data/held_out/0.parquet +0 -0
  26. meds_summary_stats-0/examples/datasets/v1_baseline/data/train/0.parquet +0 -0
  27. meds_summary_stats-0/examples/datasets/v1_baseline/data/tuning/0.parquet +0 -0
  28. meds_summary_stats-0/examples/datasets/v1_baseline/metadata/codes.parquet +0 -0
  29. meds_summary_stats-0/examples/datasets/v1_baseline/metadata/dataset.json +7 -0
  30. meds_summary_stats-0/examples/datasets/v1_baseline/metadata/subject_splits.parquet +0 -0
  31. meds_summary_stats-0/examples/datasets/v1_etl_regression/data/held_out/0.parquet +0 -0
  32. meds_summary_stats-0/examples/datasets/v1_etl_regression/data/train/0.parquet +0 -0
  33. meds_summary_stats-0/examples/datasets/v1_etl_regression/data/tuning/0.parquet +0 -0
  34. meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/codes.parquet +0 -0
  35. meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/dataset.json +7 -0
  36. meds_summary_stats-0/examples/datasets/v1_etl_regression/metadata/subject_splits.parquet +0 -0
  37. meds_summary_stats-0/examples/datasets/v2_data_refresh/data/held_out/0.parquet +0 -0
  38. meds_summary_stats-0/examples/datasets/v2_data_refresh/data/train/0.parquet +0 -0
  39. meds_summary_stats-0/examples/datasets/v2_data_refresh/data/tuning/0.parquet +0 -0
  40. meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/codes.parquet +0 -0
  41. meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/dataset.json +7 -0
  42. meds_summary_stats-0/examples/datasets/v2_data_refresh/metadata/subject_splits.parquet +0 -0
  43. meds_summary_stats-0/examples/policy.yaml +121 -0
  44. meds_summary_stats-0/examples/stats/v1_baseline.json +318 -0
  45. meds_summary_stats-0/examples/stats/v1_etl_regression.json +318 -0
  46. meds_summary_stats-0/examples/stats/v2_data_refresh.json +318 -0
  47. meds_summary_stats-0/pyproject.toml +106 -0
  48. meds_summary_stats-0/setup.cfg +4 -0
  49. meds_summary_stats-0/src/meds_summary_stats/__init__.py +10 -0
  50. meds_summary_stats-0/src/meds_summary_stats/__main__.py +10 -0
  51. meds_summary_stats-0/src/meds_summary_stats/cli.py +415 -0
  52. meds_summary_stats-0/src/meds_summary_stats/compare.py +644 -0
  53. meds_summary_stats-0/src/meds_summary_stats/config.py +204 -0
  54. meds_summary_stats-0/src/meds_summary_stats/digests.py +156 -0
  55. meds_summary_stats-0/src/meds_summary_stats/extract.py +1484 -0
  56. meds_summary_stats-0/src/meds_summary_stats/flatten.py +186 -0
  57. meds_summary_stats-0/src/meds_summary_stats/ontology.py +193 -0
  58. meds_summary_stats-0/src/meds_summary_stats/policy.py +459 -0
  59. meds_summary_stats-0/src/meds_summary_stats/report.py +249 -0
  60. meds_summary_stats-0/src/meds_summary_stats.egg-info/PKG-INFO +443 -0
  61. meds_summary_stats-0/src/meds_summary_stats.egg-info/SOURCES.txt +70 -0
  62. meds_summary_stats-0/src/meds_summary_stats.egg-info/dependency_links.txt +1 -0
  63. meds_summary_stats-0/src/meds_summary_stats.egg-info/entry_points.txt +3 -0
  64. meds_summary_stats-0/src/meds_summary_stats.egg-info/requires.txt +3 -0
  65. meds_summary_stats-0/src/meds_summary_stats.egg-info/scm_file_list.json +66 -0
  66. meds_summary_stats-0/src/meds_summary_stats.egg-info/scm_version.json +8 -0
  67. meds_summary_stats-0/src/meds_summary_stats.egg-info/top_level.txt +1 -0
  68. meds_summary_stats-0/tests/.gitkeep +0 -0
  69. meds_summary_stats-0/tests/test_examples.py +142 -0
  70. meds_summary_stats-0/tests/test_integration.py +441 -0
  71. meds_summary_stats-0/tests/test_properties.py +395 -0
  72. 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