lme-python 0.1.4__tar.gz → 0.1.7__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.
- lme_python-0.1.7/.github/workflows/audit.yml +73 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/.github/workflows/benchmarks.yml +0 -61
- lme_python-0.1.7/.github/workflows/ci.yml +225 -0
- lme_python-0.1.7/.github/workflows/crate-publish-dry-run.yml +28 -0
- lme_python-0.1.7/.github/workflows/fuzz-smoke.yml +34 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/.github/workflows/python-release.yml +12 -12
- {lme_python-0.1.4 → lme_python-0.1.7}/.github/workflows/repo-metadata.yml +1 -1
- {lme_python-0.1.4 → lme_python-0.1.7}/.gitignore +3 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/BENCHMARKS.md +1 -1
- {lme_python-0.1.4 → lme_python-0.1.7}/CHANGELOG.md +31 -0
- lme_python-0.1.7/CONTRIBUTING.md +122 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/Cargo.lock +11 -11
- {lme_python-0.1.4 → lme_python-0.1.7}/Cargo.toml +22 -1
- {lme_python-0.1.4 → lme_python-0.1.7}/GUIDE.md +11 -8
- {lme_python-0.1.4 → lme_python-0.1.7}/PKG-INFO +5 -1
- {lme_python-0.1.4 → lme_python-0.1.7}/README.md +9 -5
- {lme_python-0.1.4 → lme_python-0.1.7}/RELEASING.md +24 -1
- lme_python-0.1.7/REPO_COMPLETION_BY_AREA.md +74 -0
- lme_python-0.1.7/benches/bench_load_production.rs +188 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/benches/bench_math.rs +41 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/COMPARISONS.md +309 -78
- lme_python-0.1.7/comparisons/categorical_anova.R +17 -0
- lme_python-0.1.7/comparisons/categorical_anova.jl +13 -0
- lme_python-0.1.7/comparisons/categorical_anova.py +16 -0
- lme_python-0.1.7/comparisons/categorical_anova.rs +24 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_cbpp.R +15 -5
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_cbpp.jl +3 -0
- lme_python-0.1.7/comparisons/glmm_cbpp.py +66 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_cbpp.rs +8 -4
- lme_python-0.1.7/comparisons/glmm_gaussian.R +29 -0
- lme_python-0.1.7/comparisons/glmm_gaussian.jl +29 -0
- lme_python-0.1.7/comparisons/glmm_gaussian.py +54 -0
- lme_python-0.1.7/comparisons/glmm_gaussian.rs +46 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_grouseticks.rs +1 -1
- lme_python-0.1.7/comparisons/lmer_weighted.R +28 -0
- lme_python-0.1.7/comparisons/lmer_weighted.jl +37 -0
- lme_python-0.1.7/comparisons/lmer_weighted.py +63 -0
- lme_python-0.1.7/comparisons/lmer_weighted.rs +51 -0
- lme_python-0.1.7/comparisons/lmm_dyestuff.py +33 -0
- lme_python-0.1.7/comparisons/lmm_pastes.py +47 -0
- lme_python-0.1.7/comparisons/lmm_penicillin.py +43 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/pyproject.toml +7 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/Cargo.lock +22 -21
- {lme_python-0.1.4 → lme_python-0.1.7}/python/Cargo.toml +3 -2
- {lme_python-0.1.4 → lme_python-0.1.7}/python/PYTHON_GUIDE.md +3 -2
- lme_python-0.1.7/python/examples/plotting_demo/.gitignore +6 -0
- lme_python-0.1.7/python/examples/plotting_demo/README.md +129 -0
- lme_python-0.1.7/python/examples/plotting_demo/compare_plots.py +310 -0
- lme_python-0.1.7/python/examples/plotting_demo/figure_specs.py +45 -0
- lme_python-0.1.7/python/examples/plotting_demo/find_rscript.py +92 -0
- lme_python-0.1.7/python/examples/plotting_demo/numeric_overlay.py +143 -0
- lme_python-0.1.7/python/examples/plotting_demo/paths.py +14 -0
- lme_python-0.1.7/python/examples/plotting_demo/plot_demo.py +185 -0
- lme_python-0.1.7/python/examples/plotting_demo/plot_r.R +177 -0
- lme_python-0.1.7/python/examples/plotting_demo/run_all.py +58 -0
- lme_python-0.1.7/python/examples/verification_project/README.md +42 -0
- lme_python-0.1.7/python/examples/verification_project/conftest.py +8 -0
- lme_python-0.1.7/python/examples/verification_project/parity.py +197 -0
- lme_python-0.1.7/python/examples/verification_project/paths.py +14 -0
- lme_python-0.1.7/python/examples/verification_project/run.py +39 -0
- lme_python-0.1.7/python/examples/verification_project/test_parity.py +41 -0
- lme_python-0.1.7/python/requirements-ci.txt +87 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/src/lib.rs +39 -6
- lme_python-0.1.7/scripts/local_ci.ps1 +85 -0
- lme_python-0.1.7/scripts/local_ci.sh +86 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/run_cross_language_benchmarks.py +19 -1
- lme_python-0.1.7/src/anova.rs +398 -0
- lme_python-0.1.7/src/anova_contrasts.rs +208 -0
- lme_python-0.1.7/src/ddf.rs +196 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/src/formula.rs +21 -0
- lme_python-0.1.7/src/glmm_math.rs +1217 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/src/lib.rs +198 -27
- {lme_python-0.1.4 → lme_python-0.1.7}/src/model_matrix.rs +255 -14
- {lme_python-0.1.4 → lme_python-0.1.7}/src/optimizer.rs +88 -64
- {lme_python-0.1.4 → lme_python-0.1.7}/src/robust.rs +8 -2
- {lme_python-0.1.4 → lme_python-0.1.7}/src/satterthwaite.rs +32 -22
- lme_python-0.1.7/tests/categorical_anova_test.rs +42 -0
- lme_python-0.1.7/tests/data/golden_parity_manifest.json +440 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/intercept_only.json +2 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/mock_ml.json +2 -0
- lme_python-0.1.7/tests/data/pastes_cask_reml.json +13 -0
- lme_python-0.1.7/tests/data/penicillin.csv +145 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/penicillin.json +2 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/random_slopes.json +2 -0
- lme_python-0.1.7/tests/generate_test_data.R +506 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_anova.rs +6 -6
- lme_python-0.1.7/tests/test_anova_types.rs +56 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_confint_simulate.rs +13 -2
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_coverage_edge_cases.rs +2 -1
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_coverage_gaps.rs +3 -0
- lme_python-0.1.7/tests/test_failure_modes.rs +222 -0
- lme_python-0.1.7/tests/test_formula_stress.rs +316 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_gaps.rs +10 -3
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_glmm.rs +96 -3
- lme_python-0.1.7/tests/test_golden_parity.rs +543 -0
- lme_python-0.1.7/tests/test_production_load.rs +360 -0
- lme_python-0.1.7/tests/test_statistical_identities.rs +294 -0
- lme_python-0.1.4/.github/workflows/ci.yml +0 -66
- lme_python-0.1.4/CONTRIBUTING.md +0 -91
- lme_python-0.1.4/comparisons/glmm_cbpp.py +0 -45
- lme_python-0.1.4/comparisons/lmm_dyestuff.py +0 -21
- lme_python-0.1.4/comparisons/lmm_pastes.py +0 -35
- lme_python-0.1.4/comparisons/lmm_penicillin.py +0 -35
- lme_python-0.1.4/src/anova.rs +0 -164
- lme_python-0.1.4/src/glmm_math.rs +0 -606
- lme_python-0.1.4/tests/data/penicillin.csv +0 -145
- lme_python-0.1.4/tests/generate_test_data.R +0 -231
- {lme_python-0.1.4 → lme_python-0.1.7}/LICENSE +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/comparison_chart.png +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_grouseticks.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_grouseticks.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/glmm_grouseticks.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_dyestuff.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_dyestuff.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_dyestuff.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_pastes.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_pastes.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_pastes.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_penicillin.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_penicillin.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/lmm_penicillin.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy_ml.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy_ml.jl +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy_ml.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/comparisons/sleepstudy_ml.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/docs/benchmarks/app.js +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/docs/benchmarks/index.html +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/docs/benchmarks/styles.css +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/docs/index.html +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/examples/glmer_cbpp.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/examples/glmer_grouseticks.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/examples/lm_basics.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/examples/lmer_sleepstudy.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/examples/model_comparison.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/python/tests/test_basic.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/ast_explorations/test_fiasto.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/ast_explorations/test_fiasto_offset.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/build_benchmark_site.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/dump_dyestuff.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/dump_pastes.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/make_penicillin_csv.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/plot_comparisons.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/scripts/sync_github_repo_metadata.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/src/family.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/src/kenward_roger.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/src/math.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/cbpp_binary.csv +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/dyestuff.csv +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/glmm_binomial.json +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/glmm_poisson.json +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/grouseticks.csv +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/mock_crossed.json +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/pastes.csv +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/data/sleepstudy.csv +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/generate_mock_data.py +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_conditional_real.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_crossed_mock.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_e2e_lmer.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_edge_cases.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_errors.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_formula.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_intercept_only.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_kenward_roger.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_ml_optimization.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_no_intercept.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_numerical_parity.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_predict.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_random_slopes.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_robust.rs +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_sandwich_math.R +0 -0
- {lme_python-0.1.4 → lme_python-0.1.7}/tests/test_satterthwaite.rs +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Security audit
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
schedule:
|
|
5
|
+
# Weekly: Mondays 12:00 UTC
|
|
6
|
+
- cron: "0 12 * * 1"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
push:
|
|
9
|
+
branches: [master]
|
|
10
|
+
paths:
|
|
11
|
+
- "**/Cargo.toml"
|
|
12
|
+
- "**/Cargo.lock"
|
|
13
|
+
- "python/pyproject.toml"
|
|
14
|
+
- "python/requirements-ci.txt"
|
|
15
|
+
pull_request:
|
|
16
|
+
branches: [master]
|
|
17
|
+
paths:
|
|
18
|
+
- "**/Cargo.toml"
|
|
19
|
+
- "**/Cargo.lock"
|
|
20
|
+
- "python/pyproject.toml"
|
|
21
|
+
- "python/requirements-ci.txt"
|
|
22
|
+
|
|
23
|
+
permissions:
|
|
24
|
+
contents: read
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: audit-${{ github.workflow }}-${{ github.ref }}
|
|
28
|
+
cancel-in-progress: true
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
audit:
|
|
32
|
+
name: cargo audit
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
|
|
37
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
38
|
+
|
|
39
|
+
- name: Install cargo-audit
|
|
40
|
+
uses: taiki-e/install-action@v2
|
|
41
|
+
with:
|
|
42
|
+
tool: cargo-audit@0.22.1
|
|
43
|
+
|
|
44
|
+
- name: Run cargo audit (root)
|
|
45
|
+
run: cargo audit
|
|
46
|
+
|
|
47
|
+
- name: Run cargo audit (python/)
|
|
48
|
+
working-directory: python
|
|
49
|
+
run: cargo audit
|
|
50
|
+
|
|
51
|
+
pip-audit:
|
|
52
|
+
name: pip-audit (python lockfile)
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v6
|
|
56
|
+
|
|
57
|
+
- name: Set up Python
|
|
58
|
+
uses: actions/setup-python@v6
|
|
59
|
+
with:
|
|
60
|
+
python-version: "3.11"
|
|
61
|
+
cache: pip
|
|
62
|
+
cache-dependency-path: python/requirements-ci.txt
|
|
63
|
+
|
|
64
|
+
- name: Install locked CI dependencies
|
|
65
|
+
working-directory: python
|
|
66
|
+
run: |
|
|
67
|
+
set -euo pipefail
|
|
68
|
+
python -m pip install -U pip
|
|
69
|
+
python -m pip install -r requirements-ci.txt
|
|
70
|
+
|
|
71
|
+
- name: pip-audit
|
|
72
|
+
working-directory: python
|
|
73
|
+
run: python -m pip_audit
|
|
@@ -17,7 +17,6 @@ on:
|
|
|
17
17
|
|
|
18
18
|
permissions:
|
|
19
19
|
contents: write
|
|
20
|
-
pages: write
|
|
21
20
|
id-token: write
|
|
22
21
|
|
|
23
22
|
env:
|
|
@@ -172,63 +171,3 @@ jobs:
|
|
|
172
171
|
echo "Cross-language benchmark script failed with exit code ${{ steps.cross_language_run.outputs.status }}." >&2
|
|
173
172
|
exit 1
|
|
174
173
|
|
|
175
|
-
pages:
|
|
176
|
-
name: Publish Benchmark Dashboard
|
|
177
|
-
runs-on: ubuntu-latest
|
|
178
|
-
needs:
|
|
179
|
-
- criterion
|
|
180
|
-
- cross-language
|
|
181
|
-
if: needs.criterion.result == 'success' && needs.cross-language.result == 'success' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))
|
|
182
|
-
environment:
|
|
183
|
-
name: github-pages
|
|
184
|
-
url: ${{ steps.deploy.outputs.page_url }}
|
|
185
|
-
steps:
|
|
186
|
-
- uses: actions/checkout@v6
|
|
187
|
-
|
|
188
|
-
- uses: actions/setup-python@v6
|
|
189
|
-
with:
|
|
190
|
-
python-version: "3.11"
|
|
191
|
-
|
|
192
|
-
- name: Download cross-language artifact
|
|
193
|
-
uses: actions/download-artifact@v5
|
|
194
|
-
with:
|
|
195
|
-
name: cross-language-${{ github.sha }}
|
|
196
|
-
path: site-input
|
|
197
|
-
|
|
198
|
-
- name: Resolve release URL
|
|
199
|
-
id: release_url
|
|
200
|
-
run: |
|
|
201
|
-
if [[ "${GITHUB_REF}" == refs/tags/* ]]; then
|
|
202
|
-
echo "value=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
|
203
|
-
else
|
|
204
|
-
echo "value=" >> "$GITHUB_OUTPUT"
|
|
205
|
-
fi
|
|
206
|
-
|
|
207
|
-
- name: Build benchmark dashboard
|
|
208
|
-
run: |
|
|
209
|
-
set -euo pipefail
|
|
210
|
-
mkdir -p site
|
|
211
|
-
cp -R docs/. site/
|
|
212
|
-
mkdir -p site/benchmarks/data
|
|
213
|
-
cross_json="$(find site-input -name 'cross-language-*.json' -print -quit)"
|
|
214
|
-
python scripts/build_benchmark_site.py \
|
|
215
|
-
--cross-language-json "$cross_json" \
|
|
216
|
-
--output-dir "site/benchmarks/data" \
|
|
217
|
-
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
|
|
218
|
-
--release-url "${{ steps.release_url.outputs.value }}" \
|
|
219
|
-
--criterion-asset-name "criterion-${GITHUB_SHA}.tar.gz" \
|
|
220
|
-
--cross-language-asset-name "cross-language-${GITHUB_SHA}.json" \
|
|
221
|
-
--ref-name "${GITHUB_REF_NAME}"
|
|
222
|
-
|
|
223
|
-
- uses: actions/configure-pages@v5
|
|
224
|
-
with:
|
|
225
|
-
token: ${{ secrets.REPO_ADMIN_TOKEN != '' && secrets.REPO_ADMIN_TOKEN || github.token }}
|
|
226
|
-
enablement: ${{ secrets.REPO_ADMIN_TOKEN != '' }}
|
|
227
|
-
|
|
228
|
-
- uses: actions/upload-pages-artifact@v3
|
|
229
|
-
with:
|
|
230
|
-
path: site
|
|
231
|
-
|
|
232
|
-
- name: Deploy to GitHub Pages
|
|
233
|
-
id: deploy
|
|
234
|
-
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- "**/*.md"
|
|
8
|
+
- "LICENSE"
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [master]
|
|
11
|
+
paths-ignore:
|
|
12
|
+
- "**/*.md"
|
|
13
|
+
- "LICENSE"
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
20
|
+
cancel-in-progress: true
|
|
21
|
+
|
|
22
|
+
env:
|
|
23
|
+
CARGO_TERM_COLOR: always
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
test:
|
|
27
|
+
name: Test (${{ matrix.os }})
|
|
28
|
+
runs-on: ${{ matrix.os }}
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
|
|
37
|
+
- name: Install Rust toolchain
|
|
38
|
+
uses: dtolnay/rust-toolchain@stable
|
|
39
|
+
|
|
40
|
+
- name: Install OpenBLAS (macOS)
|
|
41
|
+
if: matrix.os == 'macos-latest'
|
|
42
|
+
run: brew install openblas
|
|
43
|
+
|
|
44
|
+
- name: Install OpenBLAS (Ubuntu)
|
|
45
|
+
if: matrix.os == 'ubuntu-latest'
|
|
46
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
47
|
+
|
|
48
|
+
- name: Cache cargo registry & build
|
|
49
|
+
uses: actions/cache@v5
|
|
50
|
+
with:
|
|
51
|
+
path: |
|
|
52
|
+
~/.cargo/registry
|
|
53
|
+
~/.cargo/git
|
|
54
|
+
target
|
|
55
|
+
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
56
|
+
restore-keys: ${{ runner.os }}-cargo-
|
|
57
|
+
|
|
58
|
+
- name: Build
|
|
59
|
+
run: cargo build --verbose --locked
|
|
60
|
+
|
|
61
|
+
- name: Run tests
|
|
62
|
+
run: cargo test --verbose --locked
|
|
63
|
+
|
|
64
|
+
- name: Set up Python
|
|
65
|
+
uses: actions/setup-python@v6
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.11"
|
|
68
|
+
cache: pip
|
|
69
|
+
cache-dependency-path: python/requirements-ci.txt
|
|
70
|
+
|
|
71
|
+
- name: Python bindings (maturin develop, pytest, wheel install, pytest)
|
|
72
|
+
working-directory: python
|
|
73
|
+
shell: bash
|
|
74
|
+
run: |
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
python -m venv .venv
|
|
77
|
+
if [ -f .venv/Scripts/python.exe ]; then
|
|
78
|
+
VPY=.venv/Scripts/python.exe
|
|
79
|
+
else
|
|
80
|
+
VPY=.venv/bin/python
|
|
81
|
+
fi
|
|
82
|
+
"$VPY" -m pip install -U pip
|
|
83
|
+
"$VPY" -m pip install -r requirements-ci.txt
|
|
84
|
+
"$VPY" -m maturin develop --release
|
|
85
|
+
"$VPY" -m pytest tests/ -v
|
|
86
|
+
"$VPY" -m maturin build --release -o dist
|
|
87
|
+
shopt -s nullglob
|
|
88
|
+
wheels=(dist/lme_python-*.whl)
|
|
89
|
+
if [ ${#wheels[@]} -eq 0 ]; then
|
|
90
|
+
echo "No wheel found under python/dist"
|
|
91
|
+
ls -la dist 2>/dev/null || true
|
|
92
|
+
exit 1
|
|
93
|
+
fi
|
|
94
|
+
"$VPY" -m pip install --force-reinstall "${wheels[0]}"
|
|
95
|
+
"$VPY" -m pytest tests/ -v
|
|
96
|
+
|
|
97
|
+
rust-all-targets:
|
|
98
|
+
name: Rust check (all targets)
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v6
|
|
102
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
103
|
+
- name: Install OpenBLAS
|
|
104
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
105
|
+
- name: Cache cargo registry & build
|
|
106
|
+
uses: actions/cache@v5
|
|
107
|
+
with:
|
|
108
|
+
path: |
|
|
109
|
+
~/.cargo/registry
|
|
110
|
+
~/.cargo/git
|
|
111
|
+
target
|
|
112
|
+
key: ubuntu-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
113
|
+
restore-keys: ubuntu-cargo-
|
|
114
|
+
- name: cargo check --workspace --all-targets
|
|
115
|
+
run: cargo check --workspace --all-targets --locked -v
|
|
116
|
+
|
|
117
|
+
production-load:
|
|
118
|
+
name: Production load & perf gates (ubuntu, release)
|
|
119
|
+
runs-on: ubuntu-latest
|
|
120
|
+
steps:
|
|
121
|
+
- uses: actions/checkout@v6
|
|
122
|
+
|
|
123
|
+
- name: Install Rust toolchain
|
|
124
|
+
uses: dtolnay/rust-toolchain@stable
|
|
125
|
+
|
|
126
|
+
- name: Install OpenBLAS (Ubuntu)
|
|
127
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
128
|
+
|
|
129
|
+
- name: Cache cargo registry & build
|
|
130
|
+
uses: actions/cache@v5
|
|
131
|
+
with:
|
|
132
|
+
path: |
|
|
133
|
+
~/.cargo/registry
|
|
134
|
+
~/.cargo/git
|
|
135
|
+
target
|
|
136
|
+
key: ubuntu-prodload-${{ hashFiles('**/Cargo.lock') }}
|
|
137
|
+
restore-keys: ubuntu-prodload-
|
|
138
|
+
|
|
139
|
+
- name: Production load tests (smoke + release wall-clock gates)
|
|
140
|
+
run: cargo test --release --locked --verbose --test test_production_load
|
|
141
|
+
|
|
142
|
+
- name: Heavy production load tests (ignored)
|
|
143
|
+
run: cargo test --release --locked --verbose --test test_production_load -- --ignored --test-threads=1
|
|
144
|
+
|
|
145
|
+
rust-doctests:
|
|
146
|
+
name: Rust doctests
|
|
147
|
+
runs-on: ubuntu-latest
|
|
148
|
+
steps:
|
|
149
|
+
- uses: actions/checkout@v6
|
|
150
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
151
|
+
- name: Install OpenBLAS
|
|
152
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
153
|
+
- name: Cache cargo registry & build
|
|
154
|
+
uses: actions/cache@v5
|
|
155
|
+
with:
|
|
156
|
+
path: |
|
|
157
|
+
~/.cargo/registry
|
|
158
|
+
~/.cargo/git
|
|
159
|
+
target
|
|
160
|
+
key: ubuntu-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
161
|
+
restore-keys: ubuntu-cargo-
|
|
162
|
+
- name: cargo test --doc
|
|
163
|
+
run: cargo test --doc --locked --verbose
|
|
164
|
+
|
|
165
|
+
python-bindings-versions:
|
|
166
|
+
name: Python ${{ matrix.python-version }} (ubuntu)
|
|
167
|
+
runs-on: ubuntu-latest
|
|
168
|
+
strategy:
|
|
169
|
+
fail-fast: false
|
|
170
|
+
matrix:
|
|
171
|
+
python-version: ["3.10", "3.12", "3.13"]
|
|
172
|
+
steps:
|
|
173
|
+
- uses: actions/checkout@v6
|
|
174
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
175
|
+
- name: Install OpenBLAS
|
|
176
|
+
run: sudo apt-get update && sudo apt-get install -y libopenblas-dev
|
|
177
|
+
- name: Cache cargo registry & build
|
|
178
|
+
uses: actions/cache@v5
|
|
179
|
+
with:
|
|
180
|
+
path: |
|
|
181
|
+
~/.cargo/registry
|
|
182
|
+
~/.cargo/git
|
|
183
|
+
target
|
|
184
|
+
key: ubuntu-cargo-${{ hashFiles('**/Cargo.lock') }}
|
|
185
|
+
restore-keys: ubuntu-cargo-
|
|
186
|
+
- name: Set up Python
|
|
187
|
+
uses: actions/setup-python@v6
|
|
188
|
+
with:
|
|
189
|
+
python-version: ${{ matrix.python-version }}
|
|
190
|
+
cache: pip
|
|
191
|
+
cache-dependency-path: python/requirements-ci.txt
|
|
192
|
+
- name: Python bindings (maturin develop, pytest, wheel install, pytest)
|
|
193
|
+
working-directory: python
|
|
194
|
+
shell: bash
|
|
195
|
+
run: |
|
|
196
|
+
set -euo pipefail
|
|
197
|
+
python -m venv .venv
|
|
198
|
+
if [ -f .venv/Scripts/python.exe ]; then
|
|
199
|
+
VPY=.venv/Scripts/python.exe
|
|
200
|
+
else
|
|
201
|
+
VPY=.venv/bin/python
|
|
202
|
+
fi
|
|
203
|
+
"$VPY" -m pip install -U pip
|
|
204
|
+
"$VPY" -m pip install -r requirements-ci.txt
|
|
205
|
+
"$VPY" -m maturin develop --release
|
|
206
|
+
"$VPY" -m pytest tests/ -v
|
|
207
|
+
"$VPY" -m maturin build --release -o dist
|
|
208
|
+
shopt -s nullglob
|
|
209
|
+
wheels=(dist/lme_python-*.whl)
|
|
210
|
+
if [ ${#wheels[@]} -eq 0 ]; then
|
|
211
|
+
echo "No wheel found under python/dist"
|
|
212
|
+
ls -la dist 2>/dev/null || true
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
"$VPY" -m pip install --force-reinstall "${wheels[0]}"
|
|
216
|
+
"$VPY" -m pytest tests/ -v
|
|
217
|
+
|
|
218
|
+
docs:
|
|
219
|
+
name: Build docs
|
|
220
|
+
runs-on: ubuntu-latest
|
|
221
|
+
steps:
|
|
222
|
+
- uses: actions/checkout@v6
|
|
223
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
224
|
+
- name: Build documentation
|
|
225
|
+
run: cargo doc --no-deps --verbose --locked
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Crate publish (dry-run)
|
|
2
|
+
|
|
3
|
+
# Verifies `cargo publish` packaging (metadata, README, excluded files) without uploading.
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags:
|
|
8
|
+
- "v*"
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: publish-dry-run-${{ github.workflow }}-${{ github.ref }}
|
|
16
|
+
cancel-in-progress: true
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
dry-run:
|
|
20
|
+
name: cargo publish --dry-run
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
|
|
25
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
26
|
+
|
|
27
|
+
- name: Dry-run publish (root crate)
|
|
28
|
+
run: cargo publish --dry-run --locked
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Manual libFuzzer smoke (long compile + short run). Trigger from the Actions tab.
|
|
2
|
+
name: Fuzz (manual)
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
libfuzzer-smoke:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
timeout-minutes: 120
|
|
11
|
+
defaults:
|
|
12
|
+
run:
|
|
13
|
+
working-directory: fuzz
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: dtolnay/rust-toolchain@nightly
|
|
19
|
+
|
|
20
|
+
- name: Install cargo-fuzz
|
|
21
|
+
run: cargo install cargo-fuzz --locked
|
|
22
|
+
|
|
23
|
+
- uses: Swatinem/rust-cache@v2
|
|
24
|
+
with:
|
|
25
|
+
workspaces: fuzz
|
|
26
|
+
|
|
27
|
+
- name: Build fuzz targets
|
|
28
|
+
run: cargo fuzz build formula_parse && cargo fuzz build formula_pipeline
|
|
29
|
+
|
|
30
|
+
- name: Run formula_parse
|
|
31
|
+
run: cargo fuzz run formula_parse corpus/formula_parse -- -runs=15000 -max_len=16384
|
|
32
|
+
|
|
33
|
+
- name: Run formula_pipeline
|
|
34
|
+
run: cargo fuzz run formula_pipeline corpus/formula_parse -- -runs=15000 -max_len=16384
|
|
@@ -23,8 +23,8 @@ jobs:
|
|
|
23
23
|
matrix:
|
|
24
24
|
target: [x86_64, aarch64]
|
|
25
25
|
steps:
|
|
26
|
-
- uses: actions/checkout@
|
|
27
|
-
- uses: actions/setup-python@
|
|
26
|
+
- uses: actions/checkout@v6
|
|
27
|
+
- uses: actions/setup-python@v6
|
|
28
28
|
with:
|
|
29
29
|
python-version: '3.10'
|
|
30
30
|
- name: Build wheels
|
|
@@ -44,7 +44,7 @@ jobs:
|
|
|
44
44
|
CFLAGS: "-std=gnu99"
|
|
45
45
|
CFLAGS_aarch64_unknown_linux_gnu: "-std=gnu99"
|
|
46
46
|
- name: Upload wheels
|
|
47
|
-
uses: actions/upload-artifact@
|
|
47
|
+
uses: actions/upload-artifact@v7
|
|
48
48
|
with:
|
|
49
49
|
name: wheels-linux-${{ matrix.target }}
|
|
50
50
|
path: dist
|
|
@@ -55,8 +55,8 @@ jobs:
|
|
|
55
55
|
matrix:
|
|
56
56
|
target: [x64]
|
|
57
57
|
steps:
|
|
58
|
-
- uses: actions/checkout@
|
|
59
|
-
- uses: actions/setup-python@
|
|
58
|
+
- uses: actions/checkout@v6
|
|
59
|
+
- uses: actions/setup-python@v6
|
|
60
60
|
with:
|
|
61
61
|
python-version: '3.10'
|
|
62
62
|
architecture: ${{ matrix.target }}
|
|
@@ -67,7 +67,7 @@ jobs:
|
|
|
67
67
|
args: --release --out dist --manifest-path python/Cargo.toml
|
|
68
68
|
sccache: 'true'
|
|
69
69
|
- name: Upload wheels
|
|
70
|
-
uses: actions/upload-artifact@
|
|
70
|
+
uses: actions/upload-artifact@v7
|
|
71
71
|
with:
|
|
72
72
|
name: wheels-windows-${{ matrix.target }}
|
|
73
73
|
path: dist
|
|
@@ -78,8 +78,8 @@ jobs:
|
|
|
78
78
|
matrix:
|
|
79
79
|
target: [x86_64, aarch64]
|
|
80
80
|
steps:
|
|
81
|
-
- uses: actions/checkout@
|
|
82
|
-
- uses: actions/setup-python@
|
|
81
|
+
- uses: actions/checkout@v6
|
|
82
|
+
- uses: actions/setup-python@v6
|
|
83
83
|
with:
|
|
84
84
|
python-version: '3.10'
|
|
85
85
|
- name: Install OpenBLAS
|
|
@@ -91,7 +91,7 @@ jobs:
|
|
|
91
91
|
args: --release --out dist --manifest-path python/Cargo.toml
|
|
92
92
|
sccache: 'true'
|
|
93
93
|
- name: Upload wheels
|
|
94
|
-
uses: actions/upload-artifact@
|
|
94
|
+
uses: actions/upload-artifact@v7
|
|
95
95
|
with:
|
|
96
96
|
name: wheels-macos-${{ matrix.target }}
|
|
97
97
|
path: dist
|
|
@@ -99,14 +99,14 @@ jobs:
|
|
|
99
99
|
sdist:
|
|
100
100
|
runs-on: ubuntu-latest
|
|
101
101
|
steps:
|
|
102
|
-
- uses: actions/checkout@
|
|
102
|
+
- uses: actions/checkout@v6
|
|
103
103
|
- name: Build sdist
|
|
104
104
|
uses: PyO3/maturin-action@v1
|
|
105
105
|
with:
|
|
106
106
|
command: sdist
|
|
107
107
|
args: --out dist --manifest-path python/Cargo.toml
|
|
108
108
|
- name: Upload sdist
|
|
109
|
-
uses: actions/upload-artifact@
|
|
109
|
+
uses: actions/upload-artifact@v7
|
|
110
110
|
with:
|
|
111
111
|
name: wheels-sdist
|
|
112
112
|
path: dist
|
|
@@ -122,7 +122,7 @@ jobs:
|
|
|
122
122
|
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
|
|
123
123
|
contents: write # needed to create GitHub Release
|
|
124
124
|
steps:
|
|
125
|
-
- uses: actions/download-artifact@
|
|
125
|
+
- uses: actions/download-artifact@v7
|
|
126
126
|
with:
|
|
127
127
|
path: dist
|
|
128
128
|
pattern: wheels-*
|
|
@@ -15,6 +15,7 @@ It currently covers these benchmark families:
|
|
|
15
15
|
- end-to-end `lmer()` fitting on `sleepstudy`
|
|
16
16
|
- end-to-end weighted `lmer_weighted()` fitting on fixture-backed and large synthetic cases
|
|
17
17
|
- end-to-end `glmer()` fitting on `grouseticks` and `cbpp`
|
|
18
|
+
- `glmm_agq_cbpp`: same CBPP binomial fit with `n_agq = 1` (Laplace) vs `n_agq = 7` (scalar AGQ), lower sample size because AGQ is expensive
|
|
18
19
|
- population-level and conditional prediction on a larger repeated `sleepstudy` frame
|
|
19
20
|
- response-scale and conditional response-scale GLMM prediction on repeated Poisson and Binomial frames
|
|
20
21
|
- GLMM post-fit helpers such as Wald confidence intervals
|
|
@@ -122,7 +123,6 @@ The repository also includes a dedicated workflow in [.github/workflows/benchmar
|
|
|
122
123
|
- runs the cross-language benchmark script
|
|
123
124
|
- uploads the resulting artifacts in CI
|
|
124
125
|
- attaches them to GitHub Releases on tag pushes
|
|
125
|
-
- publishes the latest benchmark dashboard to GitHub Pages at `https://x4g4p3x.github.io/lme-rs/benchmarks/`
|
|
126
126
|
|
|
127
127
|
## Latest published results
|
|
128
128
|
|
|
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.7] - 2026-05-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Type II** fixed-effects ANOVA via `LmeFit::anova_typed(AnovaType::Type2, ddf)` ([`src/anova_contrasts.rs`](src/anova_contrasts.rs), `lmerTest`-style Doolittle contrasts for contained terms). `anova(ddf)` remains Type III. Python: `fit.anova("satterthwaite", anova_type="II")`.
|
|
15
|
+
- Design-matrix support for **`FixedEffect`** / **`InteractionTerm`** fiasto roles and two-way categorical interactions (`y ~ a * b`).
|
|
16
|
+
- Golden parity case **`pastes_cask_multi_dof_reml`** in [`tests/data/golden_parity_manifest.json`](tests/data/golden_parity_manifest.json) (Type III Satterthwaite ANOVA for `cask` on the pastes dataset), with fixture [`tests/data/pastes_cask_reml.json`](tests/data/pastes_cask_reml.json) and R regeneration via [`tests/generate_test_data.R`](tests/generate_test_data.R).
|
|
17
|
+
- Numeric parity checks for categorical multi-DoF ANOVA in [`tests/categorical_anova_test.rs`](tests/categorical_anova_test.rs) and [`python/examples/verification_project/parity.py`](python/examples/verification_project/parity.py).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Multi-DoF Type III ANOVA denominator df for Satterthwaite now follows **`lmerTest::contestMD()`** ([`src/ddf.rs`](src/ddf.rs)): eigen-decomposed Wald contrasts, per-direction Satterthwaite dfs, and `get_Fstat_ddf()` pooling (replacing `min()` of marginal dfs). Kenward–Roger multi-DoF rows use the same pooling on marginal dfs.
|
|
22
|
+
|
|
23
|
+
## [0.1.6] - 2026-03-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Python `examples/plotting_demo`: matplotlib figures from `lme_python`, optional **lme4** parity (`plot_r.R`), side-by-side PNGs (`figures_compare/`), numeric overlay on shared axes (`figures_overlay/` via `figures_data/` JSON/CSV), `run_all.py`, Windows `Rscript` discovery (`find_rscript.py`), and raster fallback (`figures_overlay_raster/`).
|
|
28
|
+
- `scripts/local_ci.sh` and `scripts/local_ci.ps1` to run the same checks as [`.github/workflows/ci.yml`](.github/workflows/ci.yml) locally (`fmt`, `clippy`, build, test, `doc`).
|
|
29
|
+
|
|
30
|
+
## [0.1.5] - 2026-03-22
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Added automatic categorical dummy encoding (dropping collinear intercepts) directly from categorical/string Polars dataframe columns natively.
|
|
35
|
+
- Implemented Multi-DoF Joint Wald F-Tests tracking dummy groups in `anova()` (Type III Satterthwaite representations) for valid categorical fixed-effect testing.
|
|
36
|
+
- Added support for generating `Gaussian` families in Python's `glmer` mapping, alongside exposing an equivalent `lmer_weighted` function.
|
|
37
|
+
- Plumbed explicit Adaptive Gauss-Hermite Quadrature (`nAGQ`) scaling configuration arrays throughout the optimizer targeting upcoming structural convergence capabilities.
|
|
38
|
+
|
|
8
39
|
## [0.1.4] - 2026-03-22
|
|
9
40
|
|
|
10
41
|
### Added
|