Release 0.13.0 - #281
Conversation
New tests/api/ suite tests bedhost against any deployed instance via --api_root flag. Covers service-info, stats, BED/BEDSET CRUD, search, and OpenAPI docs endpoints. Removes old test_fastapi stubs and test_compliance.py.
One-command, parallel-safe local stack (ephemeral Postgres + Qdrant) plus
a tests/integration/ tree that runs the 37 compliance tests via FastAPI
TestClient against the ephemeral DB. The existing black-box tests/api/
suite stays runnable against real deployments.
- tests/scripts/services.sh: start/stop/status for postgres:17 and
qdrant/qdrant:latest containers. Unique names (PID-suffixed) and
randomized ports for parallel safety. tmpfs mounts for speed. pg_isready
and qdrant /readyz health-wait loops.
- tests/scripts/test-integration.sh: orchestrator. trap cleanup EXIT INT
TERM, pre-computes ports so cleanup has them. Exports RUN_INTEGRATION_TESTS,
TEST_DB_URL, TEST_QDRANT_URL, BEDHOST_INIT_ML=false, then runs pytest.
- tests/fixtures/bedbase_test_config.template.yaml: minimal bbconf YAML
with {DB_PORT} / {QDRANT_PORT} placeholders.
- tests/integration/conftest.py: renders the template to tmp_path, sets
BEDBASE_CONFIG, imports bedhost.main.app, wraps TestClient in a
TestClientAdapter. Gates the whole directory with a
pytest_collection_modifyitems skip when RUN_INTEGRATION_TESTS != "true".
- tests/client.py: RequestsClient and TestClientAdapter expose the same
.get/.post surface so the 37 compliance tests run unchanged in both
modes.
- tests/conftest.py: api_root fixture now returns a client object (not a
URL string). In integration mode it delegates to integration_api_root;
otherwise it builds a RequestsClient from --api_root.
- tests/api/test_compliance.py: mechanical rewrite of all 37 tests to use
api_root.get("/v1/...") instead of requests.get(f"{api_root}/v1/...").
- tests/api/conftest.py: example_bed_id / example_bedset_id fixtures use
the new adapter.
- README.md: "Testing" section documenting both modes.
Plain `pytest` still collects all 38 tests and skips cleanly when
RUN_INTEGRATION_TESTS is unset and no server is reachable.
No version pins added. No backwards compatibility (api_root return type
changed).
…egration mode configure() in bedhost/helpers.py now reads BEDHOST_INIT_ML from the env and passes init_ml= through to BedBaseAgent. Default remains True so production behavior is unchanged. With BEDHOST_INIT_ML=false (set by test-integration.sh), bbconf skips loading the dense/sparse encoders, UMAP, and region2vec - which matters for the integration test stack because the installed fastembed/torch versions are incompatible with bbconf's current ML init path. Also expand test-integration.sh to run both tests/integration/ and tests/api/, so the 37 compliance tests execute against the in-process TestClient (not just the smoke test).
The yacman/bbconf stack registers SIGINT/SIGTERM handlers in
YacAttMap.__enter__, which raises ValueError under FastAPI TestClient
since lifespan runs on an AnyIO worker thread. Monkey-patch
signal.signal to swallow that specific case so bbconf can finish
loading its config during test startup.
Also tighten example_{bed,bedset}_id fixture timeouts from 10s to 3s
to fail fast when the endpoint errors instead of blocking a test run.
- Move FastAPI's OpenAPI schema from /openapi.json to /v1/openapi.json so it lives under the same /v1 prefix as the rest of the API. The /v1 regression test and the compliance test now agree on the path. - render_markdown() now raises HTTPException(404) when the markdown source isn't on disk instead of propagating FileNotFoundError as a 500. docs/changelog.md was removed from the repo in 2024 but the /v1/docs/changelog route remained registered; this makes it behave gracefully when the file is absent without changing output when the file is present.
When bbconf is initialized with init_ml=False (BEDHOST_INIT_ML=false for CI / smoke deploys), BedBaseConfig never sets dense_encoder at all. Hybrid search deep in bbconf then raises AttributeError and surfaces as a 500. Return 503 up-front when dense_encoder is absent, matching the pattern already used by /v1/bed/analyze-genome for the reference validator.
- count_requests(): when the wrapped endpoint has query=None (as /v1/bedset/list does when no search term is supplied), the decorator called None.strip() and crashed with AttributeError. Skip usage tracking in that case. This is the root cause of the 500 seen on prod for GET /v1/bedset/list with no query param. - fetch_detailed_stats(): bbconf.bbagent._bin_number_of_regions calls statistics.mean on a list of per-bed region counts, which raises StatisticsError on an empty database. Catch it and return a FileStats with empty bins/zeroed means so the endpoint stays a 200 on a freshly-provisioned instance. Same failure was observed on prod.
- TestBedExampleEndpoint and TestBedsetExampleEndpoint now skip when
the corresponding example_{bed,bedset}_id fixture is empty (no
seeded data), matching the pattern already used by the
metadata tests. Structural schema is still exercised against
seeded instances.
- TestBedSearchEndpoints.test_text_search_* accept 503 (from the
new ML-disabled guard) in addition to 200, and skip the
shape-of-results check when 503 is returned.
- test_openapi_json_available now points at /v1/openapi.json,
matching the new mount path.
- test_v1_changelog_returns_200 renamed to ..._returns_200_or_404
since docs/changelog.md may or may not be present on disk.
SQLAlchemy queries plus numpy work, which would otherwise stall the event loop (and every other request) for the full duration. FastAPI dispatches plain def handlers to a thread pool instead.
Move logging setup out of __init__.py
Efficiency improvements
Add bulk metadata exports: listing endpoint, DRS objects, downloads page
There was a problem hiding this comment.
Pull request overview
This release PR updates both the FastAPI service and the UI to improve runtime safety (threadpool dispatch for blocking routes, safer detailed-stats caching), expand download/index functionality (new /exports + /files indexes and corresponding UI pages), and overhaul testing with a new compliance+integration test harness.
Changes:
- Added new API indexes for bulk exports and analysis files, plus new UI pages/routes to browse and download them.
- Refactored detailed-stats caching to use
app.statelifecycle-scoped cache with an empty-dataset fallback. - Replaced/modernized tests with a dual-mode compliance suite (black-box and ephemeral integration stack scripts/fixtures).
Reviewed changes
Copilot reviewed 44 out of 47 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/src/queries/useStats.ts | Extends React Query caching settings for stats queries. |
| ui/src/queries/useExports.ts | Adds a typed React Query hook for the new exports index endpoint. |
| ui/src/queries/useAnalysisFiles.ts | Adds a typed React Query hook for the new analysis-files index endpoint (with filters). |
| ui/src/pages/downloads.tsx | Adds a “Bulk exports” downloads UI, grouped by snapshot time, with copyable checksum and DuckDB snippet. |
| ui/src/pages/bed-splash.tsx | Updates bedset card/table mapping to use bedfile_count. |
| ui/src/pages/analysis-files.tsx | Adds an “Analysis files” UI to browse standalone analysis artifacts. |
| ui/src/main.tsx | Registers new frontend routes for /downloads and /files. |
| ui/src/components/search/text2bedset/text2bedset.tsx | Updates pagination wiring to the new hasMore pagination model. |
| ui/src/components/search/text2bedset/t2bs-search-results-table.tsx | Displays bedfile_count instead of deriving count from bed_ids. |
| ui/src/components/search/text2bedset/t2bs-search-results-cards.tsx | Displays bedfile_count in the card view. |
| ui/src/components/search/text2bed/text2bed.tsx | Updates pagination wiring to the new hasMore pagination model. |
| ui/src/components/search/search-bar.tsx | Adds a max-width style for the genome select to prevent layout overflow. |
| ui/src/components/search/pagination-bar.tsx | Refactors pagination UI to operate without a true total, using hasMore. |
| ui/src/components/search/bed2bed/bed2bed.tsx | Updates pagination wiring to the new hasMore pagination model. |
| ui/src/components/nav/nav-mobile.tsx | Adds mobile nav links for the new Downloads and Analysis files pages. |
| ui/src/components/nav/nav-desktop.tsx | Moves secondary links into a “More” dropdown and adds links to new pages. |
| ui/bedbase-types.d.ts | Extends generated types to include bedfile_count on bedset models. |
| tests/test_fastapi/test_config.yaml | Removes legacy fastapi test config. |
| tests/test_fastapi/test_api.py | Removes legacy (skipped) fastapi test module. |
| tests/test_fastapi/README.md | Removes legacy test setup notes superseded by new scripts/docs. |
| tests/test_compliance.py | Removes legacy compliance tests superseded by new suite. |
| tests/scripts/test-integration.sh | Adds an integration-test runner that boots ephemeral services and runs pytest. |
| tests/scripts/services.sh | Adds helper script to start/stop ephemeral Postgres + Qdrant for tests. |
| tests/integration/test_integration_smoke.py | Adds a smoke test validating the ephemeral stack boots and serves /v1/service-info. |
| tests/integration/conftest.py | Adds integration fixtures, config templating, and directory-level skip gating. |
| tests/integration/init.py | Marks integration test package. |
| tests/fixtures/bedbase_test_config.template.yaml | Adds bbconf config template filled with ephemeral service ports. |
| tests/conftest.py | Adds top-level fixture dispatch between black-box and integration modes + service availability skips. |
| tests/client.py | Adds client adapters so compliance tests can run against requests or TestClient. |
| tests/api/test_compliance.py | Introduces a comprehensive compliance test suite for core endpoints and regressions. |
| tests/api/conftest.py | Adds fixtures to discover example IDs when available. |
| tests/api/init.py | Marks API tests package. |
| requirements/requirements-all.txt | Bumps bbconf/logmuse minimum versions to support new behavior. |
| README.md | Adds Testing section documenting black-box and integration test workflows. |
| pytest.ini | Adds pytest configuration and marker registration. |
| bedhost/templates/page.html | Updates footer asset to load from the app’s /static mount. |
| bedhost/routers/objects_api.py | Adds DRS object endpoints for exports and analysis files. |
| bedhost/routers/bedset_api.py | Converts several endpoints to sync def for threadpool execution of blocking work. |
| bedhost/routers/bed_api.py | Converts several endpoints to sync def and adds an explicit 503 when ML is disabled for text search. |
| bedhost/routers/base_api.py | Adds /v1/exports and /v1/files index endpoints; wires detailed-stats via request-scoped cache. |
| bedhost/main.py | Centralizes logging init, mounts /static, tweaks OpenAPI URL, and improves markdown missing-file handling. |
| bedhost/helpers.py | Adds BEDHOST_INIT_ML handling and ensures decorated sync endpoints run in a threadpool; hardens usage tracking. |
| bedhost/dependencies.py | Moves detailed-stats caching to app.state and adds empty-dataset fallback for StatisticsError. |
| bedhost/const.py | Adds EXPORTS_URL_BASE constant for CDN URL rewriting. |
| bedhost/_version.py | Bumps bedhost version constant (currently to 0.12.8). |
| bedhost/init.py | Removes logger init side effects; leaves a standard library logger handle. |
Suppressed comments (2)
bedhost/routers/base_api.py:186
os.path.join(EXPORTS_URL_BASE, row.file_path)is not safe for constructing HTTPS URLs (it can drop the base ifrow.file_pathstarts with/, and it's not URL-aware). Prefer explicit URL concatenation orurllib.parse.urljoin.
Same pattern also exists in bedhost/routers/base_api.py:145 and bedhost/routers/objects_api.py:292,328.
result.results = [
row.model_copy(
update={"file_path": os.path.join(EXPORTS_URL_BASE, row.file_path)}
)
bedhost/routers/objects_api.py:329
os.path.join(EXPORTS_URL_BASE, row.file_path)is not safe for constructing HTTPS URLs (it can drop the base ifrow.file_pathstarts with/, and it's not URL-aware). Prefer explicit URL concatenation orurllib.parse.urljoinwhen building AccessURL.
Same issue also appears in bedhost/routers/objects_api.py:292 and bedhost/routers/base_api.py:145,185.
AccessMethod(
type="https",
access_id="https",
access_url=AccessURL(url=os.path.join(EXPORTS_URL_BASE, row.file_path)),
)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| <Dropdown.Item href='/downloads'> | ||
| <i className='bi bi-download me-2' /> | ||
| Downloads | ||
| </Dropdown.Item> | ||
| <Dropdown.Item href='/files'> | ||
| <i className='bi bi-file-earmark-binary me-2' /> | ||
| Analysis files | ||
| </Dropdown.Item> | ||
| <Dropdown.Item href='https://github.com/databio/bedhost' target='_blank'> | ||
| <i className='bi bi-github me-2' /> | ||
| GitHub | ||
| </Dropdown.Item> | ||
| <Dropdown.Item href='https://docs.bedbase.org/bedbase/' target='_blank'> | ||
| <i className='bi bi-file-earmark-text me-2' /> | ||
| Docs | ||
| </Dropdown.Item> | ||
| <Dropdown.Item href={`${API_BASE}`} target='_blank'> | ||
| <i className='bi bi-hdd-stack me-2' /> | ||
| API | ||
| </Dropdown.Item> |
| # yacman (pulled in transitively by bbconf/bedhost) registers SIGINT/SIGTERM | ||
| # handlers in YacAttMap.__enter__, which raises ``ValueError: signal only | ||
| # works in main thread of the main interpreter`` under FastAPI TestClient | ||
| # (lifespan runs on an AnyIO worker thread). Swallow those so bbconf can | ||
| # finish loading its config. Tests don't need graceful signal handling. | ||
| _real_signal = _signal_mod.signal | ||
|
|
||
|
|
||
| def _signal_main_thread_only(signum, handler): | ||
| try: | ||
| return _real_signal(signum, handler) | ||
| except ValueError: | ||
| return handler | ||
|
|
||
|
|
||
| _signal_mod.signal = _signal_main_thread_only |
| cd "$PROJECT_ROOT" | ||
|
|
||
| python3 -m pytest tests/integration/ tests/api/ "$@" | ||
| TEST_EXIT_CODE=$? | ||
|
|
||
| echo "----------------------------------------------" | ||
| exit $TEST_EXIT_CODE |
| if concise not in request.app.state.detailed_stats: | ||
| _LOGGER.info("Stats are not cached, fetching...") | ||
| try: | ||
| request.app.state.detailed_stats[concise] = bbagent.get_detailed_stats( | ||
| concise=concise | ||
| ) | ||
| except StatisticsError: | ||
| return _empty_file_stats() | ||
| return request.app.state.detailed_stats[concise] |
| result = bbagent.snapshot.list(limit=limit, offset=offset) | ||
| result.results = [ | ||
| row.model_copy( | ||
| update={"file_path": os.path.join(EXPORTS_URL_BASE, row.file_path)} | ||
| ) |
| AccessMethod( | ||
| type="https", | ||
| access_id="https", | ||
| access_url=AccessURL(url=os.path.join(EXPORTS_URL_BASE, row.file_path)), | ||
| ) |
| docker run -d \ | ||
| --name "$QDRANT_CONTAINER" \ | ||
| -p "${QDRANT_PORT}:6333" \ | ||
| --tmpfs /qdrant/storage \ | ||
| qdrant/qdrant:latest |
| @@ -1 +1 @@ | |||
| __version__ = "0.12.7" | |||
| __version__ = "0.12.8" | |||
Changes:
dependencies.pyto usingapp.state.detailed_stats(aTTLCache), ensuring thread safety and correct cache lifecycle management. Added a fallback for empty datasets to avoidStatisticsErrorand return a well-formed, zeroedFileStatsobject.async deftodefto ensure blocking database queries run in a threadpool, preventing event loop stalls./exportsand/filesto provide indexes of published bulk metadata exports and standalone analysis files, respectively. These endpoints rewritefile_pathfields to the absolute HTTPS CDN URL using the newEXPORTS_URL_BASEconstant.Testingsection to theREADME.mdwith instructions for running black-box and integration tests, as well as manual service control. Also clarified environment variable usage for ML model initialization in CI and production. (configurefunction to respect theBEDHOST_INIT_MLenvironment variable, allowing deployments to skip expensive ML model loading when not needed.main.py(now the application entry point), usinglogmusefor logger initialization and setting log levels for dependencies. Removed logger initialization from__init__.pyto avoid duplicate configuration.None, preventing potential crashes.TODO:
__version__.pyfile