From a4fc4ce1c611586cea3221f0b8ab87ab3b571483 Mon Sep 17 00:00:00 2001 From: Galtozzy Date: Sun, 22 Sep 2024 19:31:09 +0200 Subject: [PATCH] Docs refactor WIP --- .gitignore | 6 +- README.md | 140 ++++++--- docs/img/type-annotations-2.png | Bin 0 -> 53783 bytes docs/img/type-annotations-3.png | Bin 0 -> 33926 bytes docs/img/type-annotations.png | Bin 0 -> 54748 bytes docs/index.md | 77 +++-- docs/scripts/hooks.py | 15 + .../cached-decorator/dynamic-cache-keys.md | 74 +++++ docs/tutorial/cached-decorator/first-steps.md | 88 ++++++ .../cached-decorator/reset-attribute.md | 89 ++++++ .../specifying-ttl-and-encoder-decoder.md | 182 +++++++++++ docs/tutorial/index.md | 5 + docs/tutorial/initialization.md | 45 +++ docs/tutorial/install.md | 22 ++ docs/tutorial/locks/lock-parameters.md | 0 docs/tutorial/locks/locks-intro.md | 0 docs/tutorial/locks/simple-locks.md | 0 mkdocs.yml | 109 ++++--- poetry.lock | 284 ++++++++++-------- py_cachify/__init__.py | 8 +- py_cachify/asyncio.py | 4 +- py_cachify/backend/cached.py | 40 ++- py_cachify/backend/helpers.py | 14 + py_cachify/backend/lib.py | 22 +- py_cachify/backend/lock.py | 241 ++++++++++++--- py_cachify/backend/types.py | 55 +++- pyproject.toml | 25 +- sonar-project.properties | 2 +- tests/conftest.py | 1 + tests/test_backend.py | 2 +- tests/test_cached.py | 6 +- tests/test_helpers.py | 25 +- tests/test_lock_decorator.py | 194 ++++++++++++ tests/test_locks.py | 155 +++++++++- tests/test_once_decorator.py | 6 +- 35 files changed, 1612 insertions(+), 324 deletions(-) create mode 100644 docs/img/type-annotations-2.png create mode 100644 docs/img/type-annotations-3.png create mode 100644 docs/img/type-annotations.png create mode 100644 docs/scripts/hooks.py create mode 100644 docs/tutorial/cached-decorator/dynamic-cache-keys.md create mode 100644 docs/tutorial/cached-decorator/first-steps.md create mode 100644 docs/tutorial/cached-decorator/reset-attribute.md create mode 100644 docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md create mode 100644 docs/tutorial/index.md create mode 100644 docs/tutorial/initialization.md create mode 100644 docs/tutorial/install.md create mode 100644 docs/tutorial/locks/lock-parameters.md create mode 100644 docs/tutorial/locks/locks-intro.md create mode 100644 docs/tutorial/locks/simple-locks.md create mode 100644 tests/test_lock_decorator.py diff --git a/.gitignore b/.gitignore index 09f41ca..1f1eb25 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,8 @@ site_build venv docs.zip archive.zip - +.zed +.zed/* # vim temporary files *~ .*.sw? @@ -56,6 +57,8 @@ wheels/ *.egg-info/ .installed.cfg *.egg +pyrightconfig.json +.zed # PyInstaller # Usually these files are written by a python script from a template @@ -320,4 +323,3 @@ Control.system-ca-bundle # Visual Studio Code # .vscode/ .history - diff --git a/README.md b/README.md index 2494c66..c75f8e7 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,79 @@ # Py-Cachify -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![PyPI version](https://badge.fury.io/py/py-cachify.svg)](https://badge.fury.io/py/py-cachify) -[![Documentation Status](https://readthedocs.org/projects/py-cachify/badge/?version=latest)](https://py-cachify.readthedocs.io/en/latest/?badge=latest) -[![Build Status](https://github.com/EzyGang/py-cachify/actions/workflows/checks.yml/badge.svg)]() -[![Tests Status](https://github.com/EzyGang/py-cachify/actions/workflows/integration-tests.yml/badge.svg)]() -[![Coverage Status](https://coveralls.io/repos/github/EzyGang/py-cachify/badge.png?branch=main)](https://coveralls.io/github/EzyGang/py-cachify?branch=main) -[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=EzyGang_py-cachify&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=EzyGang_py-cachify) +

+ + License + + + PyPI version + + + Documentation Status + +

+

+ + Pre-build checks and Tests + + + Tests Status + + + Coverage Status + + + Reliability Rating + +

+ +--- + +**Documentation**: https://py-cachify.readthedocs.io/latest/ + +**Source Code**: https://github.com/EzyGang/py-cachify + +--- + +py-cachify is a small library that provides useful cache based utilities. + +py-cachify works well in both sync and async environments, has 100% test coverage, fully type annotated, +is backend agnostic (you can provide your own client as long as it matches the signature), and supports Python from 3.8 and upward. -py-cachify is a library that provides small but useful cache utilities. +It offers distributed (cache based) locks and decorators to lock function executions and storing their results in the cache. -Some parts were heavily inspired by [douglasfarinelli's python-cachelock](https://github.com/douglasfarinelli/python-cachelock) lib, -which is sadly no longer maintained. +The key features are: -py-cachify works well in both sync and async environments, has 100% test coverage, -is backend agnostic (you can provide your own client as long as it matches the signature), and supports Python from 3.8 and upward. +* **Intuitive to write**: Great editor support. When applying decorators your IDE will still be able to autocomplete and highlight inline errors for applied functions. +* **Fully type annotated**: You don't have to constantly look into the docs. Everything is type annotated and easy to understand out of the box. +* **Short**: Minimize code duplication. Add just one line of code to implement a cache or lock on your function. +* **Start simple**: The simplest example adds only a couple of lines of code: initialize a library and use the provided utils. +* **Backend agnostic**: Use whatever cache-backend you want to use. Py-cachify is not forcing you into anything. +* **Test coverage**: Has 100% test coverage and supports Python 3.8+ -It offers distributed (cache based) locks and decorators for securing function executions and storing their results in the cache. +--- ## Table of Contents -- [Documentation](#documentation) - [Installation](#installation) -- [Examples](#examples) +- [How to use](#how-to-use) - [Contributing](#contributing) - [License](#license) -## Documentation - -Detailed documentation can be found at https://py-cachify.readthedocs.io/en/latest/. - ## Installation -To install: + + ```bash -pip install py-cachify +$ pip install py-cachify -# or if using poetry -poetry add py-cachify +---> 100% +Successfully installed py-cachify ``` -## Examples +## How to use -To start working with it, you'll have to initialize it using `init_cachify`: +You can read more about everything in here in the tutorial + +First, to start working with it, you'll have to initialize it using `init_cachify`: ```python from py_cachify import init_cachify @@ -59,21 +91,61 @@ init_cachify(sync_client=from_url(redis_url), async_client=async_from_url(async_ ``` Normally you wouldn't have to use both sync and async clients since an application usually works in a single mode i.e. sync/async. -Once initialized you can use everything that the library provides straight up without being worried about managing the cache yourself. +Once initialized you can use everything that the library provides straight up without being worried about managing the cache yourself. If you forgot to call `init_cachify` the `CachifyInitError` will be raised during runtime. + +Let's write the simple `@cached` example and utilize the flexibility of a dynamic key: ```python -from py_cachify import once +import asyncio + +from py_cachify import init_cachify, cached + + +# Cache the result of the following function with dynamic key +@cached(key='sum_two-{original}-{inc_by}') +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + # init the library + init_cachify() + + # Call the function + print(f'First call result: {await sum_two(5, 5)}') + + # And we will call it again to make sure it's not called but the result is the same + print(f'Second call result: {await sum_two(5, 5)}') + + # Now we will call it with different args to make sure the function is indeed called for another set of arguments + print(f'Third call result: {await sum_two(5, 10)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +Run the example: + +```bash +// Run our example +$ python main.py +// The ouput should be +Called with 5 5 +First call result: 10 +Second call result: 10 +Called with 5 10 +Third call result: 15 -# Make sure there is just one copy of a function running at a time -@once(key='long_running_function') -async def long_running_function() -> str: - # Executing long-running operation... - pass ``` +As you can see the first call was cached, and the second call got the result out of the cache, +but the third call was performed again since the arguments have changed. -For more detailed documentation and examples please visit https://py-cachify.readthedocs.io/en/latest/. +For more detailed tutorial visit #. ## Contributing @@ -81,4 +153,4 @@ If you'd like to contribute, please first discuss the changes using Issues, and ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](https://github.com/EzyGang/py-cachify/blob/main/LICENSE) file for details. diff --git a/docs/img/type-annotations-2.png b/docs/img/type-annotations-2.png new file mode 100644 index 0000000000000000000000000000000000000000..0589ab3a3e3073b341f0eed20f4b5bea5627351c GIT binary patch literal 53783 zcmbrlWmH>R7%fUmk)ox=C0Hp|ytozDBE{WGgIfr}DFi9SiaQi2?(SY3f_recU)N}FJet{~4ST1i424^P6`9*)`bzects#F3#=0(EP@5&HeBfgvIETwLn6Uuin0shAxA74<0%J#hp( z(C;WDN2#2z#WY6+Mv)-S_4V~vR8*xtZ~nb_PScIe&A&;PSC+IigQ#s;2&Yq5X$x>M z@y%Z4J`YdDS~d3A@^a+eB$58^+}hf4pjTV_vjHCZ%HyL+{oRk+<;D?S$E_sn?Cb(g ztKW#R?i}`PN}@h>^j!-U9Pdv_G7^`If>`fb(@)nJJ@otIrOaPU%M^uZT^#B6BhQm@ z3z5qtiP<^TZ!lC7ZL{u64C?Xfm)s~0kB*$ThsoG9|5>NN0hlGvT2;^WrJVL^8zcW3 z?6{DG&F;vq))6(C%L3h@VGVtAb91%6{l1hK z%hCu^Q5lqTwA!A^B=C88elfyiIf7CAWJJ4S0aSMf|K zk$qb7-=$AFG~~(-$|<((i7A zTHVmFgb?x{%(kl<`YKkC3(ebf$I8qPx99e|`@6CpzPNTrB|in&UK24)7rZZ-Ekj=i z2AghC@o?FKxj%2qc6RolGmsS3R-=C{Q~EQJns44aDE)Ti$G3QUj=}T&n(BLZv}V%_ zqiRlw1j$r9Mg_p?zT;p0fkI%tcj2$nn$&{xX4o$l zqZ^K!n302}JkCcGFQn1Ex;s0G&vyY7g@^ncO48rz-<&atM)^3?W7f^eC9X?fuPm3s$asWBNhdYB zoi>%ZF4GUoL-IcG@tKcj%T#20I$a*>{ja-W(6X{dE+4J4a~!Y}MMUReEC4`WM7ZzH z8!(AzrsHE?X=mcFZt#mYNHRAmJH>JEdAreS!v#P20L$*ryi3}>+N!3i*@Wn5F=vAq?DQ52G~5bX z1O2Uw*PSMztOT*d*%@S{YtRuBU2YoV;q<7I-r+4~rwiBM>SEOGGNlk`wWudXD`4!Iex46S zKeYZ+w-kg;+WU8T%D~B$bI%b){u+m>Au5R+G)S0%4<+oYboXenc=@#>-Udin-}{yJ zXR>#FFKD>`P?)q>;+2`iqf@Jrlo-VYHkS>k&FVex%MU~U3{_tm^B?|gIy@4txdO}! zDr^U;zSLO(s87Zess*w_>i)BO`(y1r~zF&#<#cJIdO7c>q>D={5k;SfEi9aM1Ux?}*hUj@0g zHD=+q-);{f&*=?LhM|vA&zOT?4v`C5zXSQ|H~uVRBe(7!drSZx6n>t6!JoT4e>PhE znrK^3NOn4R={@jUh!pod$GzJwe(XM7+KD%J$Y+kgG2l6e#jQe2{BZrek+|M>`?Sa) zceBn~jT+W(J--k6XX{t@I3AQl&v!B(`4!KLIe}pPOp}MR#r&dYf7YT#*ZEDZsWWb8 zQs*89X%*QSk{);%-oAaYwY4=^Bt(g3&xZ88`QXiGA*7-iw>ERN{83v2Ds(j+hog*( z)fvVZxk`-Hevngt+AMYZYHcsH{Aa*uOIl{c`Sf%W0VNjj;K6kiKTKLrgWgyKs7rHu z{kIU=Q^kv%kC{e|%ugumRV69!YWf}q;k_PIUG6(CPakW$*jO_U0f)t^>(76Szq&`w z^s&R{^EqM=xs6&9aIeA~u}5ch&tvGs>ZYlQ|bdz)LI9k`Xt z@fg;Z=y#WKLm66C#r*;etuuD;^M8?U#8Q1B-H6Iu{nL)fb= za3}VuwZMNW{m8BkL8beAze<59n^@aj|2E8Ux-SGsbB%Fn+^Egj7X~5J2p05(Zzim8 zSS{@BQH`9G_ud6vcxeDP65^Nz6tHev7ndhtq)9}tgPZEK4_VoIHC3s>X=P>7U?uE{ zd(<@t3*G*vYEFD#7<&W53}=NN0qGBURgOarW=W+eS`J^Yzcg)Uox~#YKiW@EYw=fl zr3_X&DLhD~z}spo@X(n$Ja;bvR+6N-SNt!o1=hqq_+2==u}G;pbCvyz1TCW zCZl`C9 zvP-v_-F?3Iu&uK(qIG{kF+NRA{_cc*APO#g?)V-mx--2>Lr;ss4#Tsu5utyzzEcwPUm6{rF;r9o0lQ|U#!+~E8nB!LOY67_FE$TPbZEmxlzbu z?@WooWqyfOtOUo?uw#5H8*bf;{}Rv^L*m@l+{#bPoVe3$Od=+cNi}Nlo1-n=+hjq- z>pD7y?~P6^@{PIfNE}y;q{?@IYW}y@E5Gg*t5pfj`uw7Dc9n_FRJB=#Si7Om^SI0X zGiFOlu6j=vRTJUn^IbjXa#+*fx+iCewi++?J-h{)h0XUT+&^eh5L&1(8UUp>aIu&t z=a-jA_%+-O+P{56X=rS;rbVsH^M7H`_vmyZkRb@xWL|15+XeX)~D^~k)4^BtDX5`e^{y0zU4Q+$%{Q3a&>j<431m=BTs#i ze(D6`oC?j76}AW@&mX5(F5Of%vxc4(n{e`C;f~k~q-jO`NO`x#(v6yn${02U-Z?K` z-PjZ9$c8=KA<80|n2I%gk8>C}IbY@G=Gr(h{u`b*{|V1BJFIaoaRT&|jw1C+*8w4a zkU1?CvTQh8N7Ts^=d}$V2i@UuPPOI2x5ek{uxZG!%uS0gtD}YwYqo~ubR>HysLSb` zKe4N)TcI((;&U13iyU#w9o%UYe}>MiZtW%&pc|>jsxi$MH>{T6Zqe;E&`6#ptp6>r zFnaE;FHb$gdy7(8cKd`xQDImwwITftd*eeGjBFq6`3F9HfH19Gk9}0F-Hj_O zv6fYCi1PFWx0?OB6q`2LE_5vExI*Dgkoh{)m$On7ZPK)LbQBTGh~e;t z<-eudtU8>8DPB(Hz8sWna~vY0oc9EHEerUUpauyv3X{a6-Fww!itA%OVsiWe2GWDf zMCe{WS69~zo6TJG&(nuyPt8yJfB!@CdpKP7Y?vvR>D+;PoVoM2uH4_FkPJu_jtby% zuqsoGpZGMHz$R;V=8@{-!%K4cSI!zzQjN~?L=wFY9gB8+<9 z2LgfOU5Kfvqpnu`Ot5*!KINo+;ySIUtm-wG-?GE3w&PeuUoTX^HLfmNB zyGW|7Ue$R@1T4IZCR!s|76{~rY!@B4D3P!3Tjdhlv!_ypF>99Pv&YeYM>p6xE3UX# zqfLPQ2+T~xezx8=(oM)H-ve25^>R!Y9f4pEn|6DAL8Xh!oxyBKELAdN974jK z+>)c7l-H#jWVDPozv;oZL<-IYvX{+s4N35g0(u z*7l~$gSAf@qPpd1v^0}fvJE{wxK@fLBMrD+_M!rk<1WfZ^0%Od#X?K<(>&gl*Eu%J zS_KvM_1QAGvAN{Z_}G*`_J@x=!J`b)q=uDe zRy19+Q3)ABVw#ay|W%{!fu4CDkrB|TeAwhbu(9u?$i6^T>i>iH1L@Uy6aGt> zJ>l!`|95im|HACa#a%kfwnj{ipuQ}7;_`N#>gqaOIjD#KUiQ3q6#pa8Yy%9eq!AIJ z3=Iuks1Nm5LJ|rfWvy4s9LMh<% z5&PaXRM&(0>+&tn3l+V9Z0K=Leyf781zAaWV{m@=AnfH58(4oA`4h1hv{Isr4>1t_ z*Zpu%GuMj+7uJhoC9dkm9r{UO*HUWVq)Ww)P0lOGg%K~PN=v^$w4AHF*QImz;-C`TO@W;DLhIZAMes6?(S)HO<%i?fum zYttf|T;>*pqh)zmGC!tZC>@9vJ_)|AEN)3D2Pq9*q7KK#qy{k#x2 zb3X-whh&gSx`iKN@$*DwF(>uoNb|2Q*EW7IpPhWC&*3Uyy3+^R#$QvPRh3J|6CfGS z?c7lEVl-1Y_dYw*AbW$0biUx{Bs`VYP)?oRL2=oHK4qXJZbAt?SwS0esJRoZb}#W!OKC zmNy~|B^!#JT6xG?cDitq_0?wfZ1OU-w+||s@NBv2@-4ZAQ^xJgn(hoS{%jOM-#cIb z34TkR+2V{|J;O-H&5UXAP(|`-+J4`dXO@A^+4IHWp`E(PQlRRxXizqf)_xk-6HDD= z1X&9DhedWjZoR4*^G`AWqN1WYZ$$7<@Eo1Y7&y|h4*m2?=x`vjt`|p_gHZpy{HrGB znss(ZyXkNp;TGhdgp1!L`WA1C5ke;b5`0=6JEj4NnIUm0)etkz#GB;Iu4!>fzB;#) zP!%~4_+<(UmI9=nHoXR;QP^0v$6sf<)}rUBw7+whMsJa`uZ=-ZvVQR2J>2VEUYES1 zzg@pe5(K>?X44N$NJvm0brHDfqdQ8F(*#t+EG+jqy^ATv(MB zfBZOM7vCPQhw<|kR=rQ|b?gMDHjo1fc%XOC3y{|a=~+1zXk%FemFQLO4nlSdnAK(v zydQ5@bcnA)0G{&LWL`1yvUg_FvJ68HUN>)zhXLuS=_+$oqjYaU$v3G7x;h3tDy@Hc zYIL@WID6AX&NC}ts$-XH_%&n57J3@Q`)Dprrks^jicm8?@;SXTe+p{ys;Y8sZhQ~t znU7Zg#z8?tKBZfe#L}M9LNX^t%lQ1!MCsnu5bVU_AlqIMCA0c*@B=&h?=Mo!#U|-o zB;u*n^64Co_7W*3G-aXuOkP4D@4L#3I73yrZaM)S3gO|GxP3xc1|+GNwydnu?w)IH z);y;duF!zO^H%HX(~fXK*X7u<>fYg1PLWC{GQL1rE1jfK4wBM8vOiMmW8dUXKjVLw z+Z9iUWpO~yq3GkbM_%$h5{kMN#LtX%rJNiIBz`|zM(UNY!bL=Z(EIj_N!$|m;>5Y9 z%QkNvr0Vc3J*P9hb9#tufG3B5oa9#<4<4*eJ_Qg|uxWZBN&~0Z16e3s9t z^xb%Eca99n7}c9@5Q5VgoMv1hxCo3PF@(B8=lA>_u1bxrsSbLv?i0~>Y;j#(X12hB z!C4=B+8Lj(fTsdTL84aD`|Aq?a)$i(aC^gh9v+_ft_I*J;Hh%ip3w^?8uK{cE*zb& ze$o$ephF=S}P#VA<$xy^@QaBGLvhlThKFBAzgoS z%b^qdyERW3>L%J+^nU0-+ryOaPJw>|bDHX8zG`1r5O(d)@ku2psvN#apt2s$alr?+ zsc#Xn6kA)*rAO!)K+Cl@tLp&5ps7zLGF`Y)Ryt&+B$*iq72XOi#y*uafoIgz)LMzX z*cnD%r$_=`CvVej!@Hm_ecme%d_wvHqi|?80iQkEq3B($8fwBO8NVrv2c+BL&XuQ} zoQ*#GPa0)um=ILZay}rk-(SCN)KPzR^vAYwXMAoVTL|3pUWezX}oAq5<9;9k?Eo^b6?;s8qdC50Sztx^wjQW_j!y}$n{d0Xk zg8Jy1R9xH_^Kl3!`3jdWL8j+deh#YK?)46HYnrbZyr4Qm<@%~zqwl1<`&N_kN++S% zD;Ww6W8+(CC;fz5itO=u?2WeHsXR`fw90R&Rhw&s2FpMZZLp*!foBXJ@dC{&rs}S) zwcUTW6b8+G4mYsBE8}V=937PddYk?vB5LVtnRpZEVnMt5OYs;QfPx zH3TB^iO;oLY9gARo?ak3Uz3Tl3|W>P&q!3i8%X`G=fR&+thjqTTf>U;VzdRl?7>Xn z;^x?Czvr07?^b*bAWHNsv|240f^JXLSRinj)ucM$57`rgWUU{HQF>mU2YVg+pp7sj zel4X6|0u_3_$DzB-lPwI=rnMT-A9B7l1$MZx;}nv$*C2j0;J6?F0$&xjtdRiGqM%685QTRw9<2%bFjj+aNqvS@>loP2&I@eX}*e)$OiLMNslw~Bzx8WT=jFh(!OIm zs7aCB$tkKuqgpa@nhq409G`@$>`!&WMJwjSF`d~Roa!rk%2fcJkHU^>?qrA-&%KrX zU4e`A@?>JtfOFnyWI;>&MkSqPds}JgXnGb@sQz%lreTc7hYH6ajSVkK6Y-9NtTE3n zKk_6GV$z3AjJSNga|Cr$oD9p4vZd`Pt9o7I>h?2b*0Ej*AG6E)bCaIn4jUzpg8zz! z=C_OtW92MR-pRVyhN_aaBSO;476UGl#tE{2?k%~ry1(&Vv^=xd>+$%PTfB!;h8yJS z*RCO2iG9h~_)I9w4SLKW-aX^s>Rh9a4K7`!ay^xts@rzkns`IFgT~(|J*|4$@64nWLV|gaFISaR4VNmzw^|U z)xG>oK)h`@vx>y&-^Wh-)*>9C1$7UX5aQ=kL}@|swY)9LCBu;qEtbED;-}|srBb`V zX+)>+CUUkHOb(7cl|Mea$4H8&5<)C4k5{7dGdyjEiBT5kgk83+R+N98T;_Rx*aCbu z@oHXF*Kq~7sgzYft~AVq_Qhl~yE_$y^XErKK4Dj;Et_mB+;VDdSRQ*Y_7! z5BZyUF}tR7o>P z4CQist6|6;KrV;JUi1iC&*|&2_S~<;)n&5jMVPP~gvl|B&qXyGG;)WF3a}N+@$mg-6-Nrsh zrVh4F_nZ5*-NTQHJ|Ru8fu3iF_$%{VPS0o^X$NK(7Yz7*apuE(Q*_UtYeXQ*+_dUT z3mkkFD#DVv4D;tj+0xKAGT#N?%;PtADF4i^`3|j}MxLK`+bn%dYG%AsxQrmtQObBq z=M8ZU695@L-Q`M(T479njeaCgQi}m{tt@JMVcJ9eMTVF!CZzSA+!x|bUAMKhD)}k? zjO%f^rDuPOO~_y`EwvRD@kzd?>x8M+{co;ax4D)^xF4{nr#id4;SgAYuFWVPA zshQwkzvXk!Qx})D`kvMmnrhNY*+vfFx&+)8|e>O557%DJUUA(CHjMOHl^ zt}&JN>QkKgc0IV&nM8n2XjoDBG-#9sOhr<1UW`w?s?{%xh$cm77~v>Ng>zHBt?6Be zqKc6b6=f~&t32J$K*r}jJc4h29ikVDQ+9<_ira^q(0wWT8HsM}K73_u_hG7Yyd%u@ zc^vQ#y}3>-~)) zTtS)BTU=dA1G(4IkT8<(|D4q=kJGWZNGRQd^f1tt*Xn`4h!EYyr&F7mIMdRJh!E^7 zh@;)VK2{r_2tn}!=r=?)e)p=oOO`4l<0}c3Qh}qR1~##V&&^8z z+`^1ay=*yRIU&lmX8`~KhkO7 z2M_ambY&IuIR1TxAEm-OtVKXpOP zyH5Xl&g;6lPym?8iQLp7nq|I9OLCR+bcMI1jW56>gZKTbnR|MAFflPnr)oRlLbf>V zTD&YacFU#-*zC9{mP3!K4P~Yr*DUu?yR4Km41yPAOz^ za-Nb(@}eL^@?f<3eU2lLt5b%vuxas`{D}(FaHU1U(dGy8wuF?>$NEeC_dmY(cE*@L z^>a}mF9v*dD#p$Fi`VMvKj1^d#}1Cv1cZg@X=x=I^PYf=nc%D!cexR7xx0(#JuA@7 z#dO4r>V@pQS5qWtrq~^RiPb}r2kTl%DDOVvdN~-Cbj1k6#FTuJmt9hCuigPOH&lOM zzuMc&ZNk0;@dtHB_zaK{eI@*RHy_JXz!)URti|>=9kB(a9!?4wwS=h|eAMr{^_bt? z-S2HB$aR7AE&d{Ox@kLN60WWDg1@1BN%rS zucBNS{|oY}1m~2|4=C>Lt5np83~sa-xR^Yh@8dVWKGKN)hfPo87RR$^qv{veC@L%N zFP1XWqJNz)WBvdxluvsT{K>=^6Jy7Jl)J9(bj4?7uN#@ha6%j>B7ph*MA9mTgKqNv z)_ZLJKFG+}`?;vP!T7H(a+?%_R|u(5)RcV#tvLk0O2N63H@0t(xR4rI`2jM`n6ET47F>%f6{4- zoelTt4oLn7)@FWRV}rc5twTmTOv-`x*F<9VD^HESuk!OLkzs?i@@Q zu#$492W##tc^6ciTm0scOJA+&tpJXHnS%}W<$k1_tHzF@p#jw0k=pa?3vJoHqEg!V zrpGY)+c6veC=X*0eaWuJq3!5tCe@R?&Ii3nH$r_V?%6QaLQC5N5gS#%=hhcZyk9dB z@_Ftz%7r7AZDe>l*~98=Bw4jkok6rNz+qAn5z%+{owTN=zHrHPIIA!ZH{FN|^T&z$ zBK_C*;c|AKNuL8DEpDWYZkvS%bEUr{5ER7JzX&1A`jt;&OaI{= zN=SXWj2DP8k4;JXlZU9v(SX#x$UaikXn0w)Hz^W^w}s%gl%V9^?Kh13Hm2)GdBcfq z|0#Wcxdrp0>r^sLSTnH0isMgSP*rwwjtd`e}_V=qHgz{B00n_H1UgfWbn;!YU z8@XTylbS2JsHg&coGv`VOlc#I%<%1BxyKTsV4ZdpwE%W7dSbDR zQb{je`>=%X+2rvNrTli&3N>^RT>`R60!w7Q46ygBZ%JsY!9GU|wKismmB$B2Ek$JR z3qc_>J?RhZYQm9Mr#;(&gl~~I>mt*W)QBa2auy2jeSA+`G zc88NqY>Z!i@Xw{{FTe<4f7MFjda2jbfkc z{tt;eo&45-=Nj8|<_KPvW7Pcjl6#>`yEgldAXe=XHQ_$*zvFU6!+l}Wj^e(cAGnG|r>VdTN2kZBQ^^663VsE0hasLHpaLc-0f zB;u%MJhL?3GeUu*%P8gjnuLg(@~1Kz06#kyd=Q*(Ldb$)+x6Wc+O*a@fEIBVp6mJh01qS@T%5yS_h)>a$; zVwHv*9uax6Asaij<^j^~TgUUZy$dQTR9_3`yYP>|owkPaA$8Ja$9d8=6%*-%6?mW` zx%95norZ{r7T}~Vz6CiRjQ*kN*oPh0G`yUjWZAzbBklx&Fpk@qxop)PAob0PaZC!) zpBgA*dZrH8wK|rxSEv@+A=>?MmL}v1xnQytFz7lM)SntkrZhTK_w+qEBYz3;WBgo& z+uV0Tr_<)B=aznhc+!O~qs!TBr9t-13%xNLb2C%^``Kalp;E7XOl@r=@|x--Mh{NJ z)}GY5c2C}WQ%4!zdKmYivANVm2nK47KU*XFT46;c4|4u$>|+?WWNSgNbt#J`$CcSo zb7>ex3bslSl_;q%S!9miiv^Z51@(4!&OT!A`!;2e-sLy`(z8PpgEoACNP(Lx8LlBP z7A|VlF^M8aVf!bhYF+`loaJ3MFB*Ja0$ngrSC1yPAxi@~sB6T>CoWlddNu7j8o#wi zE+={E8tqWV6c~Qg|8eWnL*<=gy0ad-ojvJuU0`899b?=oEy3Ets7H7?Lv zYS*|Vu0G!U;=;fefl;w_L?)_O`De0I@Y$(3+KBkxA(QOI-E&VnQ1J^Ashn>hM+><@ zSkO-2$Gdk%#*8OW>bsAvzm)!Isp?G~S_kmV+0kacoEV&BSpIi&cyhyg z1F`?zhwk}*bdbZI`o3mM4RY@p{+;$TGxz)dk1q9FttSzI-ViQgW@-5pwJiK2N~sK& zTBdfSJ9>Myt4}q9~rC{)LG6Hp6!F9H@iIEbfx>2Nwn>A$e^01HA z&d-{fglfiEp{M=zmFB%^mh#H!MGs|V?=>=Wdf-)*k-JDunT ze-V-F${|pc0|FWMZCGkq*~RVBOnZt$&Wh{n<*-|m0;jC|)5J^$j;dn3pTr*;X0TAq zL>}$i_gDnuFHf*BWfjcWJw2OhS#iL?nU*cgsBkGOX&k!j*Gx56F}Ii+6ZtSHJ4u$> zdy(AKa105v@u^WZ36yEMNmp_nLxXh*_+e17rZLf$$(UqNrS$tFStMGp87zBRb4`+f zk{BwedEMfTT2s9p%We7Y>xhX2J(z@Tb7D1Gq>q74NFAGu9H_(p_vg=}L#s*2GD?A! zyisFrvyyosk?+;j^*WLzwY6HbEcsgN3Y(Qr#tb18YdD@MD zVc@RUQD0}+Xx(5~L6lUQY%wqPPU#_PYFnhmdu(wrEM?z?ZS!n>eLvEZ+JXfY7B1>k8~ldvy+ zTS!r)gx^LJ3kfW@N!TckEO)B&URK3Ck0ahL03EW#a9a|COX}W!6kXlj4Bszu;Vm%5 zI)mkO{a!oKTzc;5X6^j&>-6i8zMW*#{@aq0Qmd)u;Pn8?Db08rog|DEMrQk9MS2K( zXO=okOH&b!2Z>5?I~gFp?_HicDu=YSdKs3*O5g9lWCg{gk`gdI=|68f#c2Z9EpQ#F z{c=B^{DR8e9o{%^|9PNA>o*OQ{iEK7zS=dv@%_iAJQ3$y*E+hLJLV7kGljP9LMiz? zWNi=+Yl4ME11-IZt%8<6uU0mhc@%Vo3T)@P^z_sv6}=K}(z(AQ z&uHt=?7&}weLq+VQo{X*^If)uKp7KclwbQFxmh5hnGr8vMg{qqet%s@b!5Nq0Zs{+ zAmJAlCd&N!8A~bbY|51rABX(F*7%!A;_973E`0`O)5I6d{DiUA zU26h5Jvq|=zuUR9))H@oO;2lT+%xB&y#10X99lQ?ZmYM-X&yaJjSSR`{Tmirn(8qR zR8T0GKz#jn0CBSVt?0;8o7;7V9T!qqz{HwYHBu??7}5=Y>h5*uw~sjxpH06@dmkwC zr|2>xafyRpq~-pV>_vF_M#AWIrjmLhc4lbVsb*Dc(gGW6nghU?+JN!L_4wH4D? z%(q1F^VX%L=Qd^TAMW?7yW$rEWn_kHTJ%|*4yH>>B-s6kGWM-7ZFRRwlXv_rdgnSZ zYLQwIQAVtHr~zE|Gm=}xUtYF+3<*+{)@=EV-hgVlmKY~Q6g|a!aAJpM(ifoI7afOo zNw7JO8NAVX^7jL)yiH2{8x^Mz)6gy$GWz2tS?u3Jkiy=XdXBHJaqr)277jA)VKOn2 zSA>c`bUv&XfNFA_paILJ=mc5d+XU-hv zp|w?J`8IqA&bTu(%dxlBub31r?6wEm1ub-LAe%N;Kv;2V5fE;soZk2j^|*^PsAXG7lI6G z2|)+qtbHJoQr=Bg(b$cSAH(5-N?I*oS)2Clu7qM58%De7FP1@zVXVv)l;VI_fibTK z8Z?aZKE_`W5K=pUIyd#*2&dc9u2W+(=Rb2J6wx^hV`VvEaw&dq%7yTEN$E73aJ}>Lv4#MHS?gg zCh3($kCco=f=w9s$T+y|>?VNfeIFTUlaVLpZakUKt1xCfQbnZ*N8orqLSs}X0Yr#O zBwj~eT?r);#M=;=Tq7e7F1NVeQ-GFq61ssF%Nw8Y@6cXNQj*=qtRk9;k$0-F*PqrX z-`u742>jfXV;;-&sF3iv<+TR)G)m~Lx5gZAZpfKGhN@I&uTaLz4T%9YtY*QBzMA_R z_1jPRZe3d4ua$&{0y%B`5QLE6TNz(KJ!*p9VN4HG@<;w1mu)gehoRoHz{(OfLDuAT zh0&87iRTAW>YCe!u^i#JvA(n{*pxsL{%L0?iw$+R=e(w6c6O($y;>^tMxcAD#@O}9 zn9_PXd-CvaKjj%jSO*m=9CwP~P)AQ8gEhLRQ zsw@Y-^13yHN=wu{fwEU3BZJzcKAUA{eHBp?Ld?8v| zaRn>7­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 MVrXGde$fs4AL~&e$N&HU literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index fef1f7b..7a0db72 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,31 +1,68 @@ -# py-cachify +# Py-Cachify -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![PyPI version](https://badge.fury.io/py/py-cachify.svg)](https://badge.fury.io/py/py-cachify) -[![Coverage Status](https://coveralls.io/repos/github/EzyGang/py-cachify/badge.png?branch=main)](https://coveralls.io/github/EzyGang/py-cachify?branch=main) + +

+ + Pre-build checks and Tests + + + Tests Status + + + Coverage Status + + + Reliability Rating + +

-[![Homepage](https://github.githubassets.com/favicons/favicon-dark.png)](https://github.com/EzyGang/py-cachify) +--- -py-cachify is a library that provides small but useful cache-based utilities. +**Documentation**: https://py-cachify.readthedocs.io/latest/ -Some parts were heavily inspired by [douglasfarinelli's python-cachelock](https://github.com/douglasfarinelli/python-cachelock) lib, -which is sadly no longer maintained. +**Source Code**: https://github.com/EzyGang/py-cachify -## Features -* Offers distributed cache-based locks and decorators around them -* Offers convenient decorators to store function results in the cache -* Works well in both sync and async environments -* Fully type annotated -* Has 100% test coverage -* Has integration tests in place for the common scenarios -* Backend agnostic (you can provide your own client as long as it matches the signature) -* Supports Python from 3.8 and upward +--- +py-cachify is a small library that provides useful cache based utilities (caching, locks). + +py-cachify works well in both sync and async environments, has 100% test coverage, fully type annotated, +is backend agnostic (you can provide your own client as long as it matches the signature), and supports Python from 3.8 and upward. + +It offers distributed (cache based) locks and decorators to lock function executions and storing their results in the cache. + +The key features are: + +* **Intuitive to write**: Great editor support. When applying decorators your IDE will still be able to autocomplete and highlight inline errors for applied functions. +* **Fully type annotated**: You don't have to constantly look into the docs. Everything is type annotated and easy to understand out of the box. +* **Short**: Minimize code duplication. Add just one line of code to implement a cache or lock on your function. +* **Start simple**: The simplest example adds only a couple of lines of code: initialize a library and use the needed utility. +* **Backend agnostic**: Use whatever cache-backend you want to use. Py-cachify is not forcing you into anything. +* **Test coverage**: Has 100% test coverage and supports Python 3.8+ + +--- ## Quick navigation -To help you get started, see [Initial setup](setup.md). +To help you get started, see [Initial setup](setup.md) and [Tutorial](tutorial/index.md). Examples can be found [here](examples.md). -More information about [`cached` decorator](cached-decorator.md), [`once` decorator](once-decorator.md), -and [locks](locks.md) can be found on linked pages. +For a detailed API reference [API] + +## Contributing + +If you'd like to contribute, please first discuss the changes using Issues, and then don't hesitate to shoot a PR which will be reviewed shortly. + +## License + +This project is licensed under the MIT License - see the [LICENSE](https://github.com/EzyGang/py-cachify/blob/main/LICENSE) file for details. diff --git a/docs/scripts/hooks.py b/docs/scripts/hooks.py new file mode 100644 index 0000000..5963ded --- /dev/null +++ b/docs/scripts/hooks.py @@ -0,0 +1,15 @@ +import re + +from mkdocs import plugins +from mkdocs.config.defaults import MkDocsConfig +from mkdocs.structure.nav import Navigation +from paginate import Page + + +@plugins.event_priority(-100) +def on_post_page(output_content: str, page: Page, config: MkDocsConfig) -> str: + + return re.sub( + r'(\/(.+\(\))\/)', r'\2', + output_content, + ) diff --git a/docs/tutorial/cached-decorator/dynamic-cache-keys.md b/docs/tutorial/cached-decorator/dynamic-cache-keys.md new file mode 100644 index 0000000..e252601 --- /dev/null +++ b/docs/tutorial/cached-decorator/dynamic-cache-keys.md @@ -0,0 +1,74 @@ +# Cached - Dynamic cache key arguments + +Continuing our previous example, we'll customize the key in the decorator. + +The full code will look like this: + +```python +import asyncio + +from py_cachify import init_cachify, cached + + +# here we are initializing py-cachify to use an in-memory cache +init_cachify() + + +# notice that we now have {a} and {b} in the cache key +@cached(key='sum_two-{a}-{b}') +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + # Call the function first time with (5, 5) + print(f'First call result: {await sum_two(5, 5)}') + + # And we will call it again to make sure it's not called but the result is the same + print(f'Second call result: {await sum_two(5, 5)}') + + # Now we will call it with different args to make sure the function is indeed called for another set of arguments + print(f'Third call result: {await sum_two(5, 10)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + + +## Understanding what has changed + +As you can see, we now have `{a}` and `{b}` inside our key, +what it allows py-cachify to do is dynamically craft a key for a function the decorator is being applied to. + +This way it will cache the result for each set of arguments instead of creating just one key. + +Note, that in this current example key `'sum_two-{}-{}'` will have the same effect. +Providing a not named placeholders is supported to allow creating dynamic cache keys even for the functions that accept `*args, **kwargs` as its arguments. + +We have also modified our main function to showcase the introduced changes. + +## Let's run our code + +After running the example: + +```bash +# Run our example +$ python main.py + +# The ouput will be +Called with 5 5 +First call result: 10 +Second call result: 10 +Called with 5 10 +Third call result: 15 + +``` + +As you can see, the function result is being cached based on the arguments provided. + +## What's next + +In the next chapter we'll learn what other parameters `@cached()` decorator has. diff --git a/docs/tutorial/cached-decorator/first-steps.md b/docs/tutorial/cached-decorator/first-steps.md new file mode 100644 index 0000000..fb81796 --- /dev/null +++ b/docs/tutorial/cached-decorator/first-steps.md @@ -0,0 +1,88 @@ +# Cached - First steps with py-cachify + +Judging by the package's name py-cachify provides cache based utilities, so let's start by doing some simple caching :) + +The initialization details could be found [here](../initialization.md). + +For the sake of all the examples in here we will use the in-memory cache and an async environment, but everything will be the same for the sync one. + +## Function to cache + +Let's start by creating a function that we are about to cache: + +```python +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b +``` + +So this function takes two integers and returns their sum. + +## Introducing py-cachify + +To cache a function all we have to do is wrap the function in the provided `@cached()` decorator. + +Also we'll implement a simple main function to run our example, the full code will look something like this: + +```python +import asyncio + +from py_cachify import init_cachify, cached + + +# here we initializing a py-cachify to use an in-memory cache +init_cachify() + + +@cached(key='sum_two') +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + print(f'First call result: {await sum_two(5, 5)}') + print(f'Second call result: {await sum_two(5, 5)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + + +## Running the example + +Now, let's run the example above. + + +```bash +# Run our example +$ python main.py + +# The ouput should be +Called with 5 5 +First call result: 10 +Second call result: 10 +``` + +So as you can see, the function result has been successfully cached on the first call, +and the second call to the function did not invoke an actual implementation and got it's result from cache. + + +## Type annotations + +Py-Cachify is **fully** type annotated, this enhanced the developer experience and lets your IDE keep doing the work it is supposed to be doing. + +As an example, our wrapped function keeps all it's type annotations and let's you keep writing the code comfortably. + +![Inline hints](../../img/type-annotations.png) + +And another example, in this case our LSP gives us the warning that we have forgotten to `await` the async function. + +![Inline hints 2](../../img/type-annotations-2.png) + +## What's next + +Next we will utilize the dynamic cache key to create cache results based on function arguments. diff --git a/docs/tutorial/cached-decorator/reset-attribute.md b/docs/tutorial/cached-decorator/reset-attribute.md new file mode 100644 index 0000000..b40cbd3 --- /dev/null +++ b/docs/tutorial/cached-decorator/reset-attribute.md @@ -0,0 +1,89 @@ +# Cached - Manually resetting cache with `reset()` method + +## How to + +Now it's type to see some ✨magic✨ happen. + +You could've wondered: + +What if I need to manually reset the cache on something I have cached using the `@cached` decorator? +Do I have to go all the way to my actual cache client and do the reset myself? How can I reset a dynamic key with certain arguments? + +Don't worry py-cachify has got you covered. + +## Introducing /reset()/ + +Everytime you wrap something with the provided decorators that py-cachify has, there is a method being attached to the function you are wrapping. + +Also, the method attached has the same type as the original function, so if it was async, the reset method will be async or the other way around for a sync function. + +`reset()` **has the same signature** as your declared function, this way you can easily reset even the dynamic key with no issues. + +## Changing our example + +Let's modify the code we ran previously in dynamic keys introduction: + +```python +import asyncio + +from py_cachify import init_cachify, cached + + +# here we are initializing py-cachify to use an in-memory cache +init_cachify() + + +# nothing is changing in declaration +@cached(key='sum_two-{a}-{b}') +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + # Call the function first time with (5, 5) + print(f'First call result: {await sum_two(5, 5)}') + + # Let's try resetting the cache for this specific call + await sum_two.reset(a=5, b=5) + + # And then call the function again to see what will happen + print(f'Second call result: {await sum_two(5, 5)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +We have added the reset call for a specific signature. + +Let's now run it and see the output: + +After running the example: + +```bash +# Run our example +$ python main.py + +# The ouput will be +Called with 5 5 +First call result: 10 +Called with 5 5 +Second call result: 10 + +``` + +And you can see that the cache has been reset between the two calls we have. + +## Type annotations + +The `reset()` function has the same signature as the original function, which is nice and allows your IDE to help you with inline hints and errors: + +![Inline hints 3](../../img/type-annotations-3.png) + +## Conclusion + +This concludes our tutorial for the `@cached()` decorator. + +Next we'll learn about the locks and a handy decorator that will help you incorporate them without a headache. diff --git a/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md b/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md new file mode 100644 index 0000000..7a6f2b4 --- /dev/null +++ b/docs/tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md @@ -0,0 +1,182 @@ +# Cached - Providing a ttl (time-to-live) and custom encoder/decoder + + +## Explanation + +Sometimes you don't need to cache a function result indefinitely and you need to cache it let's say for a day (a common case for web apps). + +Py-Cachify has got you covered and allows for an optional `ttl` param to pass into the decorator. +This value will be passed down to a cache client and usually means how long the set value will live for in seconds. + +## Let's see it in action + + +```python +import asyncio + +from py_cachify import init_cachify, cached + + +# here we are initializing py-cachify to use an in-memory cache +init_cachify() + + +# notice ttl, that will cache the result for one second +@cached(key='sum_two-{a}-{b}', ttl=1) +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + # Call the function first time with (5, 5) + print(f'First call result: {await increment_int_by(5, 5)}') + + # Let's wait for 2 seconds + await asyncio.sleep(2) + + # And we will call it again to check what will happen + print(f'Second call result: {await increment_int_by(5, 5)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +The only changes we introduced are the removal of the third call, adding the sleep, and providing a `ttl` param. + +After running the example: + +```console +// Run our example +$ python main.py + +// The ouput will be +Called with 5 5 +First call result: 10 +Called with 5 5 +Second call result: 10 + +``` + +As you can see the cache has expired and allowed the function to be called again. + +## Encoders/Decoders + +`ttl` is not the only param that `@cached()` has available. +There is also a `enc_dec` which accepts a tuple of `(Encoder, Decoder)`, +those being the methods that are going to be applied to the function result on caching and retrieving the cache value. + +The required signature is `Callable[[Any], Any]`. +But keep in mind that results should be picklable, py-cachify uses pickle, before passing the value to the cache backend. + +
+ℹ Why it was introduced +

+The main reason is sometimes you have to cache something, that is not picklable by default. + +Even though the cases are rare, we decided to support it since it doesn't hurt to have it when its needed :) +

+
+ + +## Introducing `enc_dec` + +Usually provided encoder and decoder are supposed to work in tandem and not change the output value at all (since encoder does something, and then decoder reverts it back). +But for the sake of our demonstration we'll break that principle. + +We'll introduce the following functions: + +```python + +# our encoder will multiply the result by 2 +def encoder(val: int) -> int: + return val * 2 + + +# and our decoder will do the multiplication by 3 +def decoder(val: int) -> int: + return val * 3 +``` + +Now, as a result, the final output should be multiplied by 6. + +All we have to do now, is modify our `@cached()` decorator params to look like this: + +```python +@cached(key='sum_two-{a}-{b}', enc_dec=(encoder, decoder)) +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b +``` + + +
+ℹ Full file preview +```python +import asyncio + +from py_cachify import init_cachify, cached + + +# here we are initializing py-cachify to use an in-memory cache +init_cachify() + + +# our encoder will multiply the result by 2 +def encoder(val: int) -> int: + return val * 2 + + +# and our decoder will do the multiplication by 3 +def decoder(val: int) -> int: + return val * 3 + + +# enc_dec is provided +@cached(key='sum_two-{a}-{b}', enc_dec=(encoder, decoder)) +async def sum_two(a: int, b: int) -> int: + # Let's put print here to see what was the function called with + print(f'Called with {a} {b}') + return a + b + + +async def main() -> None: + # Call the function first time with (5, 5), this is where the encoder will be applied before setting cache value + print(f'First call result: {await sum_two(5, 5)}') + + # Calling the function again with the same arguments to make decoder do its job on retrieving value from cache + print(f'Second call result: {await sum_two(5, 5)}') + + +if __name__ == '__main__': + asyncio.run(main()) +``` + +
+ + +## Running the code + +After running the currently crafted file, we should get the following output: + + +```bash +# Run our example +$ python main.py + +# The ouput will be +Called with 5 5 +First call result: 10 +Second call result: 60 + +``` + +As you can see, the second call result was 60, which is 6 times bigger than the original value. + + +## What's next + +We'll see some magic that py-cachify does on a function wrap and learn how to manually reset a cache. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md new file mode 100644 index 0000000..35828f4 --- /dev/null +++ b/docs/tutorial/index.md @@ -0,0 +1,5 @@ +# Learn + +This is the Py-Cachify tutorial - user guide. + +Here we'll go through everything that the package provides with basic examples, explanation, and most common cases on how to use py-cachify. diff --git a/docs/tutorial/initialization.md b/docs/tutorial/initialization.md new file mode 100644 index 0000000..d4170f4 --- /dev/null +++ b/docs/tutorial/initialization.md @@ -0,0 +1,45 @@ +# Initializing a library + +## Description + +First, to start working with the library, you will have to initialize it by using provided `init_cachify` function: +```python +from py_cachify import init_cachify + +init_cachify() +``` +By default, it will use an **in-memory** cache. + +
+⚠ In-memory cache details +

+The in-memory cache is not suitable to use in any sort of serious applications, +since every python process will use it's own memory and caching/locking won't work as expected. + +So be carefull using it and make sure it is suitable for your particular use case, +for example some simple script will probably be OK utilizing an in-memory cache, but a FastAPI app won't work as expected. +

+
+ +If you want to use Redis: +```python +from py_cachify import init_cachify +from redis.asyncio import from_url as async_from_url +from redis import from_url + +init_cachify(sync_client=from_url(redis_url), async_client=async_from_url(async_redis_client)) +``` +Normally you wouldn't have to use both sync and async clients since an application usually works in a single mode i.e. sync/async. + +Once initialized you can use everything that the library provides straight up without being worried about managing the cache yourself. + +❗ If you forgot to call `init_cachify` the `CachifyInitError` will be raised during runtime. + + +## Additional info on initialization + +The clients are not the only thing that this function accepts, so make sure to check out the **[Detailed initialization reference]()**. + +## What's next + +Next we'll learn about the `@cached()` decorator and how to use it. diff --git a/docs/tutorial/install.md b/docs/tutorial/install.md new file mode 100644 index 0000000..05dceda --- /dev/null +++ b/docs/tutorial/install.md @@ -0,0 +1,22 @@ +# Installation + +Before starting, create a project directory, and then create a **virtual environment** in it to install the packages. + +Install via pip: + +```bash +$ pip install py-cachify + +---> 100% +Successfully installed py-cachify +``` + +Or if you use poetry: + +```bash +$ poetry add py-cachify + +---> 100% +Using version * for py-cachify +Successfully installed py-cachify +``` diff --git a/docs/tutorial/locks/lock-parameters.md b/docs/tutorial/locks/lock-parameters.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial/locks/locks-intro.md b/docs/tutorial/locks/locks-intro.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/tutorial/locks/simple-locks.md b/docs/tutorial/locks/simple-locks.md new file mode 100644 index 0000000..e69de29 diff --git a/mkdocs.yml b/mkdocs.yml index 96e5fd4..abeb3bf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,37 +3,34 @@ site_description: py-cachify. Cache and locks made easy. Fully type annotated. 1 repo_name: EzyGang/py-cachify repo_url: https://github.com/EzyGang/py-cachify - theme: name: material palette: - - media: '(prefers-color-scheme: light)' - scheme: default - primary: black - accent: cyan - toggle: - icon: material/lightbulb - name: Switch to dark mode - - media: '(prefers-color-scheme: dark)' - scheme: slate - primary: black - accent: cyan - toggle: - icon: material/lightbulb-outline - name: Switch to light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: black + accent: cyan + toggle: + icon: material/lightbulb + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: cyan + toggle: + icon: material/lightbulb-outline + name: Switch to light mode icon: repo: fontawesome/brands/github features: - content.code.annotate - content.code.copy + # - content.code.select - content.footnote.tooltips - content.tabs.link - content.tooltips - navigation.footer - navigation.indexes - - navigation.instant - - navigation.instant.prefetch - - navigation.instant.progress - navigation.path - navigation.tabs - navigation.tabs.sticky @@ -43,52 +40,70 @@ theme: - search.share - search.suggest - toc.follow + markdown_extensions: + # Python Markdown + abbr: + attr_list: + footnotes: + md_in_html: + tables: toc: permalink: true - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - pymdownx.inlinehilite: - pymdownx.snippets: + + # Python Markdown Extensions pymdownx.betterem: smart_enable: all pymdownx.caret: + pymdownx.highlight: + line_spans: __span + pymdownx.inlinehilite: pymdownx.keys: pymdownx.mark: pymdownx.superfences: custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format pymdownx.tilde: # pymdownx blocks pymdownx.blocks.admonition: types: - - note - - attention - - caution - - danger - - error - - tip - - hint - - warning - # Custom types - - info - - check + - note + - attention + - caution + - danger + - error + - tip + - hint + - warning + # Custom types + - info pymdownx.blocks.details: pymdownx.blocks.tab: alternate_style: True + plugins: - # Material for MkDocs - search: - termynal: + # Material for MkDocs + - search + - termynal nav: - - Introduction: 'index.md' - - Installation and Setup: 'setup.md' - - Base lock: 'locks.md' - - 'once-decorator.md' - - 'cached-decorator.md' - - 'examples.md' + - Introduction: "index.md" + - Tutorial - User Guide: + - tutorial/index.md + - tutorial/install.md + - tutorial/initialization.md + - Cached (/@cached()/ decorator): + - tutorial/cached-decorator/first-steps.md + - tutorial/cached-decorator/dynamic-cache-keys.md + - tutorial/cached-decorator/specifying-ttl-and-encoder-decoder.md + - Cached - Manually resetting cache with /reset()/ method: tutorial/cached-decorator/reset-attribute.md + - Installation and Setup: "setup.md" + - Base lock: "locks.md" + - "once-decorator.md" + - "cached-decorator.md" + - "examples.md" + +hooks: + - docs/scripts/hooks.py diff --git a/poetry.lock b/poetry.lock index 3eb8b88..7b1d63b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -283,33 +283,40 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" -version = "8.4.0" +version = "8.5.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, - {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" @@ -487,13 +494,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.34" +version = "9.5.36" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.34-py3-none-any.whl", hash = "sha256:54caa8be708de2b75167fd4d3b9f3d949579294f49cb242515d4653dbee9227e"}, - {file = "mkdocs_material-9.5.34.tar.gz", hash = "sha256:1e60ddf716cfb5679dfd65900b8a25d277064ed82d9a53cd5190e3f894df7840"}, + {file = "mkdocs_material-9.5.36-py3-none-any.whl", hash = "sha256:36734c1fd9404bea74236242ba3359b267fc930c7233b9fd086b0898825d0ac9"}, + {file = "mkdocs_material-9.5.36.tar.gz", hash = "sha256:140456f761320f72b399effc073fa3f8aac744c77b0970797c201cae2f6c967f"}, ] [package.dependencies] @@ -633,19 +640,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -706,13 +713,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pymdown-extensions" -version = "10.9" +version = "10.10" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"}, - {file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"}, + {file = "pymdown_extensions-10.10-py3-none-any.whl", hash = "sha256:5593c49032dc674c30c8382591324de92049520e6c3dd332bfba7f696d55ec34"}, + {file = "pymdown_extensions-10.10.tar.gz", hash = "sha256:13899c05c2d3cd486e9f57963f7154d2fed2c57530c50a7774fc13041610e82a"}, ] [package.dependencies] @@ -724,13 +731,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -827,13 +834,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] @@ -932,90 +939,105 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" [[package]] name = "regex" -version = "2024.7.24" +version = "2024.9.11" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" files = [ - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"}, - {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"}, - {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"}, - {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"}, - {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"}, - {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"}, - {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"}, - {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"}, - {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"}, - {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"}, - {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"}, - {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"}, - {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"}, - {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"}, - {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"}, - {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"}, - {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"}, - {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"}, - {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"}, - {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"}, - {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"}, - {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"}, - {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"}, - {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"}, - {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"}, - {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"}, - {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, + {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, + {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, + {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, + {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, + {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, + {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, + {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, + {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, + {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, + {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, + {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, + {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, + {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, ] [[package]] @@ -1041,29 +1063,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.6.3" +version = "0.6.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, - {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, - {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, - {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, - {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, - {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, - {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, + {file = "ruff-0.6.7-py3-none-linux_armv6l.whl", hash = "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2"}, + {file = "ruff-0.6.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a"}, + {file = "ruff-0.6.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18"}, + {file = "ruff-0.6.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14"}, + {file = "ruff-0.6.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb"}, + {file = "ruff-0.6.7-py3-none-win32.whl", hash = "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35"}, + {file = "ruff-0.6.7-py3-none-win_amd64.whl", hash = "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977"}, + {file = "ruff-0.6.7-py3-none-win_arm64.whl", hash = "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8"}, + {file = "ruff-0.6.7.tar.gz", hash = "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5"}, ] [[package]] @@ -1135,13 +1157,13 @@ files = [ [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -1199,13 +1221,13 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "zipp" -version = "3.20.1" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] diff --git a/py_cachify/__init__.py b/py_cachify/__init__.py index 8c6d808..5df7efc 100644 --- a/py_cachify/__init__.py +++ b/py_cachify/__init__.py @@ -1,14 +1,18 @@ +from . import asyncio, sync from .backend.cached import cached from .backend.exceptions import CachifyInitError, CachifyLockError -from .backend.helpers import Decoder, Encoder from .backend.lib import init_cachify -from .backend.lock import once +from .backend.lock import lock, once +from .backend.types import Decoder, Encoder +__version__ = '2.0.0' + __all__ = [ 'CachifyInitError', 'CachifyLockError', 'init_cachify', + 'lock', 'Encoder', 'Decoder', 'cached', diff --git a/py_cachify/asyncio.py b/py_cachify/asyncio.py index fb3a1bc..1b70b54 100644 --- a/py_cachify/asyncio.py +++ b/py_cachify/asyncio.py @@ -1,6 +1,6 @@ from .backend.cached import async_cached -from .backend.lock import async_lock, async_once +from .backend.lock import async_once from .backend.types import AsyncClient -__all__ = ['async_once', 'async_lock', 'async_cached', 'AsyncClient'] +__all__ = ['async_once', 'async_cached', 'AsyncClient'] diff --git a/py_cachify/backend/cached.py b/py_cachify/backend/cached.py index d0e7ca3..144c3fb 100644 --- a/py_cachify/backend/cached.py +++ b/py_cachify/backend/cached.py @@ -6,27 +6,45 @@ from .helpers import a_reset, encode_decode_value, get_full_key_from_signature, is_coroutine, reset from .lib import get_cachify -from .types import AsyncWithResetProtocol, Decoder, Encoder, SyncOrAsync, SyncWithResetProtocol +from .types import AsyncWithResetProto, Decoder, Encoder, SyncOrAsyncReset, SyncWithResetProto R = TypeVar('R') P = ParamSpec('P') -def cached(key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None) -> SyncOrAsync: +def cached( + key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None +) -> SyncOrAsyncReset: + """ + Decorator that caches the result of a function based on the specified key, time-to-live (ttl), + and encoding/decoding functions. + + Args: + key (str): The key used to identify the cached result, could be a format string. + ttl (Union[int, None], optional): The time-to-live for the cached result. + Defaults to None, means indefinitely. + enc_dec (Union[Tuple[Encoder, Decoder], None], optional): The encoding and decoding functions for the cached value. + Defaults to None. + + Returns: + WrappedFunction: Either a synchronous or asynchronous function with reset method attached to it, + reset(*args, **kwargs) matches the type of original function and could be used to reset the cache. + """ + @overload def _cached_inner( # type: ignore[overload-overlap] _func: Callable[P, Awaitable[R]], - ) -> AsyncWithResetProtocol[P, R]: ... + ) -> AsyncWithResetProto[P, R]: ... @overload def _cached_inner( _func: Callable[P, R], - ) -> SyncWithResetProtocol[P, R]: ... + ) -> SyncWithResetProto[P, R]: ... def _cached_inner( _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], - ) -> Union[AsyncWithResetProtocol[P, R], SyncWithResetProtocol[P, R]]: + ) -> Union[AsyncWithResetProto[P, R], SyncWithResetProto[P, R]]: signature = inspect.signature(_func) enc, dec = None, None @@ -49,7 +67,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) - return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + return cast(AsyncWithResetProto[P, R], _async_wrapper) else: @wraps(_func) # type: ignore[unreachable] @@ -65,20 +83,20 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> R: setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) - return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + return cast(SyncWithResetProto[P, R], _sync_wrapper) return _cached_inner -@deprecated('sync_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') +@deprecated('sync_cached is deprecated, use cached instead. Scheduled for removal in 3.0.0') def sync_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None -) -> SyncOrAsync: +) -> SyncOrAsyncReset: return cached(key=key, ttl=ttl, enc_dec=enc_dec) -@deprecated('async_cached is deprecated, use cached instead. Scheduled for removal in 1.3.0') +@deprecated('async_cached is deprecated, use cached instead. Scheduled for removal in 3.0.0') def async_cached( key: str, ttl: Union[int, None] = None, enc_dec: Union[Tuple[Encoder, Decoder], None] = None -) -> SyncOrAsync: +) -> SyncOrAsyncReset: return cached(key=key, ttl=ttl, enc_dec=enc_dec) diff --git a/py_cachify/backend/helpers.py b/py_cachify/backend/helpers.py index dcb3283..b616a8e 100644 --- a/py_cachify/backend/helpers.py +++ b/py_cachify/backend/helpers.py @@ -53,3 +53,17 @@ async def a_reset(*args: Any, key: str, signature: inspect.Signature, **kwargs: await cachify.a_delete(key=_key) return None + + +async def is_alocked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: + cachify = get_cachify() + _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) + + return bool(await cachify.a_get(key=_key)) + + +def is_locked(*args: Any, key: str, signature: inspect.Signature, **kwargs: Any) -> bool: + cachify = get_cachify() + _key = get_full_key_from_signature(bound_args=signature.bind(*args, **kwargs), key=key) + + return bool(cachify.get(key=_key)) diff --git a/py_cachify/backend/lib.py b/py_cachify/backend/lib.py index d1640d3..3669b95 100644 --- a/py_cachify/backend/lib.py +++ b/py_cachify/backend/lib.py @@ -11,11 +11,13 @@ def __init__( self, sync_client: Union[SyncClient, MemoryCache], async_client: Union[AsyncClient, AsyncWrapper], + default_expiration: Optional[int], prefix: str, ) -> None: self._sync_client = sync_client self._async_client = async_client self._prefix = prefix + self.default_expiration = default_expiration def set(self, key: str, val: Any, ttl: Union[int, None] = None) -> Any: self._sync_client.set(name=f'{self._prefix}{key}', value=pickle.dumps(val), ex=ttl) @@ -42,10 +44,26 @@ async def a_delete(self, key: str) -> Any: def init_cachify( sync_client: SyncClient = (mc := MemoryCache()), async_client: AsyncClient = AsyncWrapper(cache=mc), - prefix: str = '_PYC_', + default_lock_expiration: Optional[int] = 30, + prefix: str = 'PYC-', ) -> None: + """ + Initialize the Cachify instance with the specified clients and settings. + + Args: + sync_client (Union[SyncClient, MemoryCache], optional): The synchronous client to use. + Defaults to MemoryCache(). + async_client (Union[AsyncClient, AsyncWrapper], optional): The asynchronous client to use. + Defaults to AsyncWrapper(cache=MemoryCache()). + default_lock_expiration (Optional[int], optional): The default expiration time for locks. + Defaults to 30. + prefix (str, optional): The prefix to use for keys. Defaults to 'PYC-'. + """ + global _cachify - _cachify = Cachify(sync_client=sync_client, async_client=async_client, prefix=prefix) + _cachify = Cachify( + sync_client=sync_client, async_client=async_client, prefix=prefix, default_expiration=default_lock_expiration + ) def get_cachify() -> Cachify: diff --git a/py_cachify/backend/lock.py b/py_cachify/backend/lock.py index 3bb0fb1..61bb15a 100644 --- a/py_cachify/backend/lock.py +++ b/py_cachify/backend/lock.py @@ -1,15 +1,29 @@ import inspect import logging -from contextlib import asynccontextmanager, contextmanager +import time +from asyncio import sleep as asleep from functools import partial, wraps -from typing import Any, AsyncGenerator, Awaitable, Callable, Generator, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, TypeVar, Union, cast -from typing_extensions import ParamSpec, deprecated, overload +from typing_extensions import ParamSpec, Self, deprecated, overload from .exceptions import CachifyLockError -from .helpers import a_reset, get_full_key_from_signature, is_coroutine, reset +from .helpers import a_reset, get_full_key_from_signature, is_alocked, is_coroutine, is_locked, reset from .lib import get_cachify -from .types import AsyncWithResetProtocol, SyncOrAsync, SyncWithResetProtocol +from .types import ( + UNSET, + AsyncLockedProto, + AsyncWithResetProto, + LockProtocolBase, + SyncLockedProto, + SyncOrAsyncReset, + SyncWithResetProto, + UnsetType, +) + + +if TYPE_CHECKING: + from .lib import Cachify logger = logging.getLogger(__name__) @@ -17,54 +31,203 @@ P = ParamSpec('P') -def _check_is_cached(is_already_cached: bool, key: str) -> None: - if not is_already_cached: - return +class AsyncLockMethods(LockProtocolBase): + async def is_alocked(self) -> bool: + return bool(await self._cachify.a_get(key=self._key)) + + async def _a_acquire(self, key: str) -> None: + stop_at = self._calc_stop_at() + + while True: + _is_locked = bool(await self.is_alocked()) + self._raise_if_cached( + is_already_cached=_is_locked, + key=key, + do_raise=self._nowait or time.time() > stop_at, + ) + + if not _is_locked: + await self._cachify.a_set(key=key, val=1, ttl=self._get_ttl()) + return + + await asleep(0.1) + + async def _a_release(self, key: str) -> None: + await self._cachify.a_delete(key=key) + + async def __aenter__(self) -> 'Self': + await self._a_acquire(key=self._key) + return self + + async def __aexit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: + await self._a_release(key=self._key) + + +class SyncLockMethods(LockProtocolBase): + def is_locked(self) -> bool: + return bool(self._cachify.get(key=self._key)) + + def _acquire(self, key: str) -> None: + stop_at = self._calc_stop_at() + + while True: + _is_locked = bool(self.is_locked()) + self._raise_if_cached( + is_already_cached=_is_locked, + key=key, + do_raise=self._nowait or time.time() > stop_at, + ) + + if not _is_locked: + self._cachify.set(key=key, val=1, ttl=self._get_ttl()) + return + + time.sleep(0.1) + + def _release(self, key: str) -> None: + self._cachify.delete(key=key) + + def __enter__(self) -> 'Self': + self._acquire(key=self._key) + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self._release(key=self._key) + + +class lock(AsyncLockMethods, SyncLockMethods): + """ + Class to manage locking mechanism for synchronous and asynchronous functions. + + Args: + key (str): The key used to identify the lock. + nowait (bool, optional): If True, do not wait for the lock to be released. Defaults to True. + timeout (Union[int, float], optional): The time in seconds to wait for the lock if nowait is False. + Defaults to None. + exp (Union[int, None], optional): The expiration time for the lock. + Defaults to UNSET and global value from cachify is used in that case. + + Methods: + __enter__: Acquire a lock for the specified key, synchronous. + is_locked: Check if the lock is currently held, synchronous. + + __aenter__: Async version of __enter__ to acquire a lock for the specified key. + is_alocked: Check if the lock is currently held asynchronously. + + __call__: Decorator to acquire a lock for the wrapped function and handle synchronization + for synchronous and asynchronous functions. + Attaches method `is_locked(*args, **kwargs)` to a wrapped function to quickly check if it's locked. + """ + + def __init__( + self, + key: str, + nowait: bool = True, + timeout: Optional[Union[int, float]] = None, + exp: Union[Optional[int], UnsetType] = UNSET, + ) -> None: + self._key = key + self._nowait = nowait + self._timeout = timeout + self._exp = exp + + @overload + def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncLockedProto[P, R]: ... + + @overload + def __call__(self, _func: Callable[P, R]) -> SyncLockedProto[P, R]: ... + + def __call__( # type: ignore[misc] + self, _func: Union[Callable[P, Awaitable[R]], Callable[P, R]] + ) -> Union[AsyncLockedProto[P, R], SyncLockedProto[P, R]]: + signature = inspect.signature(_func) + + if is_coroutine(_func): + _awaitable_func = _func + + @wraps(_awaitable_func) + async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + + async with lock( + key=_key, + nowait=self._nowait, + timeout=self._timeout, + exp=self._exp, + ): + return await _awaitable_func(*args, **kwargs) + + setattr(_async_wrapper, 'is_locked', partial(is_alocked, signature=signature, key=self._key)) + + return cast(AsyncLockedProto[P, R], _async_wrapper) + + else: + + @wraps(_func) # type: ignore[unreachable] + def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: + bound_args = signature.bind(*args, **kwargs) + _key = get_full_key_from_signature(bound_args=bound_args, key=self._key) + + with lock(key=_key, nowait=self._nowait, timeout=self._timeout, exp=self._exp): + return _func(*args, **kwargs) + + setattr(_sync_wrapper, 'is_locked', partial(is_locked, signature=signature, key=self._key)) + + return cast(SyncLockedProto[P, R], cast(object, _sync_wrapper)) + + @property + def _cachify(self) -> 'Cachify': + return get_cachify() + + def _recreate_cm(self) -> 'Self': + return self - logger.warning(msg := f'{key} is already locked!') - raise CachifyLockError(msg) + def _calc_stop_at(self) -> float: + return time.time() + self._timeout if self._timeout is not None else float('inf') + def _get_ttl(self) -> Optional[int]: + return self._cachify.default_expiration if isinstance(self._exp, UnsetType) else self._exp -@asynccontextmanager -async def async_lock(key: str) -> AsyncGenerator[None, None]: - _cachify = get_cachify() - cached = await _cachify.a_get(key=key) - _check_is_cached(is_already_cached=bool(cached), key=key) + @staticmethod + def _raise_if_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> None: + if not is_already_cached: + return - await _cachify.a_set(key=key, val=1) - try: - yield - finally: - await _cachify.a_delete(key=key) + logger.warning(msg := f'{key} is already locked!') + if do_raise: + raise CachifyLockError(msg) -@contextmanager -def lock(key: str) -> Generator[None, None, None]: - _cachify = get_cachify() - cached = _cachify.get(key=key) - _check_is_cached(is_already_cached=bool(cached), key=key) +def once(key: str, raise_on_locked: bool = False, return_on_locked: Any = None) -> SyncOrAsyncReset: + """ + Decorator that ensures a function is only called once at a time, + based on a specified key (could be a format string). - _cachify.set(key=key, val=1) - try: - yield - finally: - _cachify.delete(key=key) + Args: + key (str): The key used to identify the lock. + raise_on_locked (bool, optional): If True, raise an exception when the function is already locked. + Defaults to False. + return_on_locked (Any, optional): The value to return when the function is already locked. + Defaults to None. + Returns: + SyncOrAsyncReset: Either a synchronous or asynchronous wrapped function with reset method attached to it. + """ -def once(key: str, raise_on_locked: bool = False, return_on_locked: Any = None) -> SyncOrAsync: @overload def _once_inner( # type: ignore[overload-overlap] _func: Callable[P, Awaitable[R]], - ) -> AsyncWithResetProtocol[P, R]: ... + ) -> AsyncWithResetProto[P, R]: ... @overload def _once_inner( _func: Callable[P, R], - ) -> SyncWithResetProtocol[P, R]: ... + ) -> SyncWithResetProto[P, R]: ... def _once_inner( _func: Union[Callable[P, R], Callable[P, Awaitable[R]]], - ) -> Union[SyncWithResetProtocol[P, R], AsyncWithResetProtocol[P, R]]: + ) -> Union[SyncWithResetProto[P, R], AsyncWithResetProto[P, R]]: signature = inspect.signature(_func) if is_coroutine(_func): @@ -76,7 +239,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: _key = get_full_key_from_signature(bound_args=bound_args, key=key) try: - async with async_lock(key=_key): + async with lock(key=_key): return await _awaitable_func(*args, **kwargs) except CachifyLockError: if raise_on_locked: @@ -86,7 +249,7 @@ async def _async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: setattr(_async_wrapper, 'reset', partial(a_reset, signature=signature, key=key)) - return cast(AsyncWithResetProtocol[P, R], _async_wrapper) + return cast(AsyncWithResetProto[P, R], _async_wrapper) else: @@ -106,19 +269,19 @@ def _sync_wrapper(*args: P.args, **kwargs: P.kwargs) -> Any: setattr(_sync_wrapper, 'reset', partial(reset, signature=signature, key=key)) - return cast(SyncWithResetProtocol[P, R], _sync_wrapper) + return cast(SyncWithResetProto[P, R], _sync_wrapper) return _once_inner -@deprecated('sync_once is deprecated, use once instead. Scheduled for removal in 1.3.0') +@deprecated('sync_once is deprecated, use once instead. Scheduled for removal in 3.0.0') def sync_once( key: str, raise_on_locked: bool = False, return_on_locked: Any = None ) -> Callable[[Callable[P, R]], Callable[P, R]]: return once(key=key, raise_on_locked=raise_on_locked, return_on_locked=return_on_locked) -@deprecated('async_once is deprecated, use once instead. Scheduled for removal in 1.3.0') +@deprecated('async_once is deprecated, use once instead. Scheduled for removal in 3.0.0') def async_once( key: str, raise_on_locked: bool = False, return_on_locked: Any = None ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]: diff --git a/py_cachify/backend/types.py b/py_cachify/backend/types.py index 03c4aa3..481fcd0 100644 --- a/py_cachify/backend/types.py +++ b/py_cachify/backend/types.py @@ -1,8 +1,12 @@ -from typing import Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional, Protocol, TypeVar, Union from typing_extensions import ParamSpec, TypeAlias, overload +if TYPE_CHECKING: + from .lib import Cachify + + R = TypeVar('R', covariant=True) P = ParamSpec('P') @@ -32,25 +36,62 @@ def set(self, name: str, value: Any, ex: Union[int, None] = None) -> Any: raise NotImplementedError -class AsyncWithResetProtocol(Protocol[P, R]): +class SyncLockedProto(Protocol[P, R]): + def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover + + def is_locked(self, *args: P.args, **kwargs: P.kwargs) -> bool: ... # pragma: no cover + + +class AsyncLockedProto(Protocol[P, R]): + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover + + def is_locked(self, *args: P.args, **kwargs: P.kwargs) -> bool: ... # pragma: no cover + + +class AsyncWithResetProto(Protocol[P, R]): async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover async def reset(self, *args: P.args, **kwargs: P.kwargs) -> None: ... # pragma: no cover -class SyncWithResetProtocol(Protocol[P, R]): +class SyncWithResetProto(Protocol[P, R]): def __call__(self, *args: P.args, **kwargs: P.kwargs) -> R: ... # pragma: no cover def reset(self, *args: P.args, **kwargs: P.kwargs) -> None: ... # pragma: no cover -class SyncOrAsync(Protocol): +class SyncOrAsyncReset(Protocol): @overload - def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncWithResetProtocol[P, R]: ... # type: ignore[overload-overlap] + def __call__(self, _func: Callable[P, Awaitable[R]]) -> AsyncWithResetProto[P, R]: ... # type: ignore[overload-overlap] @overload - def __call__(self, _func: Callable[P, R]) -> SyncWithResetProtocol[P, R]: ... + def __call__(self, _func: Callable[P, R]) -> SyncWithResetProto[P, R]: ... def __call__( self, _func: Union[Callable[P, Awaitable[R]], Callable[P, R]] - ) -> Union[AsyncWithResetProtocol[P, R], SyncWithResetProtocol[P, R]]: ... + ) -> Union[AsyncWithResetProto[P, R], SyncWithResetProto[P, R]]: ... + + +class UnsetType: + def __bool__(self) -> bool: + return False + + +UNSET = UnsetType() + + +class LockProtocolBase(Protocol): + _key: str + _nowait: bool + _timeout: Optional[Union[int, float]] + _exp: Union[Optional[int], UnsetType] + + @staticmethod + def _raise_if_cached(is_already_cached: bool, key: str, do_raise: bool = True) -> None: ... # pragma: no cover + + @property + def _cachify(self) -> 'Cachify': ... # pragma: no cover + + def _calc_stop_at(self) -> float: ... # pragma: no cover + + def _get_ttl(self) -> Optional[int]: ... # pragma: no cover diff --git a/pyproject.toml b/pyproject.toml index 1cf6968..cc7e949 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py-cachify" -version = "1.2.0" +version = "2.0.0" homepage = "https://github.com/EzyGang/py-cachify" repository = "https://github.com/EzyGang/py-cachify" license = "MIT" @@ -8,7 +8,7 @@ description = "Distributed locks and caching/locking decorators at hand" authors = ["Galtozzy "] readme = "README.md" include = ["README.md"] -packages = [{include = 'py_cachify'}] +packages = [{ include = 'py_cachify' }] [project] requires-python = ">=3.8,<4.0" @@ -43,7 +43,7 @@ redis = "^5.0.7" format-and-lint = "task ruff && task mypy-lint" ruff = "ruff format ./py_cachify/ ./tests/ ./integration_tests/ && ruff check ./py_cachify/ ./tests/ ./integration_tests/ --fix --unsafe-fixes" -tests = "PYTHONPATH=. pytest tests/ -v" +tests = "PYTHONPATH=. pytest tests/ -vvv" integration-tests = "PYTHONPATH=. pytest integration_tests/ --no-cov" ruff-lint = "ruff check ./py_cachify" @@ -67,12 +67,12 @@ select = [ "I", # isort "C", # flake8-comprehensions "B", # flake8-bugbear - "UP", # pyupgrade + "UP", # pyupgrade ] ignore = [ - "B008", # do not perform function calls in argument defaults - "C901", # too complex - "B010", # do not rewrite setattr() + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "B010", # do not rewrite setattr() ] [tool.ruff.format] @@ -88,7 +88,7 @@ max-complexity = 6 [tool.ruff.lint.isort] split-on-trailing-comma = false lines-after-imports = 2 -known-first-party = ["py_cachify",] +known-first-party = ["py_cachify"] [tool.ruff.lint.pyupgrade] keep-runtime-typing = true @@ -121,9 +121,7 @@ follow_imports_for_stubs = true show_error_codes = true plugins = [] -exclude = [ - 'test_', -] +exclude = ['test_'] [[tool.mypy.overrides]] module = "tests.*" @@ -148,9 +146,7 @@ asyncio_mode = "auto" python_files = "test*.py" # Directories that are not visited by pytest collector: norecursedirs = "*.egg .eggs dist build docs .tox .git __pycache__ config docker etc" -testpaths = [ - "tests", -] +testpaths = ["tests"] [tool.coverage.run] # Coverage configuration: @@ -171,6 +167,7 @@ exclude_lines = [ 'raise NotImplementedError', 'if __name__ == .__main__.:', '__all__', + 'if TYPE_CHECKING:', ] [build-system] diff --git a/sonar-project.properties b/sonar-project.properties index d570f19..77aec36 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,7 @@ sonar.organization=ezygang # This is the name and version displayed in the SonarCloud UI. sonar.projectName=py-cachify -sonar.projectVersion=1.2.0 +sonar.projectVersion=2.0.0 # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. diff --git a/tests/conftest.py b/tests/conftest.py index 7efe247..5b79f97 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,4 +8,5 @@ def init_cachify_fixture(): init_cachify() yield + py_cachify.backend.lib._cachify._sync_client._cache = {} py_cachify.backend.lib._cachify = None diff --git a/tests/test_backend.py b/tests/test_backend.py index b0d5171..28a6f6b 100644 --- a/tests/test_backend.py +++ b/tests/test_backend.py @@ -21,7 +21,7 @@ def async_wrapper(memory_cache): @pytest.fixture def cachify(memory_cache, async_wrapper): - return Cachify(sync_client=memory_cache, async_client=async_wrapper, prefix='_PYC_') + return Cachify(sync_client=memory_cache, default_expiration=30, async_client=async_wrapper, prefix='_PYC_') def test_memory_cache_set_and_get(memory_cache): diff --git a/tests/test_cached.py b/tests/test_cached.py index 9e39f33..c44d5da 100644 --- a/tests/test_cached.py +++ b/tests/test_cached.py @@ -7,7 +7,7 @@ from py_cachify import cached from py_cachify.backend.exceptions import CachifyInitError -from py_cachify.backend.types import AsyncWithResetProtocol, P, R, SyncWithResetProtocol +from py_cachify.backend.types import AsyncWithResetProto, P, R, SyncWithResetProto def sync_function(arg1: int, arg2: int) -> int: @@ -75,7 +75,7 @@ def test_sync_cached_preserves_type_annotations(init_cachify_fixture): for name, clz in [('arg1', int), ('arg2', int), ('return', int)]: assert func.__annotations__[name] == clz - assert_type(func, SyncWithResetProtocol[P, R]) + assert_type(func, SyncWithResetProto[P, R]) def test_async_cached_preserves_type_annotations(init_cachify_fixture): @@ -83,7 +83,7 @@ def test_async_cached_preserves_type_annotations(init_cachify_fixture): for name, clz in [('arg1', int), ('arg2', int), ('return', int)]: assert func.__annotations__[name] == clz - assert_type(func, AsyncWithResetProtocol[P, R]) + assert_type(func, AsyncWithResetProto[P, R]) def test_cached_wrapped_async_function_has_reset_callable_attached(init_cachify_fixture): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 915fe15..c8f3910 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -3,7 +3,7 @@ import pytest from pytest_mock import MockerFixture -from py_cachify.backend.helpers import a_reset, get_full_key_from_signature, reset +from py_cachify.backend.helpers import a_reset, get_full_key_from_signature, is_alocked, is_locked, reset def method_with_args_kwargs_args(*args, **kwargs) -> None: @@ -54,3 +54,26 @@ async def test_a_reset_calls_delete_with_key(init_cachify_fixture, args_kwargs_s await a_reset('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) mock.assert_called_once_with(key='key_val1_val2_val3') + + +@pytest.mark.asyncio +@pytest.mark.parametrize('val', [0, 1]) +async def test_is_alocked_accesses_a_get_with_key( + init_cachify_fixture, args_kwargs_signature, mocker: MockerFixture, val +): + mock = mocker.patch('py_cachify.backend.lib.Cachify.a_get', return_value=val) + + res = await is_alocked('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) + + mock.assert_called_once_with(key='key_val1_val2_val3') + assert res is bool(val) + + +@pytest.mark.parametrize('val', [0, 1]) +def test_is_locked_accesses_get_with_key(init_cachify_fixture, args_kwargs_signature, mocker: MockerFixture, val): + mock = mocker.patch('py_cachify.backend.lib.Cachify.get', return_value=val) + + res = is_locked('val1', 'val2', arg3='val3', key='key_{}_{}_{arg3}', signature=args_kwargs_signature) + + mock.assert_called_once_with(key='key_val1_val2_val3') + assert res is bool(val) diff --git a/tests/test_lock_decorator.py b/tests/test_lock_decorator.py new file mode 100644 index 0000000..888aa57 --- /dev/null +++ b/tests/test_lock_decorator.py @@ -0,0 +1,194 @@ +import asyncio +from asyncio import sleep as asleep +from concurrent.futures import ThreadPoolExecutor +from contextlib import nullcontext +from time import sleep + +import pytest + +from py_cachify.backend.exceptions import CachifyLockError +from py_cachify.backend.lib import init_cachify +from py_cachify.backend.lock import lock +from py_cachify.backend.types import UNSET + + +@pytest.mark.parametrize( + 'sleep_time,input1,input2,result1,result2', + [ + (1, 3, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (1, 3, 5, nullcontext(13), nullcontext(15)), + ], +) +def test_lock_decorator_no_wait_sync(init_cachify_fixture, sleep_time, input1, input2, result1, result2): + @lock(key='test_key-{arg}') + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=input1), + e.submit(sync_function, arg=input2), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,input1,input2,result1,result2', + [ + (1, 3, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (1, 3, 5, nullcontext(13), nullcontext(15)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_no_wait_async(init_cachify_fixture, sleep_time, input1, input2, result1, result2): + @lock(key='test_key-{arg}') + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(input1)) + task2 = asyncio.create_task(async_function(input2)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +@pytest.mark.parametrize( + 'sleep_time,timeout,input,result1,result2', + [ + (1, 2, 3, nullcontext(13), nullcontext(13)), + (2, 1, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (0, 1, 3, nullcontext(13), nullcontext(13)), + ], +) +def test_lock_decorator_no_wait_false_sync(init_cachify_fixture, sleep_time, timeout, input, result1, result2): + @lock(key='test_key-{arg}', nowait=False, timeout=timeout) + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=input), + e.submit(sync_function, arg=input), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,timeout,input,result1,result2', + [ + (1, 2, 3, nullcontext(13), nullcontext(13)), + (1, 0.5, 3, nullcontext(13), pytest.raises(CachifyLockError)), + (0, 0.5, 3, nullcontext(13), nullcontext(13)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_no_wait_false_async(init_cachify_fixture, sleep_time, timeout, input, result1, result2): + @lock(key='test_key-{arg}', nowait=False, timeout=timeout) + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(input)) + task2 = asyncio.create_task(async_function(input)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +@pytest.mark.parametrize( + 'sleep_time,timeout,exp,default_exp,result1,result2', + [ + (3, 2, 1, None, nullcontext(15), nullcontext(15)), + (2, 2, UNSET, 1, nullcontext(15), nullcontext(15)), + (2, 1, UNSET, 2, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 4, 1, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 1, 4, nullcontext(15), nullcontext(15)), + ], +) +def test_lock_decorator_expiration_sync(init_cachify_fixture, sleep_time, timeout, exp, default_exp, result1, result2): + init_cachify(default_lock_expiration=default_exp) + + @lock(key='test_key-{arg}', nowait=False, timeout=timeout, exp=exp) + def sync_function(arg: int) -> int: + sleep(sleep_time) + return arg + 10 + + with ThreadPoolExecutor(max_workers=2) as e: + future_1, future_2 = ( + e.submit(sync_function, arg=5), + e.submit(sync_function, arg=5), + ) + + with result1 as r1: + assert r1 == future_1.result() + + with result2 as r2: + assert r2 == future_2.result() + + +@pytest.mark.parametrize( + 'sleep_time,timeout,exp,default_exp,result1,result2', + [ + (3, 2, 1, None, nullcontext(15), nullcontext(15)), + (2, 2, UNSET, 1, nullcontext(15), nullcontext(15)), + (2, 1, UNSET, 2, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 4, 1, nullcontext(15), pytest.raises(CachifyLockError)), + (3, 2, 1, 4, nullcontext(15), nullcontext(15)), + ], +) +@pytest.mark.asyncio +async def test_lock_decorator_expiration_async( + init_cachify_fixture, sleep_time, timeout, exp, default_exp, result1, result2 +): + init_cachify(default_lock_expiration=default_exp) + + @lock(key='test_key-{arg}', nowait=False, timeout=timeout, exp=exp) + async def async_function(arg: int) -> int: + await asleep(sleep_time) + return arg + 10 + + task1 = asyncio.create_task(async_function(5)) + task2 = asyncio.create_task(async_function(5)) + + with result1 as r1: + assert r1 == await task1 + + with result2 as r2: + assert r2 == await task2 + + +def test_wrapped_function_has_is_locked_sync(): + @lock(key='test_key') + def sync_function(): ... + + assert hasattr(sync_function, 'is_locked') + assert not asyncio.iscoroutinefunction(sync_function.is_locked) + assert callable(sync_function.is_locked) + + +def test_wrapped_function_has_is_locked_async(): + @lock(key='test_key') + async def async_function(): ... + + assert hasattr(async_function, 'is_locked') + assert asyncio.iscoroutinefunction(async_function.is_locked) + assert callable(async_function.is_locked) diff --git a/tests/test_locks.py b/tests/test_locks.py index b757c78..f45cae8 100644 --- a/tests/test_locks.py +++ b/tests/test_locks.py @@ -1,13 +1,24 @@ +import asyncio +from asyncio import sleep as asleep +from contextlib import nullcontext +from threading import Thread +from time import sleep + import pytest from py_cachify import CachifyLockError -from py_cachify.backend.lock import async_lock, lock +from py_cachify.backend.lib import Cachify, init_cachify +from py_cachify.backend.lock import lock +from py_cachify.backend.types import UNSET + + +lock_obj = lock(key='test') @pytest.mark.asyncio async def test_async_lock(init_cachify_fixture): async def async_operation(): - async with async_lock('lock'): + async with lock('lock'): return None await async_operation() @@ -18,8 +29,8 @@ async def test_async_lock_already_locked(init_cachify_fixture): key = 'lock' async def async_operation(): - async with async_lock(key): - async with async_lock(key): + async with lock(key): + async with lock(key): pass with pytest.raises(CachifyLockError, match=f'{key} is already locked!'): @@ -44,3 +55,139 @@ def sync_operation(): with pytest.raises(CachifyLockError, match=f'{key} is already locked!'): sync_operation() + + +@pytest.mark.parametrize( + 'exp,timeout,expectation', [(1, 2, nullcontext(None)), (2, 1, pytest.raises(CachifyLockError))] +) +def test_waiting_lock(init_cachify_fixture, exp, timeout, expectation): + key = 'lock' + + def sync_operation(): + with lock(key=key, exp=exp): + with lock(key=key, nowait=False, timeout=timeout): + return None + + with expectation as e: + assert sync_operation() == e + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'exp,timeout,expectation', [(1, 2, nullcontext(None)), (2, 1, pytest.raises(CachifyLockError))] +) +async def test_waiting_lock_async(init_cachify_fixture, exp, timeout, expectation): + key = 'lock' + + async def async_operation(): + async with lock(key=key, exp=exp): + async with lock(key=key, nowait=False, timeout=timeout): + return None + + with expectation as e: + assert await async_operation() == e + + +def test_lock_cachify_returns_cachify_instance(init_cachify_fixture): + assert isinstance(lock_obj._cachify, Cachify) + assert lock_obj._cachify is not None + + +def test_lock_recreate_cm_returns_self(): + assert lock_obj._recreate_cm() is lock_obj + + +@pytest.mark.parametrize('timeout,expected', [(None, float('inf')), (10, 20.0)]) +def test_lock_calc_stop_at(mocker, timeout, expected): + new_lock = lock('test', timeout=timeout) + mocker.patch('time.time', return_value=10.0) + + assert new_lock._calc_stop_at() == expected + + +@pytest.mark.parametrize( + 'default_expiration,exp,expected', + [ + (None, UNSET, 30), + (60, UNSET, 60), + (30, 60, 60), + (30, None, None), + ], +) +def test_lock_get_ttl(init_cachify_fixture, default_expiration, exp, expected): + init_dict = {'default_lock_expiration': default_expiration} if default_expiration is not None else {} + + init_cachify(**init_dict) + + lock_obj = lock('test', exp=exp) + + assert lock_obj._get_ttl() == expected + + +@pytest.mark.parametrize( + 'is_already_locked,key,do_raise,expectation', + [ + (True, 'test', False, nullcontext(None)), + (True, 'test', True, pytest.raises(CachifyLockError)), + (False, 'test', True, nullcontext(None)), + ], +) +def test_lock_raise_if_cached(mocker, is_already_locked, key, do_raise, expectation): + patch_log = mocker.patch('py_cachify.backend.lock.logger.warning') + + with expectation: + lock._raise_if_cached( + is_already_cached=is_already_locked, + key=key, + do_raise=do_raise, + ) + if is_already_locked is True: + patch_log.assert_called_once_with(f'{key} is already locked!') + + +def test_unset_type_bool(): + assert bool(UNSET) is False + + +@pytest.mark.parametrize( + 'sleep_time,expected', + [ + (3, True), + (0, False), + ], +) +def test_is_locked_on_lock_obj(init_cachify_fixture, sleep_time, expected): + test_lock = lock('test') + + def sync_function(): + with test_lock: + sleep(sleep_time) + + thread = Thread(target=sync_function) + thread.start() + sleep(0.3) + + assert test_lock.is_locked() is expected + + +@pytest.mark.parametrize( + 'sleep_time,expected', + [ + (3, True), + (0, False), + ], +) +async def test_is_locked_on_lock_obj_async(init_cachify_fixture, sleep_time, expected): + test_lock = lock('test') + + async def async_function(): + async with test_lock: + await asleep(sleep_time) + + task = asyncio.create_task(async_function()) + + await asleep(0.2) + + assert await test_lock.is_alocked() is expected + + await task diff --git a/tests/test_once_decorator.py b/tests/test_once_decorator.py index c031623..e2d2061 100644 --- a/tests/test_once_decorator.py +++ b/tests/test_once_decorator.py @@ -7,7 +7,7 @@ from py_cachify import CachifyLockError, once from py_cachify.backend.lock import async_once, sync_once -from py_cachify.backend.types import AsyncWithResetProtocol, P, R, SyncWithResetProtocol +from py_cachify.backend.types import AsyncWithResetProto, P, R, SyncWithResetProto def test_once_decorator_sync_function(init_cachify_fixture): @@ -105,8 +105,8 @@ def sync_function(arg1: int, arg2: int) -> int: assert sync_function.__annotations__[name] == clz assert async_function.__annotations__[name] == clz - assert_type(sync_function, SyncWithResetProtocol[P, R]) - assert_type(async_function, AsyncWithResetProtocol[P, R]) + assert_type(sync_function, SyncWithResetProto[P, R]) + assert_type(async_function, AsyncWithResetProto[P, R]) def test_once_wrapped_async_function_has_reset_callable_attached(init_cachify_fixture):

+ + License + + + PyPI version + + + Documentation Status + +