Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bedhost/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.12.6"
__version__ = "0.12.7"
6 changes: 3 additions & 3 deletions bedhost/data_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class BedList(BaseModel):
BedDigest = Path(
...,
description="BED digest",
regex=r"^\w+$",
pattern=r"^\w+$",
max_length=32,
min_length=32,
# example=ex_bed_digest,
Expand All @@ -29,8 +29,8 @@ class BedList(BaseModel):
CROM_NUMBERS = Path(
...,
description="Chromosome number",
regex=r"^\S+$",
example=ex_chr,
pattern=r"^\S+$",
examples=[ex_chr],
)


Expand Down
30 changes: 25 additions & 5 deletions bedhost/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,28 @@
from cachetools import cached, TTLCache
from .main import bbagent
from bbconf.models.base_models import FileStats
from bbconf.bbagent import BedBaseAgent
from bbconf.models.base_models import FileStats, UsageModel
from bedboss.refgenome_validator.main import ReferenceValidator
from fastapi import Request


@cached(TTLCache(maxsize=100, ttl=14 * 24 * 60 * 60))
def fetch_detailed_stats(concise: bool = False) -> FileStats:
def get_bbagent(request: Request) -> BedBaseAgent:
return request.app.state.bbagent


def get_usage_data(request: Request) -> UsageModel:
return request.app.state.usage_data


def get_ref_validator(request: Request) -> ReferenceValidator:
return request.app.state.ref_validator


def fetch_detailed_stats(bbagent: BedBaseAgent, concise: bool = False) -> FileStats:
"""
Fetch detailed file statistics from the BedBaseAgent.

The previous implementation cached this with a 14-day TTL keyed on the
``concise`` flag. With bbagent now flowing through FastAPI dependencies
(and process lifetimes typically shorter than the old TTL anyway), the
cache has been removed — the underlying query lives in Postgres.
"""
return bbagent.get_detailed_stats(concise=concise)
45 changes: 40 additions & 5 deletions bedhost/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import datetime
from bbconf.bbagent import BedBaseAgent
from bbconf.models.base_models import UsageModel
from starlette.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi import Query
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi import Query, Request

from . import _LOGGER
from .exceptions import BedHostException
Expand Down Expand Up @@ -89,13 +89,14 @@ def drs_response(status_code, msg):


def count_requests(
usage_data: UsageModel,
event: Literal["bed_search", "bedset_search", "bed_meta", "bedset_meta", "files"],
):
"""
Decorator to count requests for different events
Decorator to count requests for different events.

The wrapped endpoint must accept ``request: Request``; the usage data model
is read from ``request.app.state.usage_data`` per-request.

:param UsageModel usage_data: usage data model
:param str event: event type
"""

Expand All @@ -109,6 +110,13 @@ async def wrapper(*args, **kwargs):
f"Test request was executed. For '{event}' event with: {args}, {kwargs}. No results saved."
)
return function_result
request = kwargs.get("request")
if request is None:
raise RuntimeError(
f"count_requests decorator requires the wrapped endpoint "
f"'{func.__name__}' to accept a 'request: Request' parameter."
)
usage_data: UsageModel = request.app.state.usage_data
if event == "files":
file_path = kwargs.get("file_path")
if "bed" in file_path or "bigbed" in file_path.lower():
Expand Down Expand Up @@ -159,3 +167,30 @@ def init_model_usage():
files={},
date_from=datetime.datetime.now(),
)


def upload_usage(bbagent: BedBaseAgent, usage_data: UsageModel) -> None:
"""
Upload usage data to the database and reset the usage data in place.

:param BedBaseAgent bbagent: the bbconf agent used to persist usage records
:param UsageModel usage_data: the live usage model to flush and reset
"""
from .const import USAGE_RECORD_DAYS

_LOGGER.info("Running uploading of the usage")
usage_data.date_to = datetime.datetime.now() + datetime.timedelta(
days=USAGE_RECORD_DAYS
)
try:
bbagent.add_usage(usage_data)
except Exception as e:
_LOGGER.error(f"Error while uploading usage data: {e}")

