Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
370 lines (340 loc) · 16.9 KB
/
Copy pathpyproject.toml
File metadata and controls
370 lines (340 loc) · 16.9 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
# Main project metadata
[project]
name = 'jquantstats'
# Derived from the git tag by hatch-vcs (see [tool.hatch.version]) rather than written
# here, so the tag is the single source of truth and a release is `git tag` alone -- no
# bump commit, and no way for a number in this file to disagree with what was tagged.
dynamic = ["version"]
description = "Analytics for quants"
authors = [{name='tschm', email= 'thomas.schmelzer@gmail.com'}]
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"jinja2>=3.1.0",
"narwhals>=2.0.0",
"numpy>=2.0.0",
"plotly>=6.1.1",
"polars>=1.43.0",
"scipy>=1.14.1"
]
license = "MIT"
license-files = ["LICENSE"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Financial and Insurance Industry",
"Intended Audience :: Science/Research",
"Topic :: Office/Business :: Financial",
"Topic :: Scientific/Engineering :: Mathematics",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
# Project URLs for documentation and reference
[project.urls]
Homepage = "https://github.com/jebel-quant/jquantstats"
Repository = "https://github.com/jebel-quant/jquantstats"
# Optional dependencies that can be installed with extras (e.g., pip install jquantstats[plot])
[project.optional-dependencies]
plot = ["kaleido>=1.3.0,<2"]
# The optional matplotlib rendering backend, selected with
# `jquantstats.set_plot_backend("matplotlib")` or a per-call `backend="matplotlib"`.
# Plotly stays a core dependency and the default; this only adds the *second* renderer,
# so the "6 runtime deps, no pandas" claim in docs/engineering.md stays true.
#
# Named `mpl` because `plot` was already taken above — and `plot` means something else
# entirely (kaleido, static image export *for the plotly figures*).
mpl = ["matplotlib>=3.9"]
web = [
"fastapi>=0.115.0",
"uvicorn>=0.32.0",
"python-multipart>=0.0.12",
]
[dependency-groups]
# A group here earns its place only when something has to be present in the project
# *environment*. Every rhiza gate provisions its own tooling on the fly — `uv run
# --with pytest --with pytest-cov ...` in the `test` target, `--with ty` in
# `typecheck`, `--with interrogate` in `docs-coverage`, `uvx pre-commit` in `fmt` —
# so a tool named by a `--with` flag must NOT be declared: the declaration only
# duplicates the pin (and can conflict with it, as `pytest-benchmark==5.2.3`
# pinned by the `benchmark` target would).
#
# Two things do qualify: a library the test suite imports under a gate that does
# not inject it, and an import deptry resolves in DEPTRY_FOLDERS (src + book).
dev = [
"pandas>=2.2.3", # required by quantstats (comparison tests only — not a runtime dependency)
"pyarrow>=22.0.0", # polars' .to_pandas() (used all over tests/test_migration) imports it; polars declares it only under [pyarrow]
"yfinance==1.8.0",
"quantstats==0.0.86", # reference implementation used in test_quantstats.py for metric validation
# starlette's TestClient needs an HTTP client present; starlette declares one only under [full].
# starlette>=1.2 prefers httpx2 and warns on plain httpx, but the lowest-direct job resolves
# fastapi's floor, whose starlette (<1.2) knows only httpx -- so both stay declared.
"httpx2>=2.13.1",
"httpx>=0.28.1",
"marimo>=0.23.6", # book/ notebooks import it and deptry scans MARIMO_FOLDER — DEP001 without this
]
test = [
# pytest itself is injected by every pytest-backed target, but stays declared so
# a bare `uv run pytest tests/...` works. Most of its plugins are not: cov, html,
# mock, xdist, benchmark and pygal all arrive via `--with`.
"pytest>=8.0",
# pytest.ini sets a global `timeout = 60`, and `benchmark`, `hypothesis-test` and
# `stress` run pytest WITHOUT injecting this plugin — without it they warn
# "Unknown config option: timeout" and a hanging test hangs forever.
"pytest-timeout>=2.0",
# Imported by tests that `make test` collects, and `make test` injects neither.
# hypothesis in particular is reached through pytest.importorskip, so dropping it
# would not fail the suite — it would silently skip the property tests.
"hypothesis>=6.150.0",
"syrupy>=4.9.1",
"python-dotenv>=1.0",
# Declared here IN ADDITION to the `mpl` extra, and neither declaration is redundant:
# * The extra is what `uv sync --all-extras` installs. benchmark-pr.yml runs
# exactly that, without --all-groups, and uv's default group
# set is `dev` only — so without the extra that job runs the suite with
# matplotlib absent.
# * The group is what a bare `uv sync && uv run pytest` installs.
# Either gap has the same consequence, and it is NOT a skip: coverage is measured
# directory-scoped over src/, so an unimported src/jquantstats/_plots/_mpl/*.py is
# reported at 0% rather than dropping out of the report — a hard failure against
# `fail_under = 100`, with a misleading message. That is why there is no
# `pytest.importorskip("matplotlib")` anywhere; contrast test_kaleido.py, where the
# dependency genuinely can be absent because it needs a Chrome subprocess.
"matplotlib>=3.9",
]
# There is no `lint` group: nothing resolves one from the project environment. ruff runs
# through the pre-commit hooks `make fmt` executes via uvx, and import-linter — which
# enforces the [tool.importlinter] contracts — runs as one of those hooks, as
# `uv run --with import-linter lint-imports`.
# Build system configuration
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
# Supplies the `version` that [project] declares dynamic, from `git describe`. Two
# consequences worth knowing:
# * The build needs the tag to be visible. hatch-vcs does not fail without it -- it
# falls back to a dev version like 0.11.1.dev3+g<sha> -- so rhiza_release.yml checks
# the built distribution's filename against the tag, and its checkout uses
# fetch-depth: 0. An sdist carries the resolved number in PKG-INFO, so building from
# one needs no git.
# * Between tags the local version is the *next* patch as a dev release
# (guess-next-dev, the default), which is why nothing pins a fallback-version here:
# a wrong-looking version must surface as a release failure, not as a plausible
# constant.
[tool.hatch.version]
source = "vcs"
# Configuration for wheel build
[tool.hatch.build.targets.wheel]
packages = ["src/jquantstats"]
# Configuration for what files to include in the build
[tool.hatch.build]
include = [
"LICENSE", # Ensure the LICENSE file is included in your package
"README.md",
"src/jquantstats",
"src/jquantstats/py.typed",
]
# Bandit security linter configuration
[tool.bandit]
exclude_dirs = ["src/tests"] # Exclude test directories from security scanning
# Settings for the rhiza-task CLI, which replaced the synced make layer in rhiza
# v1.4.0. Every value below was carried over from a file that release deleted
# (`.rhiza/make.d/*.mk`, `.rhiza/.env`, the old Makefile); each one differs from the
# CLI default, so dropping it would quietly weaken a gate rather than fail loudly.
[tool.rhiza-task]
# Was COVERAGE_FAIL_UNDER in .rhiza/make.d/custom-env.mk. The CLI default is 90.
# `[tool.coverage.report] fail_under = 100` above does NOT cover this: the `test`
# task passes `--cov-fail-under` on the command line, which outranks the config
# file. Without this line the suite may drop to 90% coverage and still go green.
coverage_fail_under = 100
# Was TYPECHECKER in .rhiza/make.d/python.mk. The CLI default is `ty` alone, so
# omitting this would stop running mypy --strict entirely. Note this is also
# stricter than the make layer was: the old shell `case` let mypy's exit status
# win, so a `ty` failure could pass. Both must now succeed.
typechecker = "both"
# Was MARIMO_FOLDER in .rhiza/.env. The CLI default is `docs/notebooks`, which does
# not exist here — the notebooks live under book/. Without this, `marimo-validate`
# finds nothing to check and the notebooks stop being verified, and the folder also
# drops out of the deptry scan (taking the `marimo` dev dependency with it).
marimo_folder = "book/marimo/notebooks"
# Was RHIZA_CHECKS_VERSION in .rhiza/make.d/quality.mk, consumed as
# `pytest-rhiza==$(RHIZA_CHECKS_VERSION)`. The CLI default resolves
# `pytest-rhiza @ git+…@v0.2.0`, older than what this repo needs.
#
# 0.6.0 is the floor, not a routine refresh: it is the release whose pyproject
# check learned that `[project].version` may be *derived* from the VCS. Up to and
# including 0.2.1 the check reads a written number unconditionally, so against the
# `dynamic = ["version"]` above it fails three ways — `version` missing from the
# required-[project]-fields list, "" failing the semver regex, and
# `Version("")` raising InvalidVersion while comparing against the latest tag. All
# three are artefacts of the checker, not of this repo.
pytest_rhiza = "pytest-rhiza==0.6.0"
# Replaces `DEPTRY_FOLDERS += api` from the old Makefile. v1.4.0 removed that
# accumulator and now *derives* the deptry folder set — source_folder plus
# marimo_folder — with no setting to extend it. The `deps` task appends
# `deptry_ignore` to deptry's argv after those folders, and deptry takes multiple
# roots positionally, so a bare path here restores the previous scope exactly.
#
# This is load-bearing, not cosmetic: without it deptry never sees api/, and
# `ci / deps` fails with `DEP002 'fastapi' defined as a dependency but not used`.
# Suppressing that with a per_rule_ignores entry would have been the weaker fix —
# it would also stop flagging fastapi if api/ ever really stopped importing it.
deptry_ignore = ["api"]
# Deliberately NOT set, each because it already matches the CLI default — listing
# them anyway is how a redundant line becomes the one someone edits:
# source_folder ("src"), tests_folder ("tests"), uv_sync_args
# ("--all-extras --all-groups"), license_fail_on ("GPL LGPL AGPL"),
# book_output ("_book"), zensical_version (">=0.0.36"), paper_folder
# ("docs/paper").
# Also deliberately unset:
# mkdocs_extra_packages the old Makefile set `--with 'mkdocstrings[python]'`,
# which is now the default. Naming the setting REPLACES
# the default list, so restating it invites someone to
# append a package and silently drop mkdocstrings.
# license_ignore_packages marimo.mk appended `docutils`; rhiza-task adds it
# itself whenever the marimo tasks are registered.
# ci_os_matrix .rhiza/.env documented leaving this unset on purpose so
# rhiza_ci.yml can pick the matrix per caller. Pinning it here
# would defeat that for every consumer.
# layers / rhiza_checks both derived — layers from the manifests present
# (pyproject.toml -> "python"), and the check set from
# layers, which still yields all five checks the make
# accumulators built up.
# Not settings at all: PROJECT_NAME_EXTRA and LOG_LEVEL from custom-env.mk were
# cosmetic and have no CLI equivalent.
# Ignore specific dependency issues.
#
# DEP002 flags a declared dependency with no import site. The three below have
# none by design, so the ignore records a fact rather than hiding a gap:
# kaleido - Plotly's static image-export backend. Selected at runtime
# by plotly.io, never imported by us.
# uvicorn - ASGI server for api/app.py. Invoked as a command
# (`uvicorn api.app:app`), not imported.
# python-multipart - parses multipart/form-data for FastAPI's Form/UploadFile.
# Imported by Starlette, never by us.
# fastapi is deliberately absent: api/ is part of the deptry scan (see
# `deptry_ignore` under [tool.rhiza-task] above — it was the Makefile's
# DEPTRY_FOLDERS line before rhiza v1.4.0), so its import there is observed
# directly.
[tool.deptry.per_rule_ignores]
DEP002 = ["kaleido", "uvicorn", "python-multipart"]
# Package to module name mapping
[tool.deptry.package_module_name_map]
jinja2 = "jinja2"
numpy = "numpy"
plotly = "plotly"
polars = "polars"
pandas = "pandas"
scipy = "scipy"
kaleido = "kaleido"
fastapi = "fastapi"
uvicorn = "uvicorn"
yfinance = "yfinance"
quantstats = "quantstats"
marimo = "marimo"
hypothesis = "hypothesis"
narwhals = "narwhals"
python-multipart = "python_multipart"
pyarrow = "pyarrow"
httpx = "httpx"
httpx2 = "httpx2"
matplotlib = "matplotlib"
# Coverage configuration
[tool.coverage.run]
branch = true
[tool.coverage.report]
fail_under = 100
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:", # type-only imports are never executed at runtime
"@overload", # overload stubs have no runtime body
]
# Interrogate docstring coverage configuration
[tool.mypy]
[[tool.mypy.overrides]]
module = ["plotly.*", "scipy.*"]
ignore_missing_imports = true
[tool.interrogate]
fail-under = 100
ignore-init-method = true
ignore-magic = true
ignore-overloaded-functions = true
# Test-layout parity (rhiza check_test_layout): tests here are organised by
# behaviour under tests/test_jquantstats/… rather than mirroring src/ 1:1, and
# per-module coverage is guaranteed by the 100% line+branch pytest gate above
# (a stronger guarantee than file-mirroring). Opt out of the 1:1 mirror rule.
[tool.check_test_layout]
enforce = false
# Keep this string free of a literal `%`: tools that read [tool.*] tables through
# configparser (radon, for one) treat it as interpolation syntax and crash outright.
reason = "Tests are organised by behaviour under tests/test_jquantstats/; per-module coverage is guaranteed by the full line-and-branch pytest coverage gate rather than 1:1 file mirroring."
# Kept even though there is no longer a version in this file to rewrite, because its
# *presence* is what `/rhiza:release` reads to tell "this repo declares where its version
# lives, and it is the tag" from "this repo declares nothing" -- the second of which makes
# the command stop. Deliberately carries no `current_version`: bump-my-version then
# derives the current version from the newest matching tag, which is the same fact
# hatch-vcs builds from, so the two cannot drift. This is the shape rhiza's `go-core` and
# `rust-core` bundles ship for exactly the same reason.
#
# There is no `[[tool.bumpversion.files]]` entry, and adding one would be the mistake:
# every location that used to hold the number is now derived from the tag, so a file
# listed here could only reintroduce the drift this change removed.
[tool.bumpversion]
allow_dirty = false
# The release flow commits and tags itself, so that the regenerated changelog lands in
# the bump commit rather than in a second one after the tag.
commit = false
tag = false
# Architectural import contracts (import-linter), run as a pre-commit hook and so
# covered by `make fmt`. These encode the layering that src/jquantstats/_protocol.py
# describes in prose: the analytics subpackages annotate against the structural
# Protocols and must never reach for the concrete Data / Portfolio classes.
#
# Type-checking-only imports are excluded, because `if TYPE_CHECKING: from ..data
# import Data` is exactly the sanctioned pattern here — it costs nothing at
# runtime and forms no import cycle. Only real runtime imports are contract
# violations.
[tool.importlinter]
root_package = "jquantstats"
exclude_type_checking_imports = true
# NOTE on the pyplot ban: it deliberately is NOT an import-linter contract.
# import-linter rejects `matplotlib.pyplot` with "subpackages of external packages are
# not valid" — it can only forbid whole external distributions, and forbidding
# `matplotlib` outright would ban the renderer itself. The ban is enforced instead by
# tests/test_jquantstats/test__plots/test_no_pyplot.py, which scans the source tree,
# plus a runtime leak-detector fixture. See that module for the reasoning.
[[tool.importlinter.contracts]]
name = "analytics subpackages must not import the concrete Data/Portfolio at runtime"
type = "forbidden"
source_modules = [
"jquantstats._stats",
"jquantstats._plots",
"jquantstats._reports",
"jquantstats._utils",
]
forbidden_modules = [
"jquantstats.data",
"jquantstats.portfolio",
]
[[tool.importlinter.contracts]]
name = "the Data reshape mixin must not import Data"
type = "forbidden"
source_modules = ["jquantstats._data_reshape"]
forbidden_modules = ["jquantstats.data"]
[[tool.importlinter.contracts]]
name = "the Portfolio mixins must not import Portfolio or Data at runtime"
type = "forbidden"
source_modules = [
"jquantstats._portfolio_attribution",
"jquantstats._portfolio_base",
"jquantstats._portfolio_constructors",
"jquantstats._portfolio_cost",
"jquantstats._portfolio_nav",
"jquantstats._portfolio_transform",
"jquantstats._portfolio_turnover",
"jquantstats._portfolio_units",
]
forbidden_modules = [
"jquantstats.data",
"jquantstats.portfolio",
]