├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── detacache ├── __init__.py ├── coder.py ├── core │ ├── __init__.py │ ├── _coder.py │ ├── _decorators.py │ ├── _detaBase.py │ ├── _helpers.py │ └── _key.py ├── decorators.py └── key.py ├── example.py └── setup.py /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/heads 6 | branches: 7 | - main 8 | # Sequence of patterns matched against refs/tags 9 | tags: 10 | - v* 11 | 12 | 13 | jobs: 14 | build-n-publish: 15 | name: Build and publish Python 🐍 distributions 📦 to PyPI 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@master 19 | - name: Set up Python 3.10 20 | uses: actions/setup-python@v3 21 | with: 22 | python-version: "3.10" 23 | - name: Install pypa/build 24 | run: >- 25 | python -m 26 | pip install 27 | build 28 | --user 29 | - name: Build a binary wheel and a source tarball 30 | run: >- 31 | python -m 32 | build 33 | --sdist 34 | --wheel 35 | --outdir dist/ 36 | - name: Publish a Python distribution to PyPI 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | password: ${{ secrets.PYPI_API_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # vscode 3 | 4 | .vscode 5 | .pypirc 6 | 7 | # python 8 | starletteTest.py 9 | fastTest.py 10 | test.py 11 | test1.py 12 | fastapiExample.py 13 | example.py 14 | 15 | __pycache__/ 16 | .venv/ 17 | dist/ 18 | build/ 19 | DetaCache.egg-info/ 20 | templates/ 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vidya Sagar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include detacache *.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DetaCache](https://github.com/vidyasagar1432/detacache) 2 | 3 | #### Async and Sync Function Decorator to cache function call's to Deta base. 4 | 5 | ## Installing 6 | 7 | ```bash 8 | pip install detacache 9 | ``` 10 | 11 | ## Async and Sync Decorator to cache function 12 | ```python 13 | import asyncio 14 | import aiohttp 15 | import requests 16 | 17 | from detacache import DetaCache 18 | 19 | app = detaCache('projectKey') 20 | 21 | 22 | @app.cache(expire=30) 23 | async def asyncgetjSON(url:str): 24 | async with aiohttp.ClientSession() as session: 25 | async with session.get(url) as response: 26 | return await response.json() 27 | 28 | @app.cache(expire=30) 29 | def syncgetjSON(url:str): 30 | return requests.get(url).json() 31 | 32 | async def main(): 33 | asyncdata = await asyncgetjSON('https://httpbin.org/json') 34 | print(asyncdata) 35 | syncdata = syncgetjSON('https://httpbin.org/json') 36 | print(syncdata) 37 | 38 | loop = asyncio.get_event_loop() 39 | loop.run_until_complete(main()) 40 | ``` 41 | 42 | ## FastAPI Decorator to cache function 43 | 44 | #### you can use `cache` method as decorator between router decorator and view function and must pass `request` as param of view function. 45 | 46 | ```python 47 | from fastapi import FastAPI, Request 48 | from fastapi.templating import Jinja2Templates 49 | from fastapi.responses import HTMLResponse, PlainTextResponse 50 | from detacache import FastAPICache 51 | 52 | app = FastAPI() 53 | 54 | templates = Jinja2Templates(directory='templates') 55 | 56 | deta = FastAPICache(projectKey='projectKey') 57 | 58 | 59 | @app.get('/t-html') 60 | @deta.cache(expire=10) 61 | def templateResponse(request:Request): 62 | return templates.TemplateResponse('home.html',context={'request':request}) 63 | 64 | @app.get('/html') 65 | @deta.cache(expire=10) 66 | def htmlResponse(request: Request): 67 | return HTMLResponse(''' 68 | 69 | 70 | 71 | 72 | My Pimpin Website 73 | 74 | 75 | 76 | 77 | 78 |
79 |
80 |
81 |

{{ data }}

82 |
83 |
84 |
85 |

This is just an example with some web content. This is the Hero Unit.

86 |
87 |
88 |

Featured Content 1

89 |

lorem ipsum dolor amet lorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum.

