[project] name = "schemabrain" version = "0.6.0" description = "The trust and intelligence layer between AI agents and your database." readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" authors = [ { name = "Arun K C" }, ] keywords = ["mcp", "database", "semantic", "ai-agents", "postgres", "schema", "schema-intelligence", "knowledge-graph", "agent-trust"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Database", "Topic :: Software Development :: Libraries", ] dependencies = [ "sqlalchemy>=2.0.50,<3.0", "psycopg[binary]>=3.1,<4.0", "pydantic>=2.5,<3.0", "structlog>=24.0,<26.0", "anthropic>=0.107.1,<1.0", "fastembed>=0.4,<1.0", "mcp>=1.0,<2.0", "rich>=13.0,<16.0", # NumPy is the engine behind `Store.search_embeddings_topk` (bulk # SELECT + float32 matmul + cosine). It arrives transitively via # fastembed too, but pin it as a direct dep so the contract is # durable: a future fastembed major can't silently drop it without # us catching the regression. "numpy>=1.26,<3.0", # sqlglot is the SQL parser behind query log mining. v0.5 uses it # to extract the set of tables a `pg_stat_statements` row touches # and to filter DDL/transaction-control statements out of the # example_queries write set. sqlglot bumps parser semantics across # majors, so the upper bound is a deliberate, tested review. # # Capped below 27.0: sqlglot 30.x renamed the `exp.Select` arg key # from `"from"` to `"from_"`, which silently breaks # `joins/mining.py:_build_alias_map` — `select.args.get("from")` # returns None, the FROM-side table never enters the alias map, and # `extract_join_predicates` returns an empty set with NO error # (see tests/test_joins_mining.py::TestSqlglotVersionRegression). # The lock pins 26.x where the suite is green; the lock-free CI # smoke job exercises whatever a fresh `pip install` resolves so # this can't silently drift again. Widen the bound only after # porting the AST walk off the version-specific `args.get("from")` # key (e.g. to `select.find(exp.From)`). "sqlglot>=25.0,<27.0", # PyYAML powers the entity YAML grammar parser # (`schemabrain entities apply ` and the in-process # loader behind it). It also arrives transitively via fastembed's # dep chain, but pinning it as a direct runtime dep makes the # contract durable: a future upstream drop can't silently break # the entity loader. Same pin previously held in dev-deps for # `tests/test_dependabot_config.py`; now lifted to runtime. "pyyaml>=6.0.3,<7.0", ] [project.optional-dependencies] # Optional OpenTelemetry span emission. Activated when ALL three hold: # (1) this extra is installed, (2) `OTEL_EXPORTER_OTLP_ENDPOINT` is # set, (3) the operator runs `schemabrain serve`. Without the extra, # `pip install schemabrain` works identically — `init_tracer_from_env` # returns None and `@instrument` skips span emission. Spans flow over # OTLP/HTTP to any OTel-compatible backend: Langfuse, Phoenix, # OpenLIT, otel-tui. See `docs/observability.md` for setup recipes. otel = [ "opentelemetry-sdk>=1.42.1,<2.0", "opentelemetry-exporter-otlp-proto-http>=1.27,<2.0", ] # Optional dashboard sidecar — the v0.4 M1 read-only UI surface # (PII Visualization + Refusal Experience UI + Audit Viewer). # Activated when (1) this extra is installed, (2) the operator runs # `schemabrain dashboard`. Without the extra, `pip install schemabrain` # works identically — the sidecar's `create_sidecar` import raises a # clear "install with `pip install schemabrain[ui]`" message. The # pre-built Next.js static export ships inside the wheel at # `schemabrain/dashboard/static/`; end users never need Node. # See `docs/internal/v0.4_ui_rfc.md` §2 for the full architecture. ui = [ # Upper bound excludes 0.136.3 — that single release modified # pyproject.toml to silently add an undocumented `fastar` typosquat # dependency to the `[standard]` extra (advisory MAL-2026-4750). # We don't use `fastapi[standard]`, but pip-audit still flags the # version, and pinning away is the cleanest path. Revisit the # upper bound when an audited 0.137+ release is available. "fastapi>=0.115,!=0.136.3,<1.0", "uvicorn[standard]>=0.30,<1.0", "sse-starlette>=3.4.4,<4.0", ] dev = [ "pytest>=9.0.3,<10.0", "pytest-cov>=7.1.0,<8.0", "ruff>=0.6,<1.0", "testcontainers[postgres]>=4.0,<5.0", # `pip-audit` runs in the `security` CI job to fail any PR that # introduces a known-CVE dep. Pinning here (rather than uvx) lets # Dependabot manage upgrades and reproducibility lives in uv.lock. "pip-audit>=2.7,<3.0", # `bandit` runs in the `security` CI job for Python SAST. Same # pinning rationale as pip-audit: Dependabot-managed, uv.lock-pinned. "bandit>=1.7,<2.0", # `pytest-benchmark` powers the retrieval p95 gate (Phase A Commit 4). # Marked `slow` so the default `pytest` run skips it; the `slow` # CI job (or `pytest -m slow` locally) runs it and gates the # `find_relevant_tables` < 100ms-at-10k-cols Charter target. "pytest-benchmark>=4.0,<6.0", # Hypothesis powers the property-based tests on the metric # compiler (Q16 in `project_v1_quality_plan.md`) — required because # the compiler is a v2-substrate seam where the single-statement / # parameterisation / grain-check invariants must hold across the # whole input space, not just hand-picked examples. "hypothesis>=6.0,<7.0", ] [project.urls] Homepage = "https://github.com/Arun-kc/schemabrain" Issues = "https://github.com/Arun-kc/schemabrain/issues" [project.scripts] schemabrain = "schemabrain.cli:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.sdist] # Belt-and-suspenders against future stray venvs / scratch artifacts # breaking the publish pipeline. `.gitignore` already excludes these, # but hatchling's sdist build walks the working tree directly rather # than asking git — so an uncommitted `.venv-foo/` next to `pyproject` # would otherwise get packed and choke on absolute symlinks to a # Python interpreter. The list mirrors `.gitignore`'s top-level # directory exclusions; extend it if a new top-level scratch dir # emerges. exclude = [ ".venv*", ".benchmarks", ".hypothesis", ".mypy_cache", ".pytest_cache", ".ruff_cache", ".review", ".claude", "dist", "build", "htmlcov", "schemabrain-*.db", "schemabrain.db", "schemabrain.db-journal", "docs/internal", ] [tool.hatch.build.targets.wheel] packages = ["schemabrain"] # Bundled data files inside the package directory. Hatchling's wheel # default would include these (no `exclude` shadows them today), but # pinning them explicitly via `artifacts` makes the contract durable — # a future contributor can't silently drop them by adding an `exclude` # pattern. Both files are referenced by `DEFAULT_GOLDEN_PATH` and the # eval CLI quickstart respectively; losing either breaks the # "pip install + 3 commands works" Preview-DoD line. artifacts = [ "schemabrain/eval/golden_sets/*.json", "schemabrain/eval/fixtures/*.sql", "schemabrain/eval/fixtures/entities/**/*.yaml", "schemabrain/imports/fixtures/*.json", "schemabrain/joins/fixtures/**/*.yaml", "schemabrain/metrics/fixtures/**/*.yaml", # Pre-built Next.js static export shipped with the [ui] extra. # Populated by `pnpm --dir web build` at release time (see # `schemabrain/dashboard/static/README.md`). The `**` glob picks # up nested `_next/static/...` assets without listing every chunk. "schemabrain/dashboard/static/**/*", ] [tool.ruff] line-length = 100 target-version = "py311" [tool.ruff.lint] select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "UP", # pyupgrade "SIM", # flake8-simplify "RUF", # ruff-specific ] ignore = [ "E501", # line too long (handled by formatter) ] [tool.ruff.format] quote-style = "double" indent-style = "space" [tool.pytest.ini_options] minversion = "8.0" # Default `pytest` invocation skips `slow` tests (currently the # pytest-benchmark perf gate — Phase A Commit 4). Local devs and CI's # default-PR job stay fast; the dedicated `slow` CI job and on-demand # runs explicitly pass `-m slow` to exercise the perf gate. Integration # tests are NOT excluded by default — they run in the regular CI suite # alongside Docker — but can be skipped locally with `-m "not integration"`. addopts = "-ra --strict-markers --strict-config -m 'not slow and not firewall_bypass'" testpaths = ["tests"] markers = [ "integration: tests that require Docker (testcontainers). Skip with -m 'not integration'.", "slow: long-running tests (e.g. pytest-benchmark perf gates). Skipped by default via `addopts`; run explicitly with `pytest -m slow`.", "firewall_bypass: firewall-bypass repros under tests/firewall/. Each test asserts the SECURE behaviour and is expected to FAIL on `main` until the corresponding fix lands. Run with `pytest -m firewall_bypass`.", ] filterwarnings = [ "error", "ignore::DeprecationWarning:pkg_resources.*", "ignore::DeprecationWarning:testcontainers.*", # OpenTelemetry's internal `_importlib_metadata.py` shim still calls # the deprecated `EntryPoints.values()` API on Python 3.10+ (see # `opentelemetry/util/_importlib_metadata.py:32`). The deprecation # is in a third-party library, not in code we control, and will # resolve when upstream OTel updates its shim. Suppress here so # the warning doesn't tip the strict-warnings filter into a test # collection failure on every OTel SDK ≥1.42 bump. "ignore::DeprecationWarning:opentelemetry.*", # Starlette 1.3.x moved its `TestClient` HTTP backend to `httpx2` # (Tom Christie / Pydantic's httpx successor): it imports `httpx2` # first and falls back to the still-supported `httpx` with a # `StarletteDeprecationWarning`. We keep the `httpx` fallback for now # rather than add a ~1-week-old package to the dev deps, so suppress # the warning here to keep the strict-warnings filter from failing # the `[ui]` sidecar TestClient tests at import. TODO: adopt `httpx2` # when it has more track record / before Starlette removes the # `httpx` fallback. "ignore::starlette.exceptions.StarletteDeprecationWarning", ] [tool.coverage.run] source = ["schemabrain"] branch = true [tool.coverage.report] exclude_lines = [ "pragma: no cover", "raise NotImplementedError", "if TYPE_CHECKING:", "if __name__ == .__main__.:", "^\\s*\\.\\.\\.\\s*$", # `def name(...) -> T: ...` — Protocol method declarations. # The body is structurally unreachable (Protocols aren't # instantiated); excluding the line keeps coverage honest. "^\\s*def\\s.*:\\s*\\.\\.\\.\\s*$", ] [tool.bandit] # Scan only the runtime package — tests intentionally use `assert` # (B101) and binding stubs against fake URLs that look like SQLi to # bandit. Scope keeps the signal high. exclude_dirs = ["tests", "scripts", ".venv", "build", "dist"] # Per-line `# nosec B608` comments justify the only deliberate # exceptions today (identifier-only f-string SQL assembled from # SQLAlchemy `IdentifierPreparer.quote()` output — no user input). # See `schemabrain/profiler/postgres.py:_fetch_aggregates` / # `_fetch_samples` for the in-source justification.