Skip to content

Commit

Permalink
Implement caching system
Browse files Browse the repository at this point in the history
  • Loading branch information
albireox committed Dec 9, 2024
1 parent 092f162 commit b109d94
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 34 deletions.
196 changes: 180 additions & 16 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ deepmerge = "^1.1.1"
fuzzy-types = "^0.1.3"
sdss-solara = {git = "https://github.com/sdss/sdss_solara.git", rev = "main", optional = true}
markdown = "^3.7"
fastapi-cache2 = { version = "^0.2.2", extras = ["redis", "memcache"] }

[tool.poetry.dev-dependencies]
ipython = ">=7.11.0"
Expand Down
72 changes: 72 additions & 0 deletions python/valis/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Filename: main.py
# Project: app
# Author: José Sánchez-Gallego
# Created: Monday, 9th December 2024
# License: BSD 3-clause "New" or "Revised" License
# Copyright (c) 2020 José Sánchez-Gallego
# Last Modified: Monday, 9th December 2024
# Modified By: José Sánchez-Gallego

from __future__ import annotations

import logging
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING

from aiomcache import Client as MemcacheClient
from fastapi_cache import FastAPICache
from fastapi_cache.backends.memcached import MemcachedBackend
from fastapi_cache.backends.redis import RedisBackend
from redis.asyncio.client import Redis

from valis.settings import settings


if TYPE_CHECKING:
from typing import AsyncIterator

from fastapi import FastAPI, Request, Response


logger = logging.getLogger("uvicorn.error")

@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
backend = settings.cache_backend
if backend == 'memcache':
logger.info('Using Memcached backend for caching')
memcache_client = MemcacheClient('localhost', 11211)
FastAPICache.init(MemcachedBackend(memcache_client),
prefix="fastapi-cache",
key_builder=valis_cache_key_builder)
elif backend == 'redis':
logger.info('Using Redis backend for caching')
redis = Redis.from_url("redis://localhost")
FastAPICache.init(RedisBackend(redis),
prefix="fastapi-cache",
key_builder=valis_cache_key_builder)
else:
raise ValueError(f'Invalid cache backend {backend}')

yield


def valis_cache_key_builder(
func,
namespace: str = "",
request: Request | None = None,
response: Response | None = None,
*args,
**kwargs,
):
return ":".join(
[
namespace,
request.method.lower() if request else "",
request.url.path if request else "",
repr(sorted(request.query_params.items())) if request else "",
]
)
29 changes: 15 additions & 14 deletions python/valis/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,29 @@
import os
import pathlib
from typing import Dict
from functools import lru_cache

from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.staticfiles import StaticFiles

import valis
from valis.routes import access, auth, envs, files, info, maskbits, mocs, target, query
from valis.cache import lifespan
from valis.routes import (
access,
auth,
envs,
files,
info,
maskbits,
mocs,
query,
target
)
from valis.routes.auth import set_auth
from valis.routes.base import release
from valis.settings import Settings, read_valis_config
from valis.settings import settings


# set up the solara server
try:
Expand Down Expand Up @@ -91,19 +102,9 @@
},
]


@lru_cache
def get_settings():
""" Get the valis settings """
cfg = read_valis_config()
return Settings(**cfg)


settings = get_settings()

# create the application
app = FastAPI(title='Valis', description='The SDSS API', version=valis.__version__,
openapi_tags=tags_metadata, dependencies=[])
openapi_tags=tags_metadata, lifespan=lifespan, dependencies=[])
# submount app to allow for production /valis location
app.mount("/valis", app)

Expand Down
8 changes: 8 additions & 0 deletions python/valis/routes/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from enum import Enum
from typing import List, Union, Dict, Annotated, Optional
from fastapi import APIRouter, Depends, Query, HTTPException
from fastapi_cache.decorator import cache
from fastapi_restful.cbv import cbv
from pydantic import BaseModel, Field, BeforeValidator

Expand Down Expand Up @@ -116,6 +117,7 @@ async def main_search(self, body: SearchModel):