usage_data.bed_meta = {}
usage_data.bedset_meta = {}
usage_data.bed_search = {}
usage_data.bedset_search = {}
usage_data.files = {}
usage_data.date_from = datetime.datetime.now()
usage_data.date_to = None
156 changes: 85 additions & 71 deletions bedhost/main.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import sys
import datetime
from contextlib import asynccontextmanager
from pathlib import Path

import markdown
import uvicorn
from apscheduler.schedulers.background import BackgroundScheduler
from bbconf.exceptions import (
BEDFileNotFoundError,
BedSetNotFoundError,
Expand All @@ -20,9 +22,14 @@
from . import _LOGGER
from ._version import __version__ as bedhost_version
from .cli import build_parser
from .const import PKG_NAME, STATIC_PATH, USAGE_SAVE_HOURS, USAGE_RECORD_DAYS
from .helpers import attach_routers, configure, drs_response, init_model_usage
from apscheduler.schedulers.background import BackgroundScheduler
from .const import PKG_NAME, STATIC_PATH, USAGE_SAVE_HOURS
from .helpers import (
attach_routers,
configure,
drs_response,
init_model_usage,
upload_usage,
)

tags_metadata = [
{
Expand Down Expand Up @@ -51,12 +58,67 @@
},
]


@asynccontextmanager
async def lifespan(app: FastAPI):
"""
Application lifespan handler.

Startup:
- Read BEDBASE_CONFIG from env (raise EnvironmentError if missing).
- Build the BedBaseAgent, UsageModel, and ReferenceValidator.
- Stash them on app.state for dependency injection.
- Start the BackgroundScheduler that periodically flushes usage data.

Shutdown:
- Stop the scheduler so it does not leak across reloads.
"""
bbconf_file_path = os.environ.get("BEDBASE_CONFIG")
if not bbconf_file_path:
raise EnvironmentError(
"No BEDBASE_CONFIG found. Can't configure server. "
"Check documentation to create config file"
)

_LOGGER.info(f"Running {PKG_NAME} app...")
app.state.bbagent = configure(bbconf_file_path)
app.state.usage_data = init_model_usage()

# Respect BEDHOST_INIT_ML for CI/smoke deployments that don't need the
# reference genome validator loaded. Default is to initialize it.
init_ml_env = os.environ.get("BEDHOST_INIT_ML", "true").lower()
if init_ml_env in ("0", "false", "no"):
_LOGGER.info(
"BEDHOST_INIT_ML=false; skipping ReferenceValidator initialization."
)
app.state.ref_validator = None
else:
_LOGGER.info("Initializing reference genome validator...")
app.state.ref_validator = ReferenceValidator()

scheduler = BackgroundScheduler()
scheduler.add_job(
upload_usage,
"interval",
hours=USAGE_SAVE_HOURS,
args=(app.state.bbagent, app.state.usage_data),
)
scheduler.start()
app.state.scheduler = scheduler

try:
yield
finally:
app.state.scheduler.shutdown(wait=False)


app = FastAPI(
title=PKG_NAME,
description="BED file/sets statistics and image server API",
version=bedhost_version,
docs_url="/v1/docs",
openapi_tags=tags_metadata,
lifespan=lifespan,
)

origins = [
Expand All @@ -75,7 +137,7 @@
allow_headers=["*"],
)

templates = Jinja2Templates(directory="bedhost/templates")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
templates.env.autoescape = False


Expand Down Expand Up @@ -106,9 +168,7 @@ def render_markdown(filename: str, request: Request):
with open(os.path.join(STATIC_PATH, filename), "r", encoding="utf-8") as input_file:
text = input_file.read()
content = markdown.markdown(text)
return templates.TemplateResponse(
"page.html", {"request": request, "content": content}
)
return templates.TemplateResponse(request, "page.html", {"content": content})


@app.exception_handler(MissingThumbnailError)
Expand All @@ -127,10 +187,16 @@ async def exc_handler_BedSetNotFoundError(req: Request, exc: BedSetNotFoundError


@app.exception_handler(MissingObjectError)
async def exc_handler_BedSetNotFoundError(req: Request, exc: MissingObjectError):
async def exc_handler_MissingObjectError(req: Request, exc: MissingObjectError):
return drs_response(404, "Object not found.")


# Router endpoints use Depends() to resolve bbagent/usage_data/ref_validator
# from app.state at request time, so attaching at module import is safe
# regardless of lifespan ordering.
attach_routers(app)


def main():
parser = build_parser()
args = parser.parse_args()
Expand All @@ -140,71 +206,19 @@ def main():
sys.exit(1)

if args.command == "serve":
_LOGGER.info(f"Running {PKG_NAME} app...")
bbconf_file_path = args.config or os.environ.get("BEDBASE_CONFIG") or None
if bbconf_file_path:
os.environ["BEDBASE_CONFIG"] = bbconf_file_path

global bbagent
# Load config once just to pull host/port out. Lifespan will rebuild
# the agent in the uvicorn worker process.
bbagent = configure(bbconf_file_path)
host = bbagent.config.config.server.host
port = bbagent.config.config.server.port

_LOGGER.info("Initializing reference genome validator...")
global ref_validator
ref_validator = ReferenceValidator()

attach_routers(app)
_LOGGER.info(f"Running {PKG_NAME} app on {host}:{port}...")
uvicorn.run(
app,
host=bbagent.config.config.server.host,
port=bbagent.config.config.server.port,
)


if __name__ != "__main__":
if os.environ.get("BEDBASE_CONFIG"):
import logging

_LOGGER.setLevel(logging.DEBUG)
_LOGGER.info(f"Running {PKG_NAME} app...")
bbconf_file_path = os.environ.get("BEDBASE_CONFIG") or None
global bbagent
global usage_data
global ref_validator

bbagent = configure(
bbconf_file_path
) # configure before attaching routers to avoid circular imports
usage_data = init_model_usage()

ref_validator = ReferenceValidator()

scheduler = BackgroundScheduler()

def upload_usage():
"""
Upload usage data to the database and reset the usage data
"""

_LOGGER.info("Running uploading of the usage")
usage_data.date_to = datetime.datetime.now() + datetime.timedelta(
days=USAGE_RECORD_DAYS
)
try:
bbagent.add_usage(usage_data)
except Exception as e:
_LOGGER.error(f"Error while uploading usage data: {e}")

usage_data.bed_meta = {}
usage_data.bedset_meta = {}
usage_data.bed_search = {}
usage_data.bedset_search = {}
usage_data.files = {}
usage_data.date_from = datetime.datetime.now()
usage_data.date_to = None

scheduler.add_job(upload_usage, "interval", hours=USAGE_SAVE_HOURS)
scheduler.start()

attach_routers(app)
else:
raise EnvironmentError(
"No BEDBASE_CONFIG found. Can't configure server. Check documentation to create config file"
"bedhost.main:app",
host=host,
port=port,
)
11 changes: 9 additions & 2 deletions bedhost/og_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,21 @@ def _font(name: str, size: int) -> ImageFont.FreeTypeFont:
return ImageFont.load_default()


def _centered_x(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.FreeTypeFont) -> int:
def _centered_x(
draw: ImageDraw.ImageDraw, text: str, font: ImageFont.FreeTypeFont
) -> int:
return (_W - int(draw.textlength(text, font=font))) // 2


def _draw_stat_card(draw: ImageDraw.ImageDraw, x: int, y: int, label: str, value: str):
w, h = 250, 115
draw.rounded_rectangle([x, y, x + w, y + h], radius=12, fill=_LIGHT_GRAY)
draw.text((x + 18, y + 14), label.upper(), font=_font("Roboto-Regular.ttf", 21), fill=_GRAY)
draw.text(
(x + 18, y + 14),
label.upper(),
font=_font("Roboto-Regular.ttf", 21),
fill=_GRAY,
)
draw.text((x + 18, y + 50), value, font=_font("Roboto-Bold.ttf", 36), fill=_DARK)


Expand Down
Loading
Loading