90 |
91 |
92 |

Featured Content 2

93 |

lorem ipsum dolor amet lorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor.

94 |
95 | 98 |
99 | 100 | 101 | ''') 102 | 103 | 104 | @app.get('/dict') 105 | @deta.cache(expire=10) 106 | def dictResponse(request: Request): 107 | return { 108 | "slideshow": { 109 | "author": "Yours Truly", 110 | "date": "date of publication", 111 | "slides": [ 112 | { 113 | "title": "Wake up to WonderWidgets!", 114 | "type": "all" 115 | }, 116 | { 117 | "items": [ 118 | "Why WonderWidgets are great", 119 | "Who buys WonderWidgets" 120 | ], 121 | "title": "Overview", 122 | "type": "all" 123 | } 124 | ], 125 | "title": "Sample Slide Show" 126 | } 127 | } 128 | 129 | 130 | @app.get('/text') 131 | @deta.cache(expire=10) 132 | def textResponse(request: Request): 133 | return PlainTextResponse('detacache') 134 | 135 | 136 | @app.get('/str') 137 | @deta.cache(expire=20) 138 | async def strResponse(request: Request): 139 | return 'fastapi detacache' 140 | 141 | 142 | @app.get('/tuple') 143 | @deta.cache(expire=10) 144 | def tupleResponse(request: Request): 145 | return ('fastapi', 'detacache') 146 | 147 | 148 | @app.get('/list') 149 | @deta.cache(expire=10) 150 | def tupleResponse(request: Request): 151 | return ['fastapi', 'detacache'] 152 | 153 | @app.get('/set') 154 | @deta.cache(expire=10) 155 | def setResponse(request: Request): 156 | return {'fastapi', 'detacache'} 157 | 158 | 159 | @app.get('/int') 160 | @deta.cache(expire=10) 161 | def intResponse(request: Request): 162 | return 10 163 | 164 | 165 | @app.get('/float') 166 | @deta.cache(expire=10) 167 | def floatResponse(request: Request): 168 | return 1.5 169 | 170 | 171 | @app.get('/bool') 172 | @deta.cache(expire=10) 173 | def boolResponse(request: Request): 174 | return True 175 | 176 | ``` 177 | 178 | ## starlette Decorator to cache function 179 | 180 | #### you can use `cache` method as decorator and must pass `request` as param of view function. 181 | 182 | ```python 183 | from starlette.applications import Starlette 184 | from starlette.responses import HTMLResponse, PlainTextResponse, JSONResponse 185 | from starlette.routing import Route 186 | from starlette.requests import Request 187 | 188 | from detacache import StarletteCache 189 | 190 | 191 | deta = StarletteCache(projectKey='projectKey') 192 | 193 | 194 | 195 | @deta.cache(expire=30) 196 | def dictResponse(request: Request): 197 | return JSONResponse({ 198 | "slideshow": { 199 | "author": "Yours Truly", 200 | "date": "date of publication", 201 | "slides": [ 202 | { 203 | "title": "Wake up to WonderWidgets!", 204 | "type": "all" 205 | }, 206 | { 207 | "items": [ 208 | "Why WonderWidgets are great", 209 | "Who buys WonderWidgets" 210 | ], 211 | "title": "Overview", 212 | "type": "all" 213 | } 214 | ], 215 | "title": "Sample Slide Show" 216 | } 217 | }) 218 | 219 | @deta.cache(expire=20) 220 | async def strResponse(request: Request): 221 | return JSONResponse('fastapi detacache') 222 | 223 | @deta.cache(expire=10) 224 | def tupleResponse(request: Request): 225 | return JSONResponse(('fastapi', 'detacache')) 226 | 227 | @deta.cache(expire=10) 228 | def listResponse(req): 229 | print(req.url) 230 | return JSONResponse(['fastapi', 'detacache']) 231 | 232 | @deta.cache(expire=10) 233 | def setResponse(request: Request): 234 | return JSONResponse({'fastapi', 'detacache'}) 235 | 236 | @deta.cache(expire=10) 237 | def intResponse(request: Request): 238 | return JSONResponse(10) 239 | 240 | @deta.cache(expire=10) 241 | def floatResponse(request: Request): 242 | return JSONResponse(1.5) 243 | 244 | @deta.cache(expire=10) 245 | def boolResponse(request: Request): 246 | return JSONResponse(True) 247 | 248 | @deta.cache(expire=10) 249 | def jsonResponse(request: Request): 250 | return JSONResponse({ 251 | "slideshow": { 252 | "author": "Yours Truly", 253 | "date": "date of publication", 254 | "slides": [ 255 | { 256 | "title": "Wake up to WonderWidgets!", 257 | "type": "all" 258 | }, 259 | { 260 | "items": [ 261 | "Why WonderWidgets are great", 262 | "Who buys WonderWidgets" 263 | ], 264 | "title": "Overview", 265 | "type": "all" 266 | } 267 | ], 268 | "title": "Sample Slide Show" 269 | } 270 | } 271 | ) 272 | 273 | @deta.cache(expire=30) 274 | def htmlResponse(request: Request): 275 | return HTMLResponse(''' 276 | 277 | 278 | 279 | 280 | My Pimpin Website 281 | 282 | 283 | 284 | 285 | 286 |
287 |
288 |
289 |

