[tool.uv] no-build-package = ["growwapi"] constraint-dependencies = ["plotly==6.9.0"] # certifi stays on the newest release growwapi 1.5.0 accepts (certifi<2026). # A uv override to the patched 2026 bundle does not apply to # `pip install --require-hashes -r requirements.lock`. The resulting advisory # is time-limited in supply-chain/pip-audit-allowlist.yml. # Kotak v3 runtime and release baseline both follow the pinned v3.0.9 # commit in brokers.lock. Freshness compares that release, not default-branch HEAD. build-constraint-dependencies = [ "packaging==26.3", "setuptools==84.1.0", "wheel==0.48.0", ] no-build-isolation-package = ["kotakneoapi", "upstox-python-sdk"] [tool.uv.workspace] members = [ "packages/core/core", "packages/core/data", "packages/core/historical", "packages/core/indicators", "packages/core/ticks", "packages/services/engine", "packages/services/screener", "packages/services/backtest", "packages/services/ai", "packages/services/ditto", "packages/services/automation", "packages/services/journal", "packages/integrations/gateway", "packages/integrations/webhooks", ] # Dev/test toolchain — installed by `uv sync` (the default `dev` group) so the # test runner is reproducible from the lockfile rather than ambient on PATH # (audit BT-05). CI parallelism + randomisation plugins are picked up via their # entry points when present (see [tool.pytest.ini_options] note below). [dependency-groups] dev = [ "pytest==9.2.0", "pytest-asyncio==1.4.1", "pytest-xdist==3.8.0", "pytest-randomly==5.0.0", "pytest-timeout==2.4.0", # CI runs `pytest --timeout=60 --timeout-method=thread` # (test.yml / nightly); the plugin must be in the synced env "pyfakefs==6.2.0", # baselines §4.4 disk-full migration test (HI24) "ruff==0.17.3", # lint gate — CI runs `uv run ruff check` (test.yml), so it # must be in the synced env, not merely on PATH ] [tool.uv.sources] upstox-python-sdk = { git = "https://github.com/upstox/upstox-python.git", rev = "d88a12ef5738a4c8d71f3b88eb7b79a359277c9d" } kotakneoapi = { git = "https://github.com/Kotak-Neo/kotak-neo-python.git", rev = "0ba71b7f7c295652d4c3ea2b5ce5a88849db18a3" } flinttrade-ai = { workspace = true } flinttrade-automation = { workspace = true } flinttrade-backtest = { workspace = true } flinttrade-core = { workspace = true } flinttrade-data = { workspace = true } flinttrade-ditto = { workspace = true } flinttrade-engine = { workspace = true } flinttrade-gateway = { workspace = true } flinttrade-historical = { workspace = true } flinttrade-indicators = { workspace = true } flinttrade-journal = { workspace = true } flinttrade-screener = { workspace = true } flinttrade-ticks = { workspace = true } flinttrade-webhooks = { workspace = true } # ---------------------------------------------------------------------------- # Pytest configuration # ---------------------------------------------------------------------------- # Three custom markers are registered for the staged-test-execution model # (unit on every commit, integration on PR, slow nightly). --strict-markers # fails CI if any test uses an unregistered marker -- catches typos before # they go unnoticed. # # Existing tests are NOT yet marked. Default behaviour is unchanged: an # untagged test simply runs in every job. As tests get touched, prefer to # tag them; CI selection clauses can then be tightened over time. # # pytest-xdist + pytest-randomly are picked up automatically when installed # (they add their own opts via entry points). Local devs can run without # them; CI installs them and uses -n auto for parallelism. # ---------------------------------------------------------------------------- [tool.pytest.ini_options] testpaths = ["tests"] # Keep package-local tests.* namespaces distinct from shared tests.mocks, # including when a focused or sharded run does not collect the root tests. consider_namespace_packages = true asyncio_mode = "auto" addopts = [ "--import-mode=importlib", "--strict-markers", "-ra", ] markers = [ "unit: fast, isolated unit test (default; runs on every commit)", "integration: touches DB or mocked external services (PR gate)", "slow: long-running test >1s (nightly or via --run-slow)", ] filterwarnings = [ # huggingface_hub still calls its own deprecated `hf_xet.download_files()` # internally — out of our control, will go away on upstream release. "ignore:hf_xet.download_files.*:DeprecationWarning:huggingface_hub.*", ] [tool.ruff] # `target-version` is the OLDEST interpreter the code must still run on, not the # one we develop against. flint.toml [requirements] sets python_requires # ">=3.14" (python_target "3.14"), so py314 matches the floor. Bump this only # when the flint.toml floor moves. target-version = "py314" line-length = 120 [tool.ruff.lint] # Baseline: ruff 0.16 widened its default rule set, so the pre-0.16 defaults are # pinned explicitly. A future ruff release that widens its defaults again cannot # silently change this gate. select = ["E4", "E7", "E9", "F"] # Modern families adopted 2026-07-30 and measured on ruff 0.16.0 (the version the # dev group resolves to). Listed one family per line, explicitly, # rather than dropping `select` and inheriting whatever a future ruff decides is # a default — the gate is intentional and dated, not implicit. Every family here # is GREEN across packages/*/*/src/; anything that could not reach zero is # either pinned in `ignore` below with its count, or listed in the deferred # adoption list at the bottom of this section. extend-select = [ "I", # isort — deterministic import blocks "UP", # pyupgrade — no outdated ways of writing modern Python "B", # flake8-bugbear — real bug classes, not style "C4", # flake8-comprehensions "RET", # flake8-return "RSE", # flake8-raise "PIE", # flake8-pie — misc anti-patterns "PERF", # perflint "FURB", # refurb — modernisation beyond pyupgrade "ASYNC", # flake8-async — blocking calls inside async def "LOG", # flake8-logging — logger misuse "G", # flake8-logging-format "ISC", # flake8-implicit-str-concat — catches the missing-comma bug "Q", # flake8-quotes "TID", # flake8-tidy-imports — no `from ..parent` across package layers "SLOT", # flake8-slots "INT", # flake8-gettext "EXE", # flake8-executable — shebang/permission mismatches "FA", # flake8-future-annotations "NPY", # NumPy-specific rules "YTT", # flake8-2020 — sys.version misuse ] ignore = [ # ---- Rules inside adopted families that are NOT yet green (2026-07-30) ---- # Each line is a live debt marker: the count is the violation count at # adoption time. Fixing one and deleting its line is always an improvement. "UP035", # 59 — `typing.Callable` → `collections.abc`. ruff's fix is UNSAFE # (it can break re-exports), so it is not auto-appliable. "UP040", # 7 — PEP 695 `type X = ...` aliases; needs per-alias review. "UP042", # 7 — `str, Enum` → `StrEnum`; changes `__str__`/format output, # which some serialised payloads depend on. Needs review. "UP047", # 2 — PEP 695 generic functions. "B904", # 18 — `raise ... from err` inside except. "B905", # 63 — `zip(strict=...)`. Turning strictness on can raise where # today it truncates, so it is a behaviour decision per site. "B007", # 5 — unused loop control variable. "B027", # 3 — empty method in ABC without @abstractmethod. "RET504", # 17 — unnecessary assignment before return (ruff fix is unsafe; # the named temporaries are often deliberate for debugging). "PERF401",# 43 — manual list comprehension. "PERF403",# 3 — manual dict comprehension. "ASYNC109",# 9 — own `timeout` parameter instead of `asyncio.timeout`; these # are public signatures, so changing them is an API break. "NPY002", # 2 — legacy `np.random`; the new Generator API draws a different # stream, so switching changes reproducible test fixtures. # PERF101 is NOT deferred debt — it is banned. Its fix is classified "safe" # but is not: applying it to the single site it flagged in this repo # (IndMoneyAdapter._symbol_keys, which iterates a set while adding aliases to # it) deleted a load-bearing `list()` cast and turned three passing # test_indmoney_adapter tests into `RuntimeError: Set changed size during # iteration`. `list(x)` in a `for` header is the standard guard for # mutate-while-iterating, and ruff cannot tell that from a redundant copy — # so leaving the rule on means the next `ruff check --fix` silently # reintroduces the same bug. Verified 2026-07-30. "PERF101", ] # ---- Deferred adoption list (measured 2026-07-30, ruff 0.16.0) ---------------- # Marginal cost of adding each family ON TOP of the set adopted above, measured # over packages/*/*/src/ with `ruff check --extend-select --statistics`. # NOT enabled, because enabling them would put the only Python quality gate # permanently red — and a red gate gets ignored, then stops gating anything. # Work a family down, then move it into `extend-select` above and delete its # line here. # # D 4205 (D413 2597, D102 767, D107 397, D401 110, D105 85, D205 44, …) # TRY 3622 (TRY003 2990, TRY400 223, TRY301 218, TRY004 100, TRY300 70, …) # EM 3010 (EM101 2126, EM102 884 — exception message as a bare literal) # PL 1883 (PLR2004 538, PLR0913 342, PLC0415 261, PLR0917 209, PLR0912 171, …) # ANN 1626 (ANN401 1572 — `Any` in signatures) # RUF 1583 (RUF100 1240 — see the noqa note below; RUF002 217, RUF003 44, …) # COM 1538 (COM812 missing-trailing-comma — formatter territory) # BLE 456 (BLE001 blind-except) # TC 394 (TC002 308, TC003 51, TC001 32 — needs runtime-import review) # C90 333 (C901 complex-structure) # FBT 254 (FBT003 106, FBT001 92, FBT002 56 — public API signatures) # S 204 (S101 61, S110 49, S608 46, S311 23, S105 7, …) # SIM 173 (SIM105 77, SIM108 33, SIM102 26, SIM114 14, SIM117 11, …) # ARG 155 (ARG002 137, ARG001 18) # SLF 119 (private-member-access — mostly deliberate, intra-package) # N 104 (N806 36, N818 34, N803 26 — many mirror broker API field names) # PTH 74 (os.path → pathlib; every fix is manual, no safe autofix exists) # ERA 74 (commented-out code) # DTZ 62 (naive datetimes; IST market-clock semantics must be preserved, # so a blanket `tz=` insert is a correctness risk, not a cleanup) # PYI 55 (PYI034 27, PYI036 22 — a stub-file family firing on .py) # T20 35 (print in library code) # PD 6, ICN 4, A 0, INP 0, DJ 0 # # NOTE on RUF100 (1240): packages/*/*/src/ already carries 1376 `# noqa` # directives, and 1240 of them name a rule that is NOT selected — 740 for # PLC0415 and 390 for BLE001 alone. Those suppressions were written by authors # who expected those families to be on. They are inert today: enabling PL/BLE # would make 1130 existing noqa comments meaningful rather than adding 1130 new # violations, which makes those two families much cheaper to adopt than their # headline numbers suggest. # # The two test trees are excluded from every count above; their debt is # enumerated in [tool.ruff.lint.per-file-ignores] below. [tool.ruff.lint.per-file-ignores] # The lint path was widened on 2026-07-30 to cover the test trees, which had # never been linted at all under the CI invocation. The test trees are now gated # on the E4/E7/E9/F baseline (the F821/F401 import-NameError class), but the # newly adopted modern families still have measured debt there: # tests/ 46 violations (41 safely autofixable) # packages/*/*/tests/ 964 violations (844 safely autofixable, 524 of them # I001 import sorting alone) # Every rule below is listed individually rather than by family prefix, so a # test-owning change can clear one and delete its line. `ruff check --fix` over # the test trees clears roughly 88% of this in one pass. "tests/**/*.py" = [ "ASYNC110", "B006", "B009", "B011", "B017", "B018", "C408", "C420", "FURB110", "FURB157", "FURB167", "FURB171", "I001", "ISC004", "LOG001", "PERF102", "PIE807", "PIE808", "PIE810", "Q000", "Q003", "RET501", "RET503", "RSE102", "SLOT000", "UP012", "UP017", "UP022", "UP037", "UP041", ] "packages/*/*/tests/**/*.py" = [ "ASYNC110", "B006", "B009", "B011", "B017", "B018", "C408", "C420", "FURB110", "FURB157", "FURB167", "FURB171", "I001", "ISC004", "LOG001", "PERF102", "PIE807", "PIE808", "PIE810", "Q000", "Q003", "RET501", "RET503", "RSE102", "SLOT000", "UP012", "UP017", "UP022", "UP037", "UP041", ] # The script test tree matches neither glob above, so widening the lint path # picked it up for the first time. Its only debt is three expected-message # assertions that each build ONE long string across several lines inside a # single-element list. That is the exact shape ISC004 exists to flag, because a # missing comma looks identical - but here the concatenation is deliberate, and # splitting the assertions to prove it would cost more clarity than it buys. "scripts/__tests__/**/*.py" = ["ISC004"] # 3 — deliberate multi-line expected messages # Two modules deliberately import BELOW module-level `sys.path` manipulation, so # their whole import section is one long `# noqa: E402` block hand-organised into # commented sections. isort cannot express that: its fix reflows the block, which # strands every section comment against the wrong group and drops the per-line # `# noqa: E402` from any statement it has to split — turning one clean file into # eleven new E402 violations. Correctness is unaffected either way, so the # hand-ordering wins and I001 is off for these two files only. "packages/core/core/src/flinttrade_core/app.py" = ["I001"] "packages/services/backtest/src/flinttrade_backtest/strategies/__init__.py" = ["I001"] # The PyO3 crate's Python shim is outside every `src/` tree, so widening the # lint path to `packages/` picked it up for the first time. One outstanding # violation, kept visible rather than silently excluded from the path: "packages/core/ticks/python/tick_engine/__init__.py" = [ "I001", # 1 — a two-name reorder inside the single `from tick_engine.tick_engine # import (...)` block. `ruff check --fix` clears it; it belongs to # a ticks-owning change, not to the lint-config change. ] [tool.ruff.lint.isort] # The repo writes `from x import a, b as c` on one line. isort's DEFAULT is to # force every aliased member onto a statement of its own, which turns one line # into three and churns files for no readability gain. Keep the existing style. combine-as-imports = true # Pin first-party classification to the workspace distributions so the same # import sorts identically no matter which package's src/ root ruff resolves. known-first-party = [ "flinttrade_ai", "flinttrade_automation", "flinttrade_backtest", "flinttrade_core", "flinttrade_data", "flinttrade_ditto", "flinttrade_engine", "flinttrade_gateway", "flinttrade_historical", "flinttrade_indicators", "flinttrade_journal", "flinttrade_screener", "flinttrade_ticks", "flinttrade_webhooks", ]