-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathpyproject.toml
More file actions
489 lines (451 loc) · 20.4 KB
/
Copy pathpyproject.toml
File metadata and controls
489 lines (451 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find]
where = ["src"]
[project]
name = "tsbootstrap"
version = "0.7.1"
description = "A Python package to generate bootstrapped time series"
maintainers = [
{ name = "Sankalp Gilda", email = "sankalp.gilda@gmail.com" },
{ name = "Franz Kiraly", email = "franz.kiraly@sktime.net"},
{ name = "Benedikt Heidrich", email = "benedikt.heidrich@sktime.net"},
]
authors = [
{ name = "Sankalp Gilda", email = "sankalp.gilda@gmail.com" },
]
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10,<3.14"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"Intended Audience :: Developers",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: Unix",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"numpy",
"scipy",
"pydantic",
"scikit-base", # base class for the sktime/skbase adapters
"narwhals", # DataFrame boundary: accept pandas/Polars/PyArrow frames
]
[project.optional-dependencies]
# ARIMA bootstraps fit with statsmodels (its MA/MLE path). AR / VAR / sieve are
# pure-numpy OLS and need no optional dependency.
models = [
"statsmodels",
]
# Out-of-bag uncertainty quantification (EnbPI) needs an estimator interface.
uq = [
"scikit-learn",
]
# Read-only MCP server (src/tsbootstrap/mcp.py). Layers the official MCP Python SDK
# on top of CORE tsbootstrap only (it does NOT pull the models or uq extras): the two
# tools are observation-resampling bootstraps, which are pure-numpy.
mcp = [
"mcp>=1.28,<2",
]
# Compiled acceleration: a numba kernel for the VAR recurrence (replicate-parallel).
# Pure-numpy is the default; installing this auto-selects the faster kernel.
accel = [
"numba",
]
# Educational notebook suite (issue #46): everything needed to RUN the tutorials,
# locally or in CI. Volatility clustering is shown on a synthetic GARCH series, so
# no financial-data dependency (e.g. arch) is required.
examples = [
"tsbootstrap[models,uq]", # statsmodels (ARIMA/VAR/sieve) + scikit-learn (EnbPI)
"matplotlib", # failure-mode histograms + the UQ interval plots
"pandas", # bundled-dataset loaders + the narwhals boundary
"sktime", # load_airline / load_lynx / load_shampoo_sales loaders
"jupyter", # ipykernel / nbconvert for notebook execution
]
# Profiling harness to find hot paths / suboptimal spots. See profiling/README.md.
profile = [
"scalene",
"line_profiler",
"memory_profiler",
"py-spy",
"snakeviz",
]
docs = [
"furo",
"jupyter",
"myst-parser",
"nbsphinx>=0.8.6",
"numpydoc",
"pydata-sphinx-theme",
"Sphinx!=7.2.0,<8.0.0",
"sphinx-rtd-theme>=1.3.0",
"sphinx-copybutton>=0.5.2",
"sphinx-design<0.6.0",
"sphinx-gallery<0.15.0",
"sphinx-issues<4.0.0",
"sphinx-version-warning",
"tabulate>=0.9.0",
]
dev = [
"hypothesis[numpy]", # property-based tests + numpy array strategies
"hypothesis-crosshair", # CrossHair concolic backend for symbolic property checks
"pre-commit",
"pytest",
"pytest-xdist[psutil]",
"pytest-cov",
"pytest-mock",
"pytest-timeout", # hang guard for the mutation ratchet (kills a non-terminating mutant)
"nbmake", # execute the tutorial notebooks as a CI test gate (issue #46)
"sktime", # validates the skbase/sktime adapter discovery + check_estimator
"polars", # exercises the narwhals DataFrame boundary with a second backend
"statsmodels", # model-based methods under test
"arch", # reference cross-check for PWSD + block bootstrap, and the head-to-head speed benchmark (benchmarks/bench_vs_arch.py)
"pandas", # narwhals boundary tests
"scikit-learn", # UQ (EnbPI) tests
"numba", # exercises the compiled VAR [accel] kernel
"asv", # performance benchmark suite
"mutmut>=3", # mutation-score ratchet (3.x: no SQLite cache, copies to mutants/; see [tool.mutmut])
"github-actions",
"importlib-metadata",
"pip-tools",
"mypy",
"pyright",
"ruff",
"autoflake",
"typos",
"tox",
"tox-gh-actions",
"pycobertura",
"tomlkit",
"memory-profiler>=0.60.0", # For performance testing
]
[project.scripts]
# `uvx tsbootstrap-mcp` runs the read-only MCP server over stdio.
tsbootstrap-mcp = "tsbootstrap.mcp:main"
[tool.uv]
# Supply-chain cooldown: refuse any distribution published within the last 7 days, so a
# compromised release must survive a week of public scrutiny (registry quarantine, OSV
# malware records) before it can enter the lockfile. Applies to every `uv lock` and
# lock-updating `uv sync`, locally and in CI. To adopt an urgent security fix younger
# than the window, add a scoped override instead of lifting the cooldown:
# exclude-newer-package = { <package> = "<RFC 3339 timestamp after the fix's upload>" }
# and remove the override once the release ages past the window. See CONTRIBUTING.md.
exclude-newer = "7 days"
# Pin the resolver itself: CI's setup-uv reads this instead of falling back to "latest",
# so a fresh uv release cannot change resolution or run unvetted code in CI minutes after
# publication. Bump deliberately, in its own commit, alongside a green `uv lock --check`.
required-version = "==0.11.28"
[tool.pytest.ini_options]
minversion = "6.0"
pythonpath = ["."] # repo root on sys.path so tests can import the shared tests._helpers package
addopts = "-ra -q -n auto --dist loadscope --max-worker-restart 3 --durations=20"
testpaths = [
"tests",
]
markers = [
"smoke: marks tests for smoke testing core functionality",
"anyio: marks tests that use anyio for async testing",
"slow: marks tests that are slow on Windows due to numerical computation performance",
"ci_performance: marks performance tests that are flaky in CI due to runner variability",
"performance: marks tests as performance benchmarks",
"integration: marks tests as integration tests",
"network: marks tests as requiring network access",
"cloud: marks tests as requiring cloud resources",
"gpu: marks tests as requiring GPU",
]
filterwarnings = [
# Guard against pkg_resources deprecation warnings (setuptools >= 81) emitted by the
# `fs` package if it is ever pulled in transitively; harmless no-ops otherwise.
"ignore:pkg_resources is deprecated.*:DeprecationWarning:fs",
"ignore:pkg_resources is deprecated.*:UserWarning:fs",
"ignore:Deprecated call to.*:DeprecationWarning:pkg_resources",
]
[tool.ruff]
target-version = 'py310'
line-length = 100 # Must agree with Black
[tool.ruff.lint]
select = [
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"D", # pydocstyle
"E", # Error
"F", # pyflakes
"I", # isort
"ISC", # flake8-implicit-str-concat
"N", # pep8-naming
"PGH", # pygrep-hooks
"PTH", # flake8-use-pathlib
"Q", # flake8-quotes
"S", # bandit
"SIM", # flake8-simplify
"TRY", # tryceratops
"UP", # pyupgrade
"W", # pycodestyle warnings
"YTT", # flake8-2020
]
exclude = [
"migrations",
"__pycache__",
"manage.py",
"settings.py",
"env",
".env",
"venv",
".venv",
]
ignore = [
"B905", # zip strict=true; remove once python <3.10 support is dropped.
"C901", # function is too complex; overly strict
"UP006", # Use `list` instead of `List` for type annotation (3.9+ style)
"UP007", # Use `X | Y` for type annotations (3.10+ only)
"UP035", # Import from `collections.abc` instead of `typing` (3.9+ style)
"UP037", # Remove quotes from type annotation (3.9+ style)
"UP038", # Use `X | Y` in isinstance (3.10+ only)
"UP039", # Unnecessary parentheses after not (3.9+ style)
"UP040", # Type alias should use TypeAlias (3.10+ only)
"UP045", # Use `X | None` for type annotations (3.10+ only)
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"D200",
"D401",
"E402",
"E501", # line length handled by ruff format
"N802", # Function name should be lowercase; overly strict
"N803", # Argument name should be lowercase; overly strict
"N806", # Variable in function should be lowercase; overly strict
"N816", # Variable in class scope should not be mixedCase; overly strict
"PGH003", # Use of "eval"; overly strict
"SIM115", # open-file-with-context-handler; overly strict
"TRY003", # Avoid specifying messages outside exception class; overly strict, especially for ValueError
"UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)`; overly strict
"UP007", # Use `X | Y` for type annotationsRuffUP007; overly strict
"UP006", # Use `list` instead of `List` for type annotations; overly strict
"UP035", # `typing.List` is deprecated, use `list` instead; overly strict
]
[tool.ruff.lint.isort]
order-by-type = true
relative-imports-order = "closest-to-furthest"
extra-standard-library = ["typing"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
known-first-party = []
[tool.ruff.lint.flake8-bugbear]
extend-immutable-calls = [
"chr",
"typer.Argument",
"typer.Option",
]
[tool.ruff.lint.pydocstyle]
convention = "numpy"
[tool.ruff.lint.per-file-ignores]
"tests/*.py" = [
"D100",
"D101",
"D102",
"D103",
"D104",
"D105",
"D106",
"D107",
"S101", # use of "assert"
"S102", # use of "exec"
"S106", # possible hardcoded password.
"S603", # subprocess call (controlled sys.executable invocations in tests)
"PGH001", # use of "eval"
]
# Tutorial notebooks are didactic: assert is a legitimate teaching/contract-check idiom.
"docs/source/tutorials/*.ipynb" = [
"S101", # use of "assert"
]
# Benchmark scripts legitimately shell out to git (SHA provenance) with a fixed,
# trusted argument list; the bandit subprocess warnings are noise there.
"benchmarks/*.py" = [
"S603", # subprocess call (fixed, trusted git argument list)
"S607", # partial executable path ("git" resolved from PATH)
]
[tool.ruff.lint.pep8-naming]
staticmethod-decorators = [
"pydantic.validator",
"pydantic.root_validator",
]
[tool.interrogate]
ignore_init_module = true
ignore_init_class = true
ignore_magic = true
ignore_semiprivate = true
ignore_private = true
ignore_nested_functions = true
ignore_nested_classes = true
ignore_imports = false
exclude = [".venv/*", "tests/*", "docs/*", "build/*", "dist/*", "src/tsbootstrap/__init__.py"]
[tool.coverage.run]
source = ['src/']
branch = true
omit = ['tests/*', '.venv/*']
[tool.coverage.report]
precision = 1
show_missing = true
skip_empty = true
exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == \"__main__\":",
]
[tool.mypy]
# Strict-minus typing gate for the tsbootstrap library (arch-style).
# warn_return_any is DELIBERATELY OMITTED: it forces no-op np.asarray copies on
# numpy-stub Any returns, which we do not want. The numpy.typing.mypy_plugin is also
# omitted: NumPy >= 2.3 ships inline types, so the plugin is a no-op and only emits a
# deprecation warning.
python_version = "3.10"
files = ["src/tsbootstrap"]
disallow_untyped_defs = true
disallow_incomplete_defs = true
strict_equality = true
no_implicit_optional = true
ignore_missing_imports = true
disallow_any_generics = true
[[tool.mypy.overrides]]
# Tests are not held to the untyped-def gate.
module = ["tests.*", "conftest"]
disallow_untyped_defs = false
disallow_incomplete_defs = false
[tool.pyright]
include = ["src"]
exclude = ["**/__pycache__", "**/.pytest_cache", "build", "dist", ".venv", "venv"]
pythonVersion = "3.10"
pythonPlatform = "All"
typeCheckingMode = "strict"
reportMissingImports = false
# Third-party libraries ship no type stubs (statsmodels, sklearn, skbase, numba,
# narwhals, arch); silence the missing-stub reports rather than littering ignores.
reportMissingTypeStubs = false
reportMissingModuleSource = false
reportPrivateUsage = false
stubPath = "typings"
venvPath = "."
venv = ".venv"
# --- Narrow strict relaxations for genuinely-untyped third-party / architecture --- #
# numpy, scikit-learn, skbase, numba, narwhals and arch ship partial or no type stubs.
# Under strict mode every value that flows through their APIs surfaces as Unknown/Any
# and pyright propagates it transitively. Suppressing these three "unknown type" reports
# is the documented way to keep a strict gate over OUR code without forcing a cast on
# every numpy expression (which the typing policy explicitly forbids).
reportUnknownVariableType = false
reportUnknownMemberType = false
reportUnknownArgumentType = false
# numba's @njit is an untyped decorator (engines/var.py compiled VAR kernel).
reportUntypedFunctionDecorator = false
# skbase.BaseObject.get_test_params is untyped (its base return is inferred as a bare
# dict), so the concrete, precisely-typed override cannot match it under strict mode.
reportIncompatibleMethodOverride = false
# Executors/preparers are reached only through the spec-type dispatch registry
# (@register_executor / @register_preparer); pyright cannot see the decorator store
# them, and engine submodules are imported purely for that registration side effect.
reportUnusedFunction = false
reportUnusedImport = false
# The dispatch registry deliberately uses lazy cross-module imports (api <-> engines)
# to register executors without an eager import cycle at module load.
reportImportCycles = false
# isinstance guards validate object-/union-typed inputs at public and dispatch
# boundaries; pyright's static narrowing flags these defensive checks as redundant.
reportUnnecessaryIsInstance = false
# "X" (design matrix) and import-guard flags like "_HAVE_NUMBA" are uppercase by the
# universal ML / feature-flag convention, not module-level constants.
reportConstantRedefinition = false
[tool.mutmut]
# STATUS (2026-06-24): in-process mutmut is NOT a blocking CI gate. Root cause, proven this session
# and reproduced on a clean 16-core box (not CPU contention): mutmut 3.x runs pytest IN-PROCESS via
# repeated pytest.main(), and numba's @njit(parallel=True) kernels (engines/var.py + block/_compiled.py)
# DEADLOCK in LLVM finalize_object/parse_assembly when that compile recurs inside one interpreter, so
# the stats pass times out and the run aborts before any mutant is tested. NUMBA_DISABLE_JIT=1 avoids
# the deadlock but runs the kernels interpreted, which then blows the per-test timeout. The locked path
# (cross-family debate 2026-06-24) is a Layer-2 subprocess-per-mutant runner: keep mutmut ONLY for AST
# mutant generation, execute each mutant in an isolated subprocess so LLVM never churns in one process,
# run nightly/non-blocking, gate on the new-survivor diff vs tests/mutation_equivalents.md. The config
# below + tools/mutmut_sitecustomize remain usable for a one-off LOCAL inspection on an idle box, but
# the durable value already landed is the killing tests + the equivalents registry, not this in-process
# runner. See ~/.claude/skills/mutation-ratchet and the debate transcript for the subprocess design.
#
# Mutation-score ratchet over the engine + model core (the established baseline scope).
# Run with: PYTHONPATH=tools/mutmut_sitecustomize HYPOTHESIS_PROFILE=mutmut uv run mutmut run
# - PYTHONPATH=tools/mutmut_sitecustomize (REQUIRED): mutate_only_covered_lines below makes mutmut
# run an in-process coverage pass and then UNLOAD every module imported during it
# (mutmut.code_coverage._unload_modules_not_in) so the source reloads fresh per mutant. That
# unload is fatal to C-extension libraries that cannot re-import in the same process: numpy
# ("cannot load module more than once per process" from _multiarray_umath) and numba
# ("duplicate registration for PolynomialType"). The sitecustomize on this path patches the
# unload to keep numpy/numba/scipy/statsmodels/... resident while still reloading tsbootstrap.
# It is gated on HYPOTHESIS_PROFILE=mutmut, so it is inert outside the ratchet. This is an
# upstream mutmut-3.x interaction (2.x did not copy + reload this way), not specific to this repo.
# - HYPOTHESIS_PROFILE=mutmut: derandomized profile so the property baseline is reproducible.
# It also activates the mutmut-only test fixture in tests/conftest.py:
# * an ephemeral per-session NUMBA_CACHE_DIR so the @njit VAR kernel recompiles its mutated
# source fresh on every mutant (JIT stays ON, at native speed). This replaces the old
# NUMBA_DISABLE_JIT=1 workaround, whose interpreted execution cost ~28 minutes per run;
# the cache, not the JIT, was what masked source mutations to the compiled kernel.
# There is deliberately NO solver-maxiter clamp (see the long note in tests/conftest.py): it
# changed the numerics on the unmutated code and broke the clean-baseline ARIMA golden, which
# mutmut requires to pass. Spinning mutants are bounded by --timeout below instead.
# RUN ON AN IDLE / DEDICATED MACHINE. mutmut 3.x runs its stats and per-mutant suites in-process
# (pytest.main) with --timeout below; under heavy CPU contention (e.g. other multi-hour jobs) the
# in-process stats pass can exceed the per-test timeout and abort the whole run before any mutant is
# tested. The full run is reliable on an idle box or a dedicated remote runner. Accepted equivalent
# survivors are catalogued in tests/mutation_equivalents.md (the ratchet baseline).
# source_paths is the whole importable tree (subdirs alone yield an incomplete copied package);
# only_mutate then restricts mutation to the engines+model scope.
source_paths = ["src/"]
only_mutate = [
"src/tsbootstrap/engines/*",
"src/tsbootstrap/model/*",
"src/tsbootstrap/uq/adaptive.py",
"src/tsbootstrap/prng_keys.py",
"src/tsbootstrap/dispatch.py",
"src/tsbootstrap/api.py",
]
# MUST stay false for the subprocess gate. true makes mutmut run an IN-PROCESS coverage pass (the
# whole suite under coverage.py) before generating, which hits the same numba parallel-JIT LLVM
# deadlock as the in-process executor -> empty coverage -> ZERO mutants generated. false skips that
# pass entirely: generation is pure libcst (no suite run, no deadlock) and mutates every in-scope
# line. The subprocess gate runs all generated mutants anyway, so covered-lines filtering is not
# needed; uncovered-line survivors are correct signal (a real "no test exercises this line").
mutate_only_covered_lines = false
# Systemic non-behavioral equivalents, suppressed at the source-pattern level (~164 of these were
# hand-classified as equivalent in the v0.2.0 mutation work). In mutmut 3.6 do_not_mutate_patterns
# is a list of PYTHON REGEXES, each re.search()-matched against every raw source LINE; a matching
# line has its expression nodes excluded from mutation (it is line-granular, NOT AST-node text and
# NOT a glob). Patterns are therefore anchored to their own kwarg/continuation lines so they do not
# over-suppress mutation of a real algorithmic expression that happens to share the line.
# - code=Codes.X / context={...}: error-construction kwargs (enum tag + diagnostic dict). Mutating
# these never changes behavior, only the exception payload, which no test asserts on by value.
# - dtype=np.<type>: the dtype-default class (e.g. dtype=np.float64 -> None). Inputs are already
# float64/int by construction, so dropping the explicit dtype is behavior-preserving.
# - def __<dunder>__ and logger/logging/log.<level>(...) call args: covered for fleet generality
# (no occurrences in the current engines+model scope, so inert here, but harmless to keep).
do_not_mutate_patterns = [
"^\\s*code=Codes\\.",
"^\\s*context=",
"dtype=np\\.",
"^\\s*def __[a-zA-Z]+__\\s*\\(",
"(?:logger|logging|log)\\.(?:debug|info|warning|error|critical|exception)\\(",
]
pytest_add_cli_args_test_selection = ["-m", "not slow", "tests/"]
# -o addopts= drops the project's default addopts (coverage, xdist, durations). The per-test
# timeout is mutmut's hang guard: a mutant that sends an iterative solver or a recurrence into a
# non-terminating loop is killed at 60s (thread method, so it interrupts even C-extension calls)
# instead of stalling the whole run.
pytest_add_cli_args = ["-o", "addopts=", "--timeout=60", "--timeout-method=thread"]