sharpit
An embedded, local-only developer console and automated test gate for FastAPI apps.
Gate CI on findings
sharpit turns runtime capture into automated test gates with pytest --sharpit. It tracks SQL
statements and observable exceptions per test, detects N+1 queries, checks query budgets, and runs
static analysis advisors. If a test exceeds its query budget, triggers an N+1 query, or swallows an
unhandled exception, the build fails. When capture cannot see enough to judge, the run status is
unknown and fails closed.
pytest --sharpit --sharpit-report=sharpit-report.json --sharpit-junit=sharpit-junit.xml
You no longer need --sharpit-app for a typical suite: sharpit picks the one FastAPI or Starlette app defined in the
project modules your tests already imported (it never imports a module to look). With none, several, or one without
routes, the REST API advisor is SKIPPED and a clean suite still passes; --sharpit-require-app makes that unknown
again, as in 0.3.0. The terminal gate counts: lists only findings that fail the gate; --sharpit-verbose (or -vv)
lists every one. The JSON report and JUnit file always hold every finding.
Adopting on an existing suite
Start without breaking the build, then freeze what is already there:
pytest --sharpit --sharpit-report-only # report everything, fail on nothing but blindness
pytest --sharpit --sharpit-write-baseline=sharpit-baseline.json # after a complete run: record today's findings
pytest --sharpit --sharpit-baseline=sharpit-baseline.json # in CI: only new findings fail
- Report-only (
--sharpit-report-only, inisharpit_report_only = true) runs every gate and advisor and writes the report, but findings do not fail tests or the session. An unknown reason still exits 1 unless--sharpit-allow-unknown: report-only relaxes findings, never blindness. - Baselines match findings by fingerprint. A listed finding is
existingand never fails, an unlisted one isnewand gates as usual, a listed one not seen isfixed. A count gate that grows past its baselined count isnew. Vulnerability findings are not baselined in 0.4.0, and INFO findings never fail. --sharpit-write-baseline=PATHis the only way a baseline is written. It refuses a partial run (-k,-m, node ids,--lf,--deselect,-xstopping early, an interrupt) and a run with any unknown reason.- A missing or malformed baseline never switches sharpit off: the session runs and the baseline becomes an unknown reason, which fails the run. Own the baseline file in CODEOWNERS, or a PR can baseline its own findings.
GitHub Action
The repository root is a composite action (action.yml) that runs pytest with sharpit and writes a findings summary to
the job summary. Pin it to a release tag:
- uses: ffroliva/sharpit@<release tag>
with:
pytest-args: "-q tests/"
baseline: sharpit-baseline.json
Inputs: pytest-args (split with Python shlex), report-only (default "true": findings are reported, never fail the
build), baseline, fail-on (lowest severity that fails), report-path (default sharpit-report.json), install
(pip installs the pinned sharpit; none when your project already depends on it, in which case python must be
able to import sharpit) and runner (default python -m pytest, for example uv run pytest). Set report-only: "false"
to gate. The pytest exit code is never masked: the summary step runs if: always() and only warns if it fails. The
summary is rendered by python -m sharpit.report summary <report.json>. On Windows runners runner and pytest-args
are split in non-POSIX mode so backslashes in paths survive.
Outside proof: gflow-cli
Run by sharpit's author on the 0.3.0 integration branch against gflow-cli (a public FastAPI daemon using raw sqlite3; see sharpit's own report), sharpit ran across 5,662 tests (5 failed by sharpit gates):
- 0 unattributed SQL statements across ~53k statements executed (27,997 attributed + 25,092 filtered);
- 6 N+1 findings, of which 1 was a real defect, 2 were noise (filtered since 0.3.0), and 3 were expected polling;
- Proved out-of-code runtime observation without changing production code.
60 seconds
Install sharpit with the FastAPI host extra:
pip install "sharpit[fastapi]"
# or
uv add "sharpit[fastapi]"
Add one call to your FastAPI app:
from fastapi import FastAPI
import sharpit
app = FastAPI()
# Register your application middleware first, then mount sharpit:
sharpit.install(app) # mounts /sharpit and /sharpit/api
Run tests with sharpit gate reporting:
pytest --sharpit --sharpit-report=sharpit-report.json
Run it locally with a dev signal and open http://127.0.0.1:8000/sharpit/:
fastapi dev app.py # or: uvicorn app:app --reload
Overview
One call mounts a web console at /sharpit and a JSON API at /sharpit/api inside your own app,
so a running service becomes understandable in minutes: its configuration and where each value
came from, every route, the dependency providers, loggers you can retune live, and a vulnerability
scan of the installed packages.
sharpit is a Python/FastAPI port of BootUI by Julien Dubois (Apache-2.0), the Spring Boot developer console. It ports BootUI's design goals, JSON contract and test cases; the UI is new.
Status: alpha (0.4.0, FastAPI only). Django and Flask are not supported. See CHANGELOG.md.
Installation and extras
Python 3.12+. Runtime dependencies are only starlette>=0.44 and packaging>=23.
pip install "sharpit[fastapi]"
Optional extras:
| Extra | Adds | Why |
|---|---|---|
sharpit[fastapi] |
fastapi>=0.115 |
the supported host |
sharpit[settings] |
pydantic-settings>=2.4 |
the Config panel shows which source each value came from |
Configuration and activation
Call sharpit.install(app) after adding your own middleware. Starlette builds middleware from the outside in, so
calling install() last ensures sharpit's capture middleware sits outside your application middleware. This lets sharpit
measure total request durations and record unhandled errors correctly.
Keyword arguments for sharpit.install():
engines: optional sequence of SQLAlchemyEngineorAsyncEngineinstances to monitor in connection pools and health checks (e.g.engines=[engine]). If omitted, engines are discovered automatically on first connection.health_checks: optional mapping of check names to zero-argument callables (e.g.health_checks={"cache": ping_cache}).alembic_config: optional path toalembic.ini(default"alembic.ini").settings: optional pydantic-settings instance or sequence of instances to show configuration provenance.models: optional SQLAlchemy 2.0 declarative base, registry, or a list of them or of mapped classes, for the Data panel (e.g.models=Base). sharpit never configures mappers or runs SQL to read them.
With a PostgreSQL engine, the PostgreSQL panel reads vital signs, sessions, statements, indexes, tables, vacuum,
replication and settings on an explicit POST /sharpit/api/postgresql/read: one read-only transaction with statement
and lock timeouts, a read budget and row limits, query text masked. GET returns the last read and never touches the database.
The sqlalchemy-statistics panel (BootUI's hibernate-statistics; GET {api}/sqlalchemy-statistics, MCP
get_sqlalchemy_statistics) shows statement counts from SQL Trace's lifetime totals, the slowest statement and N+1
candidates per route. SQLAlchemy reports SQL statements, not ORM sessions or entities, so the Hibernate-only counters
(sessions, flushes, transactions, entity and collection counts) are null, each with a reason. It needs SQL Trace, and
reports statistics.warnings when SQL Trace recording is paused.
Other options are keyword arguments or environment variables, see Settings.
If /sharpit answers 404, read Activation: sharpit stays off unless a dev signal turns it on.
Documentation
The full documentation is at https://ffroliva.github.io/sharpit/:
- Activation: when sharpit turns on, and why a plain
uvicorn app:appgives 404 - Panels: what each panel shows
- Pytest plugin: CI gates for queries, N+1, exceptions, and advisors
- Test report: the
sharpit-report/1JSON schema and JUnit XML - SQL sources: tracing SQLAlchemy and raw
sqlite3 - MCP for AI agents: the read-only MCP endpoint and its tools
- Settings: every environment variable and
install()keyword - Safety model: local-only guard, tokens, masking, proxies and workers
- How it works and Deviations from BootUI
License
Apache License 2.0. See LICENSE and NOTICE. sharpit is derived from BootUI, Copyright Julien Dubois.
Metadata
Release files for sharpit 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sharpit-0.4.0.tar.gz | 977.0 kB | With provenance | Details for sharpit-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sharpit-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for sharpit-0.4.0-py3-none-any.whl |
Total release size: 1.3 MB
Security for sharpit 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sharpit-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.