Repository navigation
Expand file tree
/
Copy pathpyproject.toml
More file actions
279 lines (265 loc) · 13.1 KB
/
Copy pathpyproject.toml
File metadata and controls
279 lines (265 loc) · 13.1 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
[project]
name = "openkos"
version = "0.5.5"
description = "Local-first engine that turns your scattered text into a living, portable knowledge base in the Open Knowledge Format (OKF)"
readme = "README.md"
license = "Apache-2.0"
# Apache-2.0 section 4 requires every redistribution to carry a copy of the
# license, and `license` above is only an SPDX *expression* -- metadata, not
# the text. Without this, both artifacts shipped `License-Expression:
# Apache-2.0` pointing at a file neither of them contained. The backend
# resolves these paths against the project root and lands them in the sdist
# and in the wheel's `.dist-info/licenses/`.
license-files = ["LICENSE"]
authors = [
{ name = "Jason", email = "jasonssdev@gmail.com" }
]
requires-python = ">=3.14"
keywords = [
"okf",
"open-knowledge-format",
"knowledge-base",
"knowledge-graph",
"local-first",
"rag",
"llm",
"second-brain",
"cli",
]
# The `License ::` trove classifier is deliberately omitted: `license` above is
# an SPDX expression (PEP 639), and declaring the license twice makes modern
# build backends warn or fail. The SPDX string is the single source of truth.
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Text Processing :: Indexing",
"Typing :: Typed",
]
dependencies = [
"networkx>=3.4",
"python-frontmatter>=1.3.0",
"pyyaml>=6.0.3",
# `cli/main.py` imports `rich.console.Console` directly. Typer happens to
# depend on rich too, but an undeclared transitive import is a dependency
# we do not control: the day Typer drops or vendors it, openkos breaks at
# import time on an install that resolved cleanly. Declare what we import.
# The floor matches Typer's own so the resolver has no extra constraint.
"rich>=13.8.0",
"sqlite-vec>=0.1.6",
"typer>=0.27.0",
]
[project.optional-dependencies]
# Optional OS file-notification wake-up for the daemon's inbox watch
# (`unattended.watch_backend: native`, ADR-0040). Polling is the default and the
# fallback, so the core never needs it.
watch = ["watchdog>=4.0.0"]
# The local HTTP API (`openkos serve`, ADR-0061) for the desktop app. Imported
# only inside `openkos.localapi`; the core never needs it. Each package listed is
# imported directly (pydantic by the request and response models).
api = ["fastapi>=0.143.2", "pydantic>=2.14.1", "uvicorn>=0.54.0"]
[project.urls]
Homepage = "https://github.com/jasonssdev/openkos"
Repository = "https://github.com/jasonssdev/openkos"
Issues = "https://github.com/jasonssdev/openkos/issues"
Changelog = "https://github.com/jasonssdev/openkos/blob/main/CHANGELOG.md"
[project.scripts]
openkos = "openkos.cli.main:app"
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
[dependency-groups]
# `git-filter-repo` is a real test requirement, not just `purge`'s runtime
# system tool: `test_filter_repo_available_true_when_installed` asserts the
# PATH probe finds it, so it fails on a machine without the binary.
# `watchdog` is the optional `watch` extra's dependency, installed here so CI
# type-checks the native backend and runs its one real-event smoke test.
# `fastapi`, `pydantic` and `uvicorn` are the `api` extra's dependencies and
# `httpx` backs FastAPI's `TestClient`, installed here so CI exercises the
# local HTTP API.
dev = [
"fastapi>=0.143.2",
"git-filter-repo>=2.47.0",
"httpx>=0.28.1",
"mypy>=2.3.0",
"pre-commit>=4.6.0",
"pydantic>=2.14.1",
"pytest>=9.1.1",
"pytest-cov>=7.1.0",
"ruff>=0.15.21",
"types-networkx>=3.6.1.20260624",
"types-pyyaml>=6.0.12.20260518",
"uvicorn>=0.54.0",
"watchdog>=4.0.0",
]
# PyInstaller freezes the engine for the desktop app (ADR-0056) and nothing
# else: `desktop/scripts/build-engine.sh` syncs it into its own build
# environment with `--group freeze`. A dependency group is not package metadata,
# so the wheel and the sdist never declare it, and `uv sync` leaves it out of
# the development environment.
freeze = ["pyinstaller>=6.22.3"]
[tool.pytest.ini_options]
testpaths = ["tests"]
# `--strict-markers` turns an unregistered marker into a collection error
# instead of a `PytestUnknownMarkWarning` nobody reads (#264). The entire
# opt-out from the unit suite's fail-closed network guard is a marker NAME
# matched as a string, so a misspelled `live_backend` would otherwise leave
# BOTH autouse fixtures installed while reading as a deliberate opt-out --
# and, combined with the absorbability the guard's own docstring concedes,
# a mis-marked live-backend pin would degrade through a broad handler and
# read green while never contacting the backend it claims to measure. That
# is #217's silence, re-entered through the escape hatch.
addopts = ["--import-mode=importlib", "--strict-markers"]
markers = [
# Opts a test out of the unit suite's fail-closed network guard (#217).
# Reserved for assumption pins that measure the real backend and already
# gate themselves on a reachability probe -- not an escape hatch for a
# unit test that is merely inconvenient to stub.
"live_backend: test deliberately reaches a live Ollama backend",
# Selects the REDUCED subset CI runs on macos-latest/windows-latest
# (#929): CI's `test` job runs the full suite on Linux only, so this
# marks the small, deliberately-cross-platform slice -- filesystem
# normalization, symlink/permission refusals, the workspace lock's
# `fcntl`/`msvcrt` branches, SQLite WAL PRAGMAs, and init/ingest/doctor
# against the packaged wheel -- that exercises what Linux alone cannot.
# Not an escape hatch for slow or flaky tests: platform-gated tests that
# first ran on Linux cost two fix cycles on PR #492, and HFS+/SMB
# normalization differences produced real graph-edge bugs (#430); this
# marker exists to stop paying that bill twice, not to grow unbounded.
"cross_platform_smoke: reduced macOS/Windows CI subset (#929)",
]
# Promotes an unclosed `sqlite3.Connection`'s `ResourceWarning` to an error
# (#927: a 2026-08-31 audit found 25, all left by test code that opened a
# connection/handle and never closed it). Both entries below are required,
# and BOTH are scoped to sqlite3 specifically -- not a blanket
# `error::ResourceWarning` -- so this does NOT also gate
# `test_lock.py`/`test_main.py`'s unrelated unclosed-FILE `ResourceWarning`s
# (a distinct, already-tracked concern; #938 covers those via subprocess
# isolation):
#
# 1. `error:unclosed database in <sqlite3\.Connection:ResourceWarning` makes
# SQLite's C-level `tp_dealloc` RAISE (instead of merely warn) the moment
# a connection's refcount hits zero without `close()` having run. Raising
# inside a destructor is exactly what CPython calls an "unraisable
# exception" -- it cannot propagate normally, so it routes through
# `sys.unraisablehook` instead.
# 2. `error:Exception ignored in.*<sqlite3\.Connection:pytest.
# PytestUnraisableExceptionWarning` is what turns THAT into an actual
# test failure. Note the `.*`: it stands in for the literal ": " that
# pytest itself prints, and it is NOT a stylistic choice -- a literal
# colon there is the misparse this file documents below, so quoting the
# filter with one would teach a maintainer to re-derive an entry that
# silently disables the gate. What follows describes the mechanism:
# `_pytest.unraisableexception` installs its own
# `sys.unraisablehook`, and re-raises what it caught as a
# `PytestUnraisableExceptionWarning` (a SEPARATE `warnings.warn` call,
# a different category, message-prefixed "Exception ignored in: " --
# entry 1's filter does not cover it) via `collect_unraisable`, drained
# at `pytest_runtest_setup`/`call`/`teardown` for every item. Without
# this second entry the leak still surfaces, but only as a warnings-
# summary line ("N warnings"), and the suite exits 0 -- confirmed
# empirically while building this gate.
#
# These two lines make the suite go RED on any new unclosed connection, which
# is the whole guarantee. WHICH test they blame is a separate, weaker
# property, and it depends on `tests/unit/conftest.py`'s
# `_reclaim_cyclic_garbage_for_deterministic_leak_attribution` autouse
# fixture forcing a generation-0 collection in every test's own teardown.
# That fixture's docstring records what attribution does and does not
# cover -- a connection RETURNED from a `@pytest.fixture` is still blamed on
# a bystander, and a full `gc.collect()` does not fix it while costing this
# suite 210 seconds a run on each of three interpreters.
filterwarnings = [
"error:unclosed database in <sqlite3\\.Connection:ResourceWarning",
# No literal ':' inside the message field below (`.*` stands in for the
# "ignored in: <sqlite3" colon-space) -- pytest's ini `filterwarnings`
# splits each entry on ':' into action/message/category/module/lineno,
# so a literal colon inside the message text misparses the rest of the
# line (confirmed empirically: it read "<sqlite3" as a category to
# import and failed with a `PytestConfigWarning`, silently leaving the
# gate NOT installed).
"error:Exception ignored in.*<sqlite3\\.Connection:pytest.PytestUnraisableExceptionWarning",
]
# Coverage is opt-in: `--cov` is deliberately not in pytest's addopts, so plain
# `uv run pytest` stays fast for the local loop. CI passes `--cov`, and because
# the gate lives here, a local `uv run pytest --cov` gates identically to CI.
[tool.coverage.run]
source = ["openkos"]
# Line coverage alone lets an `if` that never goes both ways read as covered;
# branch coverage counts each direction.
branch = true
[tool.coverage.report]
fail_under = 90
show_missing = true
# Ruff infers `target-version` from `project.requires-python` (3.14) and
# defaults `line-length` to 88; neither is restated here. The formatter runs on
# defaults, so there is no [tool.ruff.format] section.
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"F", # Pyflakes: undefined names, unused imports
"I", # isort: deterministic import order, keeps the layering rule auditable
"UP", # pyupgrade: the 3.14 floor means no back-compat idioms are needed
"B", # bugbear: mutable defaults, `raise ... from`, unstrict `zip`
"BLE", # blind-except: a broad catch must name its reason (`noqa: BLE001 -- why`)
"SIM", # flake8-simplify
"S", # bandit: untrusted user text, YAML frontmatter, git subprocess
"DTZ", # naive datetimes would corrupt OKF `as of` / freshness stamps
"PT", # flake8-pytest-style: tests are a first-class quality gate
"PTH", # pathlib: the engine is filesystem-centric and must stay portable
"RUF", # Ruff-specific, incl. RUF100 (unused noqa) to keep suppressions honest
]
ignore = [
"E501", # line length is the formatter's job; E501 only fights it on long strings/URLs
]
[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # `assert` is what a test is made of
"DTZ001", # `event_date` times are naive local wall-clock by contract (ADR-0052)
]
"evals/**/*.py" = [
"BLE001", # probe harnesses record a failed run and move on; they are not engine code
]
"desktop/scripts/*.py" = [
"S603", # the smoke check exists to run the frozen engine it is given
"S607", # and `ps`, from PATH, to list the engine's process group
"S310", # its only URL is the engine's own loopback ready line, over http
]
[tool.ruff.lint.flake8-bugbear]
# `typer.Argument(...)`/`typer.Option(...)` in a parameter default is the
# idiomatic Typer CLI pattern (B008 normally flags a mutable default called
# at definition time), and both are effectively immutable declarative
# markers, not runtime state. `bool`/`str | None`-annotated parameters are
# already exempt by B008's own immutable-type check; `Path`-annotated ones
# (e.g. `ingest`'s `src` argument) are not, so both callables are listed
# explicitly here for consistency across every future Typer command.
extend-immutable-calls = ["typer.Argument", "typer.Option"]
[tool.mypy]
# Pin the analysis target to the declared floor so a contributor on a newer
# interpreter cannot merge code that is broken on 3.14.
python_version = "3.14"
files = ["src", "tests"]
# `mypy .` must not walk build outputs: the frozen engine under `build/` carries
# a second copy of the `openkos` package (ADR-0056), which mypy would report as
# a duplicate module.
exclude_gitignore = true
strict = true
# Not implied by `strict`: dead code left by a wrong narrowing is a real bug,
# and `py.typed` means our type errors become our consumers' type errors.
warn_unreachable = true
enable_error_code = [
"ignore-without-code", # a bare `# type: ignore` silences more than it claims
"redundant-expr",
"truthy-bool", # catches `if obj:` on an always-truthy object
"possibly-undefined",
]
# `sqlite-vec` ships no type stubs; this keeps `mypy . --strict` (repo-wide,
# incl. tests) green without weakening `strict` for the rest of the codebase.
[[tool.mypy.overrides]]
module = "sqlite_vec.*"
ignore_missing_imports = true