Skip to content

Commit 961c5c8

Browse files
authored
feat: adds cache (#10)
* feat: caches sheets and refactors
1 parent 7b697d3 commit 961c5c8

15 files changed

Lines changed: 781 additions & 268 deletions

File tree

aind-smartsheet-service-server/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Requires docker to build and run package locally.
1313

1414
- Create a file called env/webapp.env with appropriate env variables.
1515
- Run `docker build -t aind-smartsheet-service-server-local:latest .`
16-
- Run `docker run -p 58350:58350 -p 5000:80 --env-file=env/webapp.env aind-smartsheet-service-server-local:latest`
16+
- Run `docker run -p 5000:80 --env-file=env/webapp.env aind-smartsheet-service-server-local:latest`
1717
- Service will be available at `http://localhost:5000`
1818
- Check docs at `http://localhost:5000/docs`
1919

aind-smartsheet-service-server/pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ dynamic = ["version"]
1818

1919
dependencies = [
2020
'aind-settings-utils>=0.0.3',
21-
'aind-smartsheet-api==0.2.0',
21+
'smartsheet-python-sdk>=3.0',
2222
'pydantic>=2.0',
2323
'pydantic-settings>=2.0',
2424
'fastapi[standard]>=0.114.0',
25+
'fastapi-cache2[redis]>=0.2.2',
2526
]
2627

2728
[project.optional-dependencies]

aind-smartsheet-service-server/src/aind_smartsheet_service_server/configs.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from aind_settings_utils.aws import (
66
ParameterStoreAppBaseSettings,
77
)
8-
from pydantic import Field, SecretStr
8+
from pydantic import Field, RedisDsn, SecretStr
99
from pydantic_settings import SettingsConfigDict
1010

1111

@@ -32,6 +32,12 @@ class Settings(ParameterStoreAppBaseSettings):
3232
protocols_id: int = Field(
3333
..., description="SmartSheet ID of protocols info"
3434
)
35+
redis_url: Optional[RedisDsn] = Field(default=None)
3536
model_config = SettingsConfigDict(
3637
env_prefix="SMARTSHEET_", case_sensitive=False
3738
)
39+
40+
41+
def get_settings() -> Settings:
42+
"""Return a settings object"""
43+
return Settings()

aind-smartsheet-service-server/src/aind_smartsheet_service_server/handler.py

Lines changed: 93 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,121 @@
11
"""Module to handle smartsheet api responses"""
22

3-
from typing import Any, List, Optional, Type
3+
from typing import Any, Dict, List, Optional, Type, TypeVar
44

5-
from aind_smartsheet_api.client import SmartsheetClient
6-
from pydantic import BaseModel
5+
from pydantic import BaseModel, ValidationError
76

87
from aind_smartsheet_service_server.models import (
98
FundingModel,
109
PerfusionsModel,
1110
ProtocolsModel,
11+
SheetFields,
12+
SheetRow,
1213
)
1314

15+
T = TypeVar("T", bound=BaseModel)
1416

15-
class SessionHandler:
16-
"""Handle session object to get data"""
1717

18-
def __init__(self, session: SmartsheetClient):
18+
class SheetHandler:
19+
"""Handle raw sheet object"""
20+
21+
def __init__(self, raw_sheet: str, validate: bool = True):
1922
"""Class constructor"""
20-
self.session = session
23+
self.raw_sheet = raw_sheet
24+
self.validate = validate
25+
26+
def _get_sheet_fields(self) -> SheetFields:
27+
"""
28+
Returns
29+
-------
30+
SheetFields
31+
32+
"""
33+
return SheetFields.model_validate_json(self.raw_sheet)
2134

22-
def get_parsed_sheet(
23-
self, sheet_id: int, model: Type[BaseModel]
24-
) -> List[Any]:
35+
@staticmethod
36+
def _column_id_map(sheet_fields: SheetFields) -> Dict[int, str]:
2537
"""
26-
Converts the rows in a Smart Sheet into a Pydantic model
38+
SmartSheet uses integer ids for the columns. We need a way to
39+
map the column titles to the ids so we can retrieve information using
40+
just the titles.
2741
Parameters
2842
----------
29-
sheet_id : int
30-
ID number of smart sheet
31-
model : Type[BaseModel]
32-
Pydantic model to parse the sheet to
43+
sheet_fields : SheetFields
3344
3445
Returns
3546
-------
36-
List[Any]
37-
A list of Pydantic models
47+
Dict[int, str]
3848
3949
"""
40-
sheet = self.session.get_parsed_sheet_model(
41-
sheet_id=sheet_id, model=model, validate=False
42-
)
43-
return sheet
50+
return {c.id: c.title for c in sheet_fields.columns}
4451

4552
@staticmethod
53+
def _map_row_to_dict(
54+
row: SheetRow, column_id_map: Dict[int, str]
55+
) -> Dict[str, Any]:
56+
"""
57+
Maps a row into a dictionary that maps the column names to their values
58+
Parameters
59+
----------
60+
row : SheetRow
61+
A SheetRow that will be parsed. This a list of cells with a columnId
62+
and a cell value.
63+
column_id_map: Dict[int, str]
64+
Map of column ids into the column names
65+
66+
Returns
67+
-------
68+
Dict[str, Any]
69+
The list of row cells is converted to a dictionary.
70+
71+
"""
72+
output_dict = {}
73+
for cell in row.cells:
74+
column_id = cell.columnId
75+
column_name = column_id_map[column_id]
76+
output_dict[column_name] = cell.value
77+
return output_dict
78+
79+
def _get_parsed_sheet_model(self, model: Type[T]) -> List[T]:
80+
"""
81+
Parse raw sheet json into basic dictionary of {"name": "value"}
82+
Parameters
83+
----------
84+
model : T
85+
BaseModel type
86+
87+
Returns
88+
-------
89+
List[T]
90+
91+
"""
92+
93+
sheet_fields = self._get_sheet_fields()
94+
column_id_map = self._column_id_map(sheet_fields=sheet_fields)
95+
parsed_rows = list()
96+
for row in sheet_fields.rows:
97+
row_dict = self._map_row_to_dict(row, column_id_map=column_id_map)
98+
try:
99+
row_as_model = model.model_validate(row_dict)
100+
parsed_rows.append(row_as_model)
101+
102+
except ValidationError as e:
103+
if self.validate:
104+
raise e
105+
else:
106+
row_as_model = model.model_construct(**row_dict)
107+
parsed_rows.append(row_as_model)
108+
return parsed_rows
109+
46110
def get_project_funding_info(
47-
sheet_model: List[FundingModel],
111+
self,
48112
project_name: Optional[str] = None,
49113
subproject: Optional[str] = None,
50114
) -> List[FundingModel]:
51115
"""
52116
Filters funding sheet by specific project name
53117
Parameters
54118
----------
55-
sheet_model : List[FundingModel]
56119
project_name : str | None
57120
subproject : str | None
58121
@@ -63,6 +126,7 @@ def get_project_funding_info(
63126
A list of all of that match the project name.
64127
65128
"""
129+
sheet_model = self._get_parsed_sheet_model(model=FundingModel)
66130
filtered_rows = [
67131
r
68132
for r in sheet_model
@@ -74,20 +138,17 @@ def get_project_funding_info(
74138
]
75139
return filtered_rows
76140

77-
@staticmethod
78-
def get_project_names(sheet_model: List[FundingModel]) -> List[str]:
141+
def get_project_names(self) -> List[str]:
79142
"""
80143
Returns a sorted list of unique project names found in the Funding
81144
SmartSheet.
82-
Parameters
83-
----------
84-
sheet_model : List[FundingModel]
85145
86146
Returns
87147
-------
88148
List[str]
89149
90150
"""
151+
sheet_model = self._get_parsed_sheet_model(model=FundingModel)
91152
project_names = set()
92153
for funding_model in sheet_model:
93154
project_name = funding_model.project_name
@@ -99,15 +160,13 @@ def get_project_names(sheet_model: List[FundingModel]) -> List[str]:
99160
sorted_names = sorted(list(set(project_names)))
100161
return sorted_names
101162

102-
@staticmethod
103163
def get_protocols_info(
104-
sheet_model: List[ProtocolsModel], protocol_name: Optional[str] = None
164+
self, protocol_name: Optional[str] = None
105165
) -> List[ProtocolsModel]:
106166
"""
107167
Filter protocols Smartsheet by protocols name
108168
Parameters
109169
----------
110-
sheet_model : List[ProtocolsModel]
111170
protocol_name : str | None
112171
113172
Returns
@@ -117,22 +176,21 @@ def get_protocols_info(
117176
A list of all of that match the project name.
118177
119178
"""
179+
sheet_model = self._get_parsed_sheet_model(model=ProtocolsModel)
120180
filtered_rows = [
121181
r
122182
for r in sheet_model
123183
if r.protocol_name == protocol_name or protocol_name is None
124184
]
125185
return filtered_rows
126186

127-
@staticmethod
128187
def get_perfusions_info(
129-
sheet_model: List[PerfusionsModel], subject_id: Optional[str] = None
188+
self, subject_id: Optional[str] = None
130189
) -> List[PerfusionsModel]:
131190
"""
132191
133192
Parameters
134193
----------
135-
sheet_model : List[PerfusionsModel]
136194
subject_id : str | None
137195
138196
Returns
@@ -141,6 +199,7 @@ def get_perfusions_info(
141199
In the event that there are multiple project names, will return
142200
A list of all of that match the project name.
143201
"""
202+
sheet_model = self._get_parsed_sheet_model(model=PerfusionsModel)
144203
filtered_rows = [
145204
r
146205
for r in sheet_model

aind-smartsheet-service-server/src/aind_smartsheet_service_server/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22

33
import logging
44
import os
5+
from collections.abc import AsyncIterator
6+
from contextlib import asynccontextmanager
57

68
from fastapi import FastAPI
79
from fastapi.middleware.cors import CORSMiddleware
810
from fastapi.routing import APIRoute
11+
from fastapi_cache import FastAPICache
12+
from fastapi_cache.backends.inmemory import InMemoryBackend
13+
from fastapi_cache.backends.redis import RedisBackend
14+
from redis.asyncio import from_url # noqa
915

1016
from aind_smartsheet_service_server import __version__ as service_version
17+
from aind_smartsheet_service_server.configs import get_settings
1118
from aind_smartsheet_service_server.route import router
1219

1320
# The log level can be set by adding an environment variable before launch.
@@ -21,12 +28,26 @@
2128
2229
"""
2330

31+
32+
@asynccontextmanager
33+
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
34+
"""Init cache and add to lifespan of app"""
35+
settings = get_settings()
36+
if settings.redis_url is not None:
37+
redis = from_url(settings.redis_url)
38+
FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")
39+
else:
40+
FastAPICache.init(InMemoryBackend(), prefix="fastapi-cache")
41+
yield
42+
43+
2444
# noinspection PyTypeChecker
2545
app = FastAPI(
2646
title="aind-smartsheet-service",
2747
description=description,
2848
summary="Serves data from SmartSheet.",
2949
version=service_version,
50+
lifespan=lifespan,
3051
)
3152

3253
# noinspection PyTypeChecker

0 commit comments

Comments
 (0)