@router.get('/cone', summary='Perform a cone search for SDSS targets with sdss_ids',
response_model=List[SDSSModel], dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def cone_search(self,
ra: Annotated[Union[float, str], Query(description='Right Ascension in degrees or hmsdms', example=315.78)],
dec: Annotated[Union[float, str], Query(description='Declination in degrees or hmsdms', example=-3.2)],
Expand All @@ -132,6 +134,7 @@ async def cone_search(self,

@router.get('/sdssid', summary='Perform a search for an SDSS target based on the sdss_id',
response_model=Union[SDSSidStackedBase, dict], dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def sdss_id_search(self, sdss_id: Annotated[int, Query(description='Value of sdss_id', example=47510284)]):
""" Perform an sdss_id search.
Expand Down Expand Up @@ -163,27 +166,31 @@ async def catalog_id_search(self, catalog_id: Annotated[int, Query(description='

@router.get('/list/cartons', summary='Return a list of all cartons',
response_model=list, dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def cartons(self):
""" Return a list of all carton or programs """

return carton_program_list("carton")

@router.get('/list/programs', summary='Return a list of all programs',
response_model=list, dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def programs(self):
""" Return a list of all carton or programs """

return carton_program_list("program")

@router.get('/list/program-map', summary='Return a mapping of cartons in all programs',
response_model=Dict[str, List[str]], dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def program_map(self):
""" Return a mapping of cartons in all programs """

return carton_program_map()

@router.get('/list/parents', summary='Return a list of available parent catalog tables',
response_model=List[str])
@cache(expire=3600, namespace='valis-cache')
async def parent_catalogs(self):
"""Return a list of available parent catalog tables."""

Expand All @@ -197,6 +204,7 @@ async def parent_catalogs(self):

@router.get('/carton-program', summary='Search for all SDSS targets within a carton or program',
response_model=List[SDSSModel], dependencies=[Depends(get_pw_db)])
@cache(expire=3600, namespace='valis-cache')
async def carton_program(self,
name: Annotated[str, Query(description='Carton or program name', example='manual_mwm_tess_ob')],
name_type: Annotated[str,
Expand Down
7 changes: 6 additions & 1 deletion python/valis/routes/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
import astropy.units as u
from astropy.coordinates import SkyCoord
from astroquery.simbad import Simbad

from fastapi_cache.decorator import cache
from valis.routes.base import Base
from valis.db.queries import (get_target_meta, get_a_spectrum, get_catalog_sources,
get_parent_catalog_data, get_target_cartons,
get_target_pipeline, get_target_by_altid, append_pipes)
from valis.db.db import get_pw_db
from valis.db.models import CatalogResponse, CartonModel, ParentCatalogModel, PipesModel, SDSSModel


router = APIRouter()

Simbad.add_votable_fields('distance_result')
Expand Down Expand Up @@ -182,6 +183,7 @@ async def get_target_altid(self,
@router.get('/spectra/{sdss_id}', summary='Retrieve a spectrum for a target sdss_id',
dependencies=[Depends(get_pw_db)],
response_model=List[SpectrumModel])
@cache(expire=3600, namespace='valis-cache')
async def get_spectrum(self, sdss_id: Annotated[int, Path(title="The sdss_id of the target to get", example=23326)],
product: Annotated[str, Query(description='The file species or data product name', example='specLite')],
ext: Annotated[str, Query(description='For multi-extension spectra, e.g. mwmStar, the name of the spectral extension', example='BOSS/APO')] = None,
Expand All @@ -192,6 +194,7 @@ async def get_spectrum(self, sdss_id: Annotated[int, Path(title="The sdss_id of
dependencies=[Depends(get_pw_db)],
response_model=List[CatalogResponse],
response_model_exclude_unset=True, response_model_exclude_none=True)
@cache(expire=3600, namespace='valis-cache')
async def get_catalogs(self, sdss_id: int = Path(title="The sdss_id of the target to get", example=23326)):
""" Return catalog information for a given sdss_id """

Expand Down Expand Up @@ -240,6 +243,7 @@ async def get_parents(self,
dependencies=[Depends(get_pw_db)],
response_model=List[CartonModel],
response_model_exclude_unset=True, response_model_exclude_none=True)
@cache(expire=3600, namespace='valis-cache')
async def get_cartons(self, sdss_id: int = Path(title="The sdss_id of the target to get", example=23326)):
""" Return carton information for a given sdss_id """
return get_target_cartons(sdss_id).dicts().iterator()
Expand All @@ -248,6 +252,7 @@ async def get_cartons(self, sdss_id: int = Path(title="The sdss_id of the target
dependencies=[Depends(get_pw_db)],
response_model=PipesModel,
response_model_exclude_unset=True)
@cache(expire=3600, namespace='valis-cache')
async def get_pipeline(self, sdss_id: int = Path(title="The sdss_id of the target to get", example=23326),
pipe: Annotated[str,
Query(enum=['all', 'boss', 'apogee', 'astra'],
Expand Down
25 changes: 22 additions & 3 deletions python/valis/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
#

from enum import Enum
from typing import List, Union, Optional
from valis import config
from functools import lru_cache
from typing import List, Literal, Optional, Union

from pydantic import AnyHttpUrl, Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import field_validator, Field, AnyHttpUrl

from valis import config


def read_valis_config() -> dict:
Expand All @@ -30,6 +33,11 @@ class EnvEnum(str, Enum):
prod = 'production'


class CacheBackendEnum(str, Enum):
memcached = 'memcached'
redis = 'redis'


class Settings(BaseSettings):
valis_env: EnvEnum = EnvEnum.dev
allow_origin: Union[str, List[AnyHttpUrl]] = Field([])
Expand All @@ -40,6 +48,7 @@ class Settings(BaseSettings):
db_host: Optional[str] = 'localhost'
db_pass: Optional[str] = None
db_reset: bool = True
cache_backend: CacheBackendEnum = CacheBackendEnum.redis
model_config = SettingsConfigDict(env_prefix="valis_")

@field_validator('allow_origin')
Expand All @@ -53,3 +62,13 @@ def must_be_list(cls, v):
@classmethod
def strip_slash(cls, v):
return [i.rstrip('/') for i in v]


@lru_cache
def get_settings():
""" Get the valis settings """
cfg = read_valis_config()
return Settings(**cfg)


settings = get_settings()

0 comments on commit b109d94

Please sign in to comment.