{{ data }}

290 |
291 |
292 |
293 |

This is just an example with some web content. This is the Hero Unit.

294 |
295 |
296 |

Featured Content 1

297 |

lorem ipsum dolor amet lorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum.

298 |
299 |
300 |

Featured Content 2

301 |

lorem ipsum dolor amet lorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor ametlorem ipsum dolor.

302 |
303 | 306 |
307 | 308 | 309 | ''') 310 | 311 | @deta.cache(expire=20) 312 | def textResponse(request: Request): 313 | return PlainTextResponse('detacache') 314 | 315 | 316 | routes = [ 317 | Route("/text", endpoint=textResponse), 318 | Route("/html", endpoint=htmlResponse), 319 | Route("/json", endpoint=jsonResponse), 320 | Route("/bool", endpoint=boolResponse), 321 | Route("/float", endpoint=floatResponse), 322 | Route("/int", endpoint=intResponse), 323 | Route("/set", endpoint=setResponse), 324 | Route("/list", endpoint=listResponse), 325 | Route("/tuple", endpoint=tupleResponse), 326 | Route("/str", endpoint=strResponse), 327 | Route("/dict", endpoint=dictResponse), 328 | ] 329 | 330 | app = Starlette(routes=routes) 331 | ``` 332 | ## License 333 | 334 | MIT License 335 | 336 | Copyright (c) 2021 [Vidya Sagar](https://github.com/vidyasagar1432) -------------------------------------------------------------------------------- /detacache/__init__.py: -------------------------------------------------------------------------------- 1 | from detacache.core._decorators import DetaCache,BaseDecorator,FastAPICache,StarletteCache 2 | 3 | 4 | __version__ = 'v0.1.2' 5 | 6 | __all__ = [ 7 | 'BaseDecorator', 8 | 'DetaCache', 9 | 'FastAPICache', 10 | 'StarletteCache' 11 | ] 12 | 13 | -------------------------------------------------------------------------------- /detacache/coder.py: -------------------------------------------------------------------------------- 1 | from .core._coder import Coder,DetaCoder,FastAPICoder,StarletteCoder 2 | 3 | __all__ = [ 4 | 'Coder', 5 | 'DetaCoder', 6 | 'FastAPICoder', 7 | 'StarletteCoder' 8 | ] 9 | -------------------------------------------------------------------------------- /detacache/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vidyasagar1432/detacache/cced6469f56c5983a0f27094896d715fccd44cdc/detacache/core/__init__.py -------------------------------------------------------------------------------- /detacache/core/_coder.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Any 3 | 4 | class Coder: 5 | @classmethod 6 | def encode(cls, value: Any): 7 | raise NotImplementedError 8 | 9 | @classmethod 10 | def decode(cls, value: Any): 11 | raise NotImplementedError 12 | 13 | 14 | JSON_CONVERTERS = { 15 | "dict": lambda x: dict(x), 16 | "list": lambda x: list(x), 17 | "tuple": lambda x: tuple(x), 18 | "float": lambda x: float(x), 19 | "set": lambda x: set(x), 20 | "str": lambda x: str(x), 21 | "int": lambda x: int(x), 22 | "bool": lambda x: bool(x), 23 | } 24 | 25 | def objDecode(value,con): 26 | _type = value.get("type") 27 | if not _type: 28 | return value 29 | 30 | if _type in con: 31 | return con[_type](value["value"]) 32 | else: 33 | raise TypeError("Unknown {}".format(_type)) 34 | 35 | 36 | class DetaCoder(Coder): 37 | '''(dict, list, tuple, set, float, str, int, bool)''' 38 | 39 | @classmethod 40 | def encode(cls, value: Any): 41 | if isinstance(value, (dict, list, float, str, int, bool)): 42 | return value 43 | elif isinstance(value, (tuple, set)): 44 | return list(value) 45 | else: 46 | raise Exception( 47 | "function response must be (dict, list, tuple, set, float, str, int, bool)") 48 | 49 | @classmethod 50 | def decode(cls, value: Any): 51 | return objDecode(value,JSON_CONVERTERS) 52 | 53 | try: 54 | import fastapi 55 | from fastapi.encoders import jsonable_encoder 56 | from fastapi.responses import HTMLResponse, PlainTextResponse, JSONResponse, Response 57 | except ImportError: 58 | fastapi = None 59 | 60 | 61 | 62 | def fastAPIdecode(): 63 | _FASTAPIHTML = lambda x: HTMLResponse(str(x['body']), headers=dict(x['raw_headers']), status_code=x['status_code']) 64 | FASTAPI_CONVERTERS = { 65 | "_TemplateResponse":_FASTAPIHTML, 66 | "HTMLResponse": _FASTAPIHTML, 67 | "JSONResponse":lambda x: JSONResponse(str(x['body']), headers=dict(x['raw_headers']), status_code=x['status_code']), 68 | "PlainTextResponse": lambda x: PlainTextResponse(str(x['body']), headers=dict(x['raw_headers']), status_code=x['status_code']), 69 | "Response": lambda x: Response(str(x['body']), headers=dict(x['raw_headers']), status_code=x['status_code']), 70 | } 71 | FASTAPI_CONVERTERS.update(JSON_CONVERTERS) 72 | return FASTAPI_CONVERTERS 73 | 74 | 75 | 76 | 77 | class FastAPICoder(Coder): 78 | 79 | @classmethod 80 | def encode(cls, value: Any): 81 | assert fastapi is not None, "fastapi must be installed to use FastAPICoder.encode" 82 | return jsonable_encoder(value) 83 | 84 | @classmethod 85 | def decode(cls, value: Any): 86 | assert fastapi is not None, "fastapi must be installed to use FastAPICoder.decode" 87 | return objDecode(value,fastAPIdecode()) 88 | 89 | class StarletteCoder(FastAPICoder): 90 | pass 91 | -------------------------------------------------------------------------------- /detacache/core/_decorators.py: -------------------------------------------------------------------------------- 1 | 2 | import asyncio 3 | 4 | from functools import wraps 5 | from typing import Any, Type, Callable 6 | 7 | from ._coder import Coder, DetaCoder, FastAPICoder,StarletteCoder 8 | from ._key import KeyGenerator, DetaKey, FastAPIKey,StarletteKey 9 | from ._detaBase import SyncBase, AsyncBase 10 | from ._helpers import getCurrentTimestamp 11 | 12 | 13 | class BaseDecorator: 14 | 15 | def __init__(self, 16 | projectKey: str = None, 17 | projectId: str = None, 18 | baseName: str = 'cache', 19 | key: KeyGenerator = None, 20 | coder: Coder = None, 21 | expire: int = 0, 22 | ): 23 | 24 | self._syncDb = SyncBase(projectKey, baseName, projectId) 25 | self._asyncDb = AsyncBase(projectKey, baseName, projectId) 26 | self.key = key 27 | self.coder = coder 28 | self.expire = expire 29 | 30 | def putDataInBase(self, response: Any, coder: Coder, expire: int) -> dict: 31 | return { 32 | 'value': coder.encode(response), 33 | 'type': type(response).__name__, 34 | '__expires': getCurrentTimestamp() + expire, 35 | } 36 | 37 | def cache(self, 38 | expire: int = 0, 39 | key: KeyGenerator = None, 40 | coder: Coder = None, 41 | ) -> None: 42 | 43 | key = key or self.key 44 | coder = coder or self.coder 45 | expire = expire or self.expire 46 | 47 | def wrapped(function): 48 | 49 | @wraps(function) 50 | async def asyncWrappedFunction(*args, **kwargs): 51 | _key = key.generate(function, args, kwargs) 52 | cached = await self._asyncDb.get(key.generate(function, args, kwargs)) 53 | 54 | if not cached: 55 | functionResponse = await function(*args, **kwargs) 56 | await self._asyncDb.put(self.putDataInBase(functionResponse, coder, expire), _key) 57 | return functionResponse 58 | 59 | return coder.decode(cached) 60 | 61 | @wraps(function) 62 | def syncWrappedFunction(*args, **kwargs): 63 | _key = key.generate(function, args, kwargs) 64 | cached = self._syncDb.get(_key) 65 | 66 | if not cached: 67 | functionResponse = function(*args, **kwargs) 68 | self._syncDb.put(self.putDataInBase( 69 | functionResponse, coder, expire), _key) 70 | return functionResponse 71 | 72 | return coder.decode(cached) 73 | 74 | if asyncio.iscoroutinefunction(function): 75 | return asyncWrappedFunction 76 | else: 77 | return syncWrappedFunction 78 | 79 | return wrapped 80 | 81 | 82 | class DetaCache(BaseDecorator): 83 | def __init__(self, 84 | projectKey: str = None, 85 | projectId: str = None, 86 | baseName: str = 'cache', 87 | key: KeyGenerator = DetaKey, 88 | coder: Coder = DetaCoder, 89 | ): 90 | super().__init__(projectKey, projectId, baseName, key, coder) 91 | 92 | class FastAPICache(BaseDecorator): 93 | def __init__(self, 94 | projectKey: str = None, 95 | projectId: str = None, 96 | baseName: str = 'cache', 97 | key: KeyGenerator = FastAPIKey, 98 | coder: Coder = FastAPICoder, 99 | ): 100 | super().__init__(projectKey, projectId, baseName, key, coder) 101 | 102 | class StarletteCache(BaseDecorator): 103 | def __init__(self, 104 | projectKey: str = None, 105 | projectId: str = None, 106 | baseName: str = 'cache', 107 | key: KeyGenerator = StarletteKey, 108 | coder: Coder = StarletteCoder, 109 | ): 110 | super().__init__(projectKey, projectId, baseName, key, coder) -------------------------------------------------------------------------------- /detacache/core/_detaBase.py: -------------------------------------------------------------------------------- 1 | import typing 2 | import aiohttp 3 | import requests 4 | 5 | 6 | class DetaBase: 7 | def __init__(self,projectKey: str ,baseName: str , projectId: str = None): 8 | if not "_" in projectKey: 9 | raise ValueError("Bad project key provided") 10 | 11 | if not projectId: 12 | projectId = projectKey.split("_")[0] 13 | 14 | self._headers = { 15 | "Content-type": "application/json", 16 | "X-API-Key": projectKey, 17 | } 18 | self._baseUrl = f'https://database.deta.sh/v1/{projectId}/{baseName}' 19 | 20 | def put(self, data: dict) -> dict: 21 | raise NotImplementedError 22 | 23 | def get(self, key: str) -> dict: 24 | raise NotImplementedError 25 | 26 | class SyncBase(DetaBase): 27 | def __init__(self, projectKey: str, baseName: str, projectId: str = None): 28 | super().__init__(projectKey, baseName, projectId) 29 | 30 | def put(self, data: typing.Union[dict, list, str, int, bool],key: str = None) -> dict: 31 | if key: 32 | data["key"] = key 33 | with requests.Session() as _session: 34 | with _session.put(f"{self._baseUrl}/items", json={"items": [data]},headers=self._headers) as resp: 35 | return resp.json()["processed"]["items"][0] 36 | 37 | def get(self, key: str) -> dict: 38 | with requests.Session() as _session: 39 | with _session.get(f"{self._baseUrl}/items/{key}",headers=self._headers) as resp: 40 | _res = resp.json() 41 | return _res if len(_res) > 1 else None 42 | 43 | class AsyncBase(DetaBase): 44 | def __init__(self, projectKey: str, baseName: str, projectId: str = None): 45 | super().__init__(projectKey, baseName, projectId) 46 | 47 | async def put(self, data: typing.Union[dict, list, str, int, bool],key: str = None)-> dict: 48 | if key: 49 | data["key"] = key 50 | async with aiohttp.ClientSession(headers=self._headers) as _session: 51 | async with _session.put(f"{self._baseUrl}/items", json={"items": [data]}) as resp: 52 | _res = await resp.json() 53 | return _res["processed"]["items"][0] 54 | 55 | async def get(self, key: str)-> dict: 56 | async with aiohttp.ClientSession(headers=self._headers) as _session: 57 | async with _session.get(f"{self._baseUrl}/items/{key}") as resp: 58 | _res = await resp.json() 59 | return _res if len(_res) > 1 else None -------------------------------------------------------------------------------- /detacache/core/_helpers.py: -------------------------------------------------------------------------------- 1 | 2 | import hashlib 3 | import datetime 4 | 5 | 6 | def createStringHashKey(string: str): 7 | '''Returns a md5 Hash of string as `string`''' 8 | return hashlib.md5(str(string).encode()).hexdigest() 9 | 10 | 11 | def getCurrentTimestamp(): 12 | '''Returns Current Timestamp as `int`''' 13 | return int(round(datetime.datetime.now().timestamp())) 14 | 15 | 16 | -------------------------------------------------------------------------------- /detacache/core/_key.py: -------------------------------------------------------------------------------- 1 | 2 | import inspect 3 | import typing 4 | 5 | from ._helpers import createStringHashKey 6 | 7 | class KeyGenerator: 8 | 9 | @classmethod 10 | def generate(cls, function:typing.Callable, args: tuple, kwargs: dict) -> str: 11 | raise NotImplementedError 12 | 13 | 14 | 15 | class DetaKey(KeyGenerator): 16 | @classmethod 17 | def generate(cls,function:typing.Callable, args: tuple, kwargs: dict): 18 | '''Returns a deta cache key''' 19 | 20 | argspec = inspect.getfullargspec(function) 21 | 22 | data = {str(argspec.args[index]): str(arg) if isinstance(arg, 23 | (dict, list, tuple, set, str, int, bool)) else None for index, arg in enumerate(args) 24 | if argspec.args and type(argspec.args is list)} 25 | 26 | data.update({str(k): str(v) if isinstance(v, (dict, list, tuple, set, str, int, bool)) 27 | else None for k, v in kwargs.items() if kwargs}) 28 | 29 | return createStringHashKey(f'{function.__name__}{data}') 30 | 31 | try: 32 | import fastapi 33 | from fastapi.requests import Request 34 | except ImportError: 35 | fastapi = None 36 | 37 | class FastAPIKey(KeyGenerator): 38 | 39 | @classmethod 40 | def generate(cls,function:typing.Callable, args: tuple, kwargs: dict,): 41 | '''Returns a deta cache key''' 42 | 43 | assert fastapi is not None, "fastapi must be installed to use FastAPIKey" 44 | 45 | request = kwargs.get('request') 46 | 47 | assert request, f"function {function.__name__} needs a `request` argument" 48 | 49 | if not isinstance(request, Request): 50 | raise Exception("`request` must be an instance of `fastapi.request.Request`") 51 | 52 | return createStringHashKey(f'{function.__name__}{request.url}') 53 | 54 | try: 55 | import starlette 56 | from starlette.requests import Request 57 | except ImportError: 58 | starlette = None 59 | 60 | 61 | class StarletteKey(KeyGenerator): 62 | 63 | @classmethod 64 | def generate(cls,function:typing.Callable, args: tuple, kwargs: dict,): 65 | '''Returns a deta cache key''' 66 | 67 | assert starlette is not None, "starlette must be installed to use StarletteKey" 68 | 69 | request =None 70 | 71 | for i in args: 72 | if isinstance(i, Request): 73 | request = i 74 | break 75 | 76 | assert request, f"function {function.__name__} needs a `request` argument" 77 | 78 | if not isinstance(request, Request): 79 | raise Exception("`request` must be an instance of `starlette.request.Request`") 80 | 81 | return createStringHashKey(f'{function.__name__}{request.url}') -------------------------------------------------------------------------------- /detacache/decorators.py: -------------------------------------------------------------------------------- 1 | from .core._decorators import BaseDecorator,DetaCache,FastAPICache,StarletteCache 2 | 3 | __all__ = [ 4 | 'BaseDecorator', 5 | 'DetaCache', 6 | 'FastAPICache', 7 | 'StarletteCache' 8 | ] 9 | -------------------------------------------------------------------------------- /detacache/key.py: -------------------------------------------------------------------------------- 1 | from .core._key import DetaKey,FastAPIKey,StarletteKey 2 | 3 | __all__ = [ 4 | 'DetaKey', 5 | 'FastAPIKey', 6 | 'StarletteKey' 7 | ] 8 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import aiohttp 3 | import requests 4 | 5 | from detacache import DetaCache 6 | 7 | 8 | app = DetaCache(projectKey='projectKey') 9 | 10 | 11 | @app.cache(expire=20) 12 | async def asyncgetJson(url: str): 13 | async with aiohttp.ClientSession() as session: 14 | async with session.get(url) as response: 15 | return await response.json() 16 | 17 | 18 | @app.cache(expire=20) 19 | async def asyncgetText(url: str): 20 | async with aiohttp.ClientSession() as session: 21 | async with session.get(url) as response: 22 | return await response.text() 23 | 24 | 25 | @app.cache(expire=10) 26 | def syncgetJson(url: str): 27 | return requests.get(url).json() 28 | 29 | 30 | @app.cache(expire=10) 31 | def syncgetText(url: str): 32 | return requests.get(url).text 33 | 34 | 35 | async def main(): 36 | asyncJsonData = await asyncgetJson('https://httpbin.org/json') 37 | print(asyncJsonData) 38 | syncJsonData = syncgetJson('https://httpbin.org/json') 39 | print(syncJsonData) 40 | asyncTextData = await asyncgetText('https://httpbin.org/html') 41 | print(asyncTextData) 42 | syncTextData = syncgetText('https://httpbin.org/html') 43 | print(syncTextData) 44 | 45 | 46 | loop = asyncio.get_event_loop() 47 | loop.run_until_complete(main()) 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | 7 | setup( 8 | name='detacache', 9 | packages=['detacache'], 10 | version='v0.1.2', 11 | license='MIT', 12 | description='Async and Sync Function Decorator to cache function call\'s to Deta base', 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | author='vidya sagar', 16 | author_email='svidya051@gmail.com', 17 | url='https://github.com/vidyasagar1432/detacache', 18 | keywords=['deta', 'cache', 'asyncio', 'deta base cache','fastapi cache', 19 | 'cache api call', 'cache functions', 'cache requests'], 20 | install_requires=[ 21 | 'aiohttp', 22 | 'requests', 23 | ], 24 | include_package_data=True, 25 | zip_safe=False, 26 | classifiers=[ 27 | # Chose either "3 - Alpha", "4 - Beta" or "5 - Production/Stable" as the current state of your package 28 | 'Development Status :: 4 - Beta', 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python :: 3', 33 | ], 34 | python_requires='>=3.6', 35 | ) 36 | --------------------------------------------------------------------------------