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
10 changes: 7 additions & 3 deletions .github/workflows/black.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: psf/black@20.8b1
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install ruff
- run: ruff check .
- run: ruff format --check .
11 changes: 5 additions & 6 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This workflows will upload a Python Package using Twine when a release is created
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

name: Upload Python Package
Expand All @@ -9,23 +9,22 @@ on:

jobs:
deploy:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install setuptools wheel twine
pip install build twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
python setup.py sdist bdist_wheel
python -m build
twine upload dist/*
19 changes: 7 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: end-of-file-fixer
- id: requirements-txt-fixer
- id: trailing-whitespace

- repo: https://github.com/PyCQA/isort
rev: 5.7.0
hooks:
- id: isort
args: ["--profile", "black"]
- id: check-ast

- repo: https://github.com/psf/black
rev: 20.8b1
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.0
hooks:
- id: black
- id: ruff
args: [--fix]
- id: ruff-format
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM tiangolo/uvicorn-gunicorn:python3.7-alpine3.8
FROM python:3.12-slim
LABEL authors="Nathan Sheffield, Michal Stolarczyk"

COPY . /app
WORKDIR /app
RUN pip install .
CMD ["uvicorn", "refgenieserver.main:app", "--host", "0.0.0.0", "--port", "80"]
5 changes: 0 additions & 5 deletions MANIFEST.in

This file was deleted.

16 changes: 16 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) format.

## [0.8.0] -- 2026-02-25

### Changed
- Updated yacman and refgenconf dependency requirements
- Modernized packaging to use pyproject.toml with hatchling
- Updated FastAPI route definitions to use `pattern` instead of deprecated `regex`
- Updated GitHub Actions to modern versions

### Added
- `app_factory.create_app()` function for programmatic server creation

### Fixed
- Compatibility with yacman v1 (`with rgc as r:` context manager removed)
- Compatibility with refgenconf 0.13.0
- Various modernization and small bugfix improvements

## [0.7.0] -- 2021-04-27
### Added
- `remotes` section in the refgenieserver config, which supersedes `remote_url_base`. It can be used to define multiple remote data providers.
Expand Down
67 changes: 67 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
[project]
name = "refgenieserver"
version = "0.8.0"
description = "A web interface and RESTful API for reference genome assets"
readme = "README.md"
license = {text = "BSD-2-Clause"}
requires-python = ">=3.10"
authors = [
{ name = "Michal Stolarczyk" },
{ name = "Vince Reuter" },
{ name = "Nathan Sheffield" },
]
keywords = ["bioinformatics", "sequencing", "ngs", "genomes", "server"]
classifiers = [
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Bio-Informatics",
]
dependencies = [
"aiofiles",
"fastapi",
"jinja2",
"logmuse>=0.2",
"refgenconf>=0.13.0",
"ubiquerg>=0.6.1",
"uvicorn>=0.7.1",
"yacman>=0.9.5",
]

[project.urls]
Homepage = "https://refgenie.databio.org"

[project.scripts]
refgenieserver = "refgenieserver.__main__:main"

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
include = ["refgenieserver*"]

[tool.setuptools.package-data]
refgenieserver = ["templates/**", "static/*"]

[project.optional-dependencies]
test = [
"pytest",
"httpx",
]

[tool.pytest.ini_options]
addopts = "-rfE"
testpaths = ["tests"]

[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["F403", "F405", "E501"]

[tool.ruff.lint.isort]
known-first-party = ["refgenieserver"]
2 changes: 2 additions & 0 deletions refgenieserver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from .const import *
from .helpers import *
from .main import *
Expand Down
7 changes: 6 additions & 1 deletion refgenieserver/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
__version__ = "0.7.0"
from __future__ import annotations

# Version is defined in pyproject.toml
from importlib.metadata import version

__version__ = version("refgenieserver")
81 changes: 81 additions & 0 deletions refgenieserver/app_factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Factory function to create a configured refgenieserver FastAPI app."""

from __future__ import annotations

import logging
import sys

from fastapi import FastAPI
from refgenconf import RefGenConf

from .const import PKG_NAME, PRIVATE_API, TAGS_METADATA
from .helpers import purge_nonservable

_LOGGER = logging.getLogger(PKG_NAME)


def create_app(config_path: str, archive_base_dir: str | None = None) -> FastAPI:
"""Create a configured FastAPI app for refgenieserver.

This builds a fresh FastAPI app with the real refgenieserver routers,
configured from a given YAML config file. Used both for production
(as an alternative to the CLI entry point) and for integration tests.

Args:
config_path: Path to the refgenie server config YAML.
archive_base_dir: Override for BASE_DIR (default: /genomes).
Used in tests to point at a temp directory.

Returns:
Configured FastAPI app ready to serve.
"""
# Use sys.modules to get the actual module objects. Using
# `import refgenieserver.main as m` can return a different object
# than what's in sys.modules (due to __init__.py's `from .main import *`),
# which means attribute modifications won't be visible to other modules.
import refgenieserver.const # noqa: F401 ensure loaded
import refgenieserver.helpers # noqa: F401 ensure loaded
import refgenieserver.main # noqa: F401 ensure loaded

main_module = sys.modules["refgenieserver.main"]
const_module = sys.modules["refgenieserver.const"]
helpers_module = sys.modules["refgenieserver.helpers"]

# Load config and purge non-servable entries
rgc = RefGenConf.from_yaml_file(config_path)
purge_nonservable(rgc)

# Override the module-level globals that the routers import.
# The routers do `from ..main import _LOGGER, rgc, app, templates`
# which reads from main's module dict at import time.
main_module.rgc = rgc
main_module._LOGGER = _LOGGER

if archive_base_dir is not None:
# Must override BASE_DIR in both const and helpers modules,
# because helpers.py uses `from .const import *` which copies
# BASE_DIR into helpers' own namespace.
const_module.BASE_DIR = archive_base_dir
helpers_module.BASE_DIR = archive_base_dir

from ._version import __version__ as server_v

app = FastAPI(
title=PKG_NAME,
description="a web interface and RESTful API for reference genome assets",
version=server_v,
openapi_tags=TAGS_METADATA,
)

# Set the app on main_module so routers that import `app` from main
# can access it (needed for openapi spec introspection)
main_module.app = app

# Import routers AFTER rgc is set (they read rgc at import time)
from .routers import private, version3

app.include_router(version3.router)
app.include_router(version3.router, prefix="/v3")
app.include_router(private.router, prefix=f"/{PRIVATE_API}")

return app
45 changes: 25 additions & 20 deletions refgenieserver/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
""" Package constants """
"""Package constants"""

from __future__ import annotations

import os
from platform import python_version

Expand All @@ -7,39 +10,41 @@

from ._version import __version__ as server_v

ALL_VERSIONS = {
ALL_VERSIONS: dict[str, str] = {
"server_version": server_v,
"rgc_version": rgc_v,
"python_version": python_version(),
}
PKG_NAME = "refgenieserver"
DEFAULT_PORT = 80
BASE_DIR = "/genomes"
PKG_NAME: str = "refgenieserver"
DEFAULT_PORT: int = 80
BASE_DIR: str = "/genomes"
# if running outside of the Docker container 'BASE_DIR' can be replaced with rgc[CFG_ARCHIVE_KEY]
TEMPLATES_DIRNAME = "templates"
TEMPLATES_PATH = os.path.join(
TEMPLATES_DIRNAME: str = "templates"
TEMPLATES_PATH: str = os.path.join(
os.path.dirname(os.path.abspath(__file__)), TEMPLATES_DIRNAME
)
STATIC_DIRNAME = "static"
STATIC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), STATIC_DIRNAME)
LOG_FORMAT = "%(levelname)s in %(funcName)s: %(message)s"
MSG_404 = "No such {} on server"
DESC_PLACEHOLDER = "No description"
CHECKSUM_PLACEHOLDER = "No digest"
STATIC_DIRNAME: str = "static"
STATIC_PATH: str = os.path.join(
os.path.dirname(os.path.abspath(__file__)), STATIC_DIRNAME
)
LOG_FORMAT: str = "%(levelname)s in %(funcName)s: %(message)s"
MSG_404: str = "No such {} on server"
DESC_PLACEHOLDER: str = "No description"
CHECKSUM_PLACEHOLDER: str = "No digest"
# Here we define the key name changes; format: {"new_key": "old_key"}
# This dict is then used to pre-process the attributes dict before serving to the old versions of the client
CHANGED_KEYS = {CFG_ASSET_PATH_KEY: "path"}
CHANGED_KEYS: dict[str, str] = {CFG_ASSET_PATH_KEY: "path"}

# TODO: to be removed in the future
CFG_LEGACY_ARCHIVE_CHECKSUM_KEY = "legacy_archive_digest"
CFG_LEGACY_ARCHIVE_CHECKSUM_KEY: str = "legacy_archive_digest"

API1_ID = "APIv1"
API2_ID = "APIv2"
API3_ID = "APIv3"
PRIV_API_ID = "PRIVATE_API"
API1_ID: str = "APIv1"
API2_ID: str = "APIv2"
API3_ID: str = "APIv3"
PRIV_API_ID: str = "PRIVATE_API"


TAGS_METADATA = [
TAGS_METADATA: list[dict[str, str]] = [
{
"name": API3_ID,
"description": "These are the most recent API endpoints. "
Expand Down
14 changes: 5 additions & 9 deletions refgenieserver/data_models.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from __future__ import annotations

from typing import Dict, List

from pydantic import BaseModel


class Tag(BaseModel):
"""
Tag data model
"""
"""Tag data model."""

asset_path: str
asset_digest: str
Expand All @@ -19,19 +19,15 @@ class Tag(BaseModel):


class Asset(BaseModel):
"""
Asset data model
"""
"""Asset data model."""

asset_description: str
tags: Dict[str, Tag]
default_tag: str


class Genome(BaseModel):
"""
Genome data model
"""
"""Genome data model."""

genome_description: str
assets: Dict[str, Asset]
Expand Down
Loading
Loading