├── .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 |
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 |
96 | ©2012 Francisco Campos Arias, All Rigts Reserved.
97 |
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 |
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 |
304 | ©2012 Francisco Campos Arias, All Rigts Reserved.
305 |
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 |
--------------------------------------------------------------------------------