Skip to content

Commit

Permalink
Add fastapi-cache2 for pydantic v2 compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
andersy005 committed Sep 18, 2024
1 parent b5be9cc commit 87ca88b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 4 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ s3fs
uvicorn
xarray>=2022.06
zarr
fastapi-cache2 @ git+https://github.com/andersy005/fastapi-cache@pydantic-v2-compat # for pydantic v2 compatilibity
78 changes: 74 additions & 4 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
from __future__ import annotations

import os
import traceback
from contextlib import asynccontextmanager

import pydantic
from fastapi import FastAPI, Query, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi_cache import FastAPICache
from fastapi_cache.backends.inmemory import InMemoryBackend
from fastapi_cache.decorator import cache

from .log import get_logger

origins = ['*']

app = FastAPI()
logger = get_logger()


@asynccontextmanager
async def lifespan_event(app: FastAPI):
"""
Context manager that yields the application's startup and shutdown events.
"""
logger.info('⏱️ Application startup...')

worker_num = int(os.environ.get('APP_WORKER_ID', 9999))

logger.info(f'πŸ‘· Worker num: {worker_num}')

# set up cache
logger.info('πŸ”₯ Setting up cache...')
expiration = int(60 * 60 * 1) # 24 hours
cache_status_header = 'x-html-reprs-cache'
FastAPICache.init(
InMemoryBackend(),
expire=expiration,
cache_status_header=cache_status_header,
)
logger.info(
f'πŸ”₯ Cache set up with expiration={expiration:,} seconds | {cache_status_header} cache status header.'
)

yield

logger.info('Application shutdown...')
logger.info('Clearing cache...')
FastAPICache.reset()
logger.info('πŸ‘‹ Goodbye!')


app = FastAPI(lifespan=lifespan_event)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
Expand All @@ -23,34 +67,60 @@ def index():


@app.get('/xarray/')
@cache(namespace='html-reprs')
def xarray(
url: pydantic.AnyUrl = Query(
...,
description='URL to a zarr store',
example='https://ncsa.osn.xsede.org/Pangeo/pangeo-forge/HadISST-feedstock/hadisst.zarr',
),
):
logger.info(f'πŸš€ Starting to process request for URL: {url}')

import time

import xarray as xr
import zarr

error_message = f'An error occurred while fetching the data from URL: {url}'
start_time = time.time()

error_message = f'❌ An error occurred while fetching the data from URL: {url}'

try:
logger.info(f'πŸ“‚ Attempting to open dataset from URL: {url}')
with xr.open_dataset(url.unicode_string(), engine='zarr', chunks={}) as ds:
logger.info('βœ… Successfully opened dataset. Generating HTML representation.')
html = ds._repr_html_().strip()

del ds
logger.info('🧹 Cleaned up dataset object')

end_time = time.time()
processing_time = end_time - start_time
logger.info(f'🏁 Request processed successfully in {processing_time:.2f} seconds')

return {'html': html, 'dataset': url}

except (zarr.errors.GroupNotFoundError, FileNotFoundError):
message = traceback.format_exc()
logger.error(f'πŸ” {error_message}: Dataset not found\n{message}')
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={'detail': f'{error_message}. Dataset not found.'},
content={'detail': f'{error_message} Dataset not found. πŸ˜•'},
)

except PermissionError:
message = traceback.format_exc()
logger.error(f'πŸ”’ {error_message}: Permission denied\n{message}')
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={'detail': f'{error_message}. Permission denied.'},
content={'detail': f'{error_message} Permission denied. 🚫'},
)

except Exception as e:
message = traceback.format_exc()
logger.error(f'πŸ’₯ {error_message}: Unexpected error\n{message}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'detail': f'{error_message} {str(e)} 😱'},
)
21 changes: 21 additions & 0 deletions src/log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logging
import os
import sys


def get_logger() -> logging.Logger:
logger = logging.getLogger('html-reprs')
worker_id = os.environ.get('APP_WORKER_ID', '')

if not logger.handlers:
handler = logging.StreamHandler(stream=sys.stdout)
if worker_id != '':
handler.setFormatter(
logging.Formatter(f'[%(name)s] [worker {worker_id}] [%(levelname)s] %(message)s')
)
else:
handler.setFormatter(logging.Formatter('[%(name)s] [%(levelname)s] %(message)s'))
logger.addHandler(handler)

logger.setLevel(logging.DEBUG)
return logger

0 comments on commit 87ca88b

Please sign in to comment.