Package: cashews
🥔 CASHEWS 🥔Async cache framework with simple API to build fast and reliable applicationspipinstallcashews
pipinstallcashews[redis]pipinstallcashews[diskcache]pipinstallcashews[dill]# can cache in redis more types of objectspipinstallcashews[speedup]# for bloom filtersWhyCache plays a significant role in modern applications and everybody wants to use all the power of async programming and cache.
There are a few advanced techniques with cache and async programming that can help you build simple, fast,
scalable and reliable applications. This library intends to make it easy to implement such techniques.FeaturesEasy to configure and useDecorator-based API, decorate and playDifferent cache strategies out-of-the-boxSupport for multiple storage backends (In-memory,Redis,DiskCache)Set TTL as a string ("2h5m"), astimedeltaor use a function in case TTL depends on key parametersTransactionalityMiddlewaresClient-side cache (10x faster than simple cache with redis)Bloom filtersDifferent cache invalidation techniques (time-based or tags)Cache any objects securely with pickle (usehash key)2x faster thanaiocache(with client side caching)Usage Examplefromcashewsimportcachecache.setup("mem://")# configure as in-memory cache, but redis/diskcache is also supported# use a decorator-based API@cache(ttl="3h",key="user:{request.user.uid}")asyncdeflong_running_function(request):...# or for fine-grained control, use it directly in a functionasyncdefcache_using_function(request):awaitcache.set(key=request.user.uid,value=request.user,expire="20h")...More exampleshereTable of ContentsConfigurationAvailable BackendsBasic APIDisable CacheStrategiesCache conditionKeys templatingTTLWhat can be cachedCache InvalidationCache invalidation on code changeDetect the source of a resultMiddlewareCallbacksTransactional modeContribFastapiPrometheusConfigurationcashewsprovides a default cache, that you can setup in two different ways:fromcashewsimportcache# via urlcache.setup("redis://0.0.0.0/?db=1&socket_connect_timeout=0.5&suppress=0&hash_key=my_secret&enable=1")# or via kwargscache.setup("redis://0.0.0.0/",db=1,wait_for_connection_timeout=0.5,suppress=False,hash_key=b"my_key",enable=True)Alternatively, you can create a cache instance yourself:fromcashewsimportCachecache=Cache()cache.setup(...)Optionally, you can disable cache withdisable/enableparameter (seeDisable Cache):cache.setup("redis://redis/0?enable=1")cache.setup("mem://?size=500",disable=True)cache.setup("mem://?size=500",enable=False)You can setup different Backends based on a prefix:cache.setup("redis://redis/0")cache.setup("mem://?size=500",prefix="user")awaitcache.get("accounts")# will use the redis backendawaitcache.get("user:1")# will use the memory backendAvailable BackendsIn-memoryThe in-memory cache uses fixed-sized LRU dict to store values. It checks expiration ongetand periodically purge expired keys.cache.setup("mem://")cache.setup("mem://?check_interval=10&size=10000")RedisRequiresredispackage.\This will use Redis as a storage.This backend usespicklemodule to serialize
values, but the cashes can store values with sha1-keyed hash.Usesecretanddigestmodparameters to protect your application from security vulnerabilities.Thedigestmodis a hashing algorithm that can be used:sum,md5(default),sha1andsha256Thesecretis a salt for a hash.Pickle can't serialize any type of object. In case you need to store more complex typesyou can usedill- setpickle_type="dill".
Dill is great, but less performance.
If you need complex serializer forsqlalchemyobjects you can setpickle_type="sqlalchemy"Any connection errors are suppressed, to disable it usesuppress=False- aCacheBackendInteractionErrorwill be raisedIf you would like to useclient-side cachesetclient_side=TrueClient side cache will addcashews:prefix for each key, to customize it useclient_side_prefixoption.cache.setup("redis://0.0.0.0/?db=1&minsize=10&suppress=false&hash_key=my_secret",prefix="func")cache.setup("redis://0.0.0.0/2",password="my_pass",socket_connect_timeout=0.1,retry_on_timeout=True,hash_key="my_secret")cache.setup("redis://0.0.0.0",client_side=True,client_side_prefix="my_prefix:",pickle_type="dill")For using secure connections to redis (over ssl) uri should haveredissas schemacache.setup("rediss://0.0.0.0/",ssl_ca_certs="path/to/ca.crt",ssl_keyfile="path/to/client.key",ssl_certfile="path/to/client.crt",)DiskCacheRequiresdiskcachepackage.This will use local sqlite databases (with shards) as storage.It is a good choice if you don't want to use redis, but you need a shared storage, or your cache takes a lot of local memory.
Also, it is a good choice for client side local storage.You can setup disk cache withFanoutCache parameters** Warning **cache.scanandcache.get_matchdoes not work with this storage (works only if shards are disabled)cache.setup("disk://")cache.setup("disk://?directory=/tmp/cache&timeout=1&shards=0")# disable shardsGb=1073741824cache.setup("disk://",size_limit=3*Gb,shards=12)Basic APIThere are a few basic methods to work with cache:fromcashewsimportcachecache.setup("mem://")# configure as in-memory cacheawaitcache.set(key="key",value=90,expire=60,exist=None)# -> boolawaitcache.set_raw(key="key",value="str")# -> boolawaitcache.get("key",default=None)# -> Anyawaitcache.get_raw("key")awaitcache.get_many("key1","key2",default=None)asyncforkey,valueincache.get_match("pattern:*",batch_size=100):...awaitcache.incr("key")# -> intawaitcache.delete("key")awaitcache.delete_many("key1","key2")awaitcache.delete_match("pattern:*")asyncforkeyincache.scan("pattern:*"):...awaitcache.expire("key",timeout=10)awaitcache.get_expire("key")# -> int seconds to expireawaitcache.ping(message=None)# -> bytesawaitcache.clear()awaitcache.is_locked("key",wait=60)# -> boolasyncwithcache.lock("key",expire=10):...awaitcache.set_lock("key",value="value",expire=60)# -> boolawaitcache.unlock("key","value")# -> boolawaitcache.get_keys_count()# -> int - total number of keys in cacheawaitcache.close()Disable CacheCache can be disabled not only at setup, but also in runtime. Cashews allow you to disable/enable any call of cache or specific commands:fromcashewsimportcache,Commandcache.setup("mem://")# configure as in-memory cachecache.disable(Command.DELETE)cache.disable()cache.enable(Command.GET,Command.SET)cache.enable()withcache.disabling():...StrategiesSimple cacheFail cache (Failover cache)Hit cacheEarlySoftAsync IteratorsLockedRate limitCircuit breakerSimple cacheThis is a typical cache strategy: execute, store and return from cache until it expires.fromdatetimeimporttimedeltafromcashewsimportcachecache.setup("mem://")@cache(ttl=timedelta(hours=3),key="user:{request.user.uid}")asyncdeflong_running_function(request):...Fail cache (Failover cache)Return cache result, if one of the given exceptions is raised (at least one function
call should succeed prior to that).fromcashewsimportcachecache.setup("mem://")# note: the key will be "__module__.get_status:name:{name}"@cache.failover(ttl="2h",exceptions=(ValueError,MyException))asyncdefget_status(name):value=awaitapi_call()return{"status":value}If exceptions didn't get will catch all exceptions or use default if it is set by:cache.set_default_fail_exceptions(ValueError,MyException)Hit cacheExpire cache after given numbers of callcache_hits.fromcashewsimportcachecache.setup("mem://")@cache.hit(ttl="2h",cache_hits=100,update_after=2)asyncdefget(name):value=awaitapi_call()return{"status":value}EarlyCache strategy that tries to solveCache stampede problemwith a hot cache recalculating result in a background.fromcashewsimportcache# or: from cashews import early# if you call this function after 7 min, cache will be updated in a background@cache.early(ttl="10m",early_ttl="7m")asyncdefget(name):value=awaitapi_call()return{"status":value}SoftLike a simple cache, but with a fail protection base on soft ttl.fromcashewsimportcachecache.setup("mem://")# if you call this function after 7 min, cache will be updated and return a new result.# If it fail on recalculation will return current cached value (if it is not more than 10 min old)@cache.soft(ttl="10m",soft_ttl="7m")asyncdefget(name):value=awaitapi_call()return{"status":value}IteratorsAll upper decorators can be used only with coroutines. Cashing async iterators works differently.
To cache async iterators useiteratordecoratorfromcashewsimportcachecache.setup("mem://")@cache.iterator(ttl="10m",key="get:{name}")asyncdefget(name):asyncforiteminget_pages(name):yield...LockedDecorator that can help you to solveCache stampede problem.
Lock the following function calls until the first one is finished.
This guarantees exactly one function call for given ttl.:warning: **Warning: this decorator will not cache the result
To do it you can combine this decorator with any cache decorator or use parameterlock=Truewith@cache()fromcashewsimportcachecache.setup("mem://")@cache.locked(ttl="10s")asyncdefget(name):value=awaitapi_call()return{"status":value}Rate limitRate limit for a function call: if rate limit is reached raise anRateLimitErrorexception.:warning: **Warning: this decorator will not cache the result
To do it you can combine this decorator with any cache failover decorator`fromcashewsimportcache,RateLimitErrorcache.setup("mem://")# no more than 10 calls per minute or ban for 10 minutes - raise RateLimitError@cache.rate_limit(limit=10,period="1m",ttl="10m")asyncdefget(name):value=awaitapi_call()return{"status":value}# no more than 100 calls in 10 minute window. if rate limit will rich -> return from cache@cache.failover(ttl="10m",exceptions=(RateLimitError,))@cache.slice_rate_limit(limit=100,period="10m")asyncdefget_next(name):value=awaitapi_call()return{"status":value}Circuit breakerCircuit breaker pattern. Count the number of failed calls and if the error rate reaches the specified value, it will raiseCircuitBreakerOpenexception:warning: **Warning: this decorator will not cache the result
To do it you can combine this decorator with any cache failover decorator`fromcashewsimportcache,CircuitBreakerOpencache.setup("mem://")@cache.circuit_breaker(errors_rate=10,period="1m",ttl="5m")asyncdefget(name):...@cache.failover(ttl="10m",exceptions=(CircuitBreakerOpen,))@cache.circuit_breaker(errors_rate=10,period="10m",ttl="5m",half_open_ttl="1m")asyncdefget_next(name):...Bloom filter (experimental)Simple Bloom filter:fromcashewsimportcachecache.setup("mem://")@cache.bloom(capacity=10_000,false_positives=1)asyncdefemail_exists(email:str)->bool:...foremailinall_users_emails:awaitemail_exists.set(email)awaitemail_exists("example@example.com")Cache conditionBy default, any successful result of the function call is stored, even if it is aNone.
Caching decorators have the parameter -condition, which can be:a callable object that receives the result of a function call or an exception, args, kwargs and a cache keya string: "not_none" or "skip_none" to do not cacheNonevalues infromcashewsimportcache,NOT_NONEcache.setup("mem://")@cache(ttl="1h",condition=NOT_NONE)asyncdefget():...defskit_test_result(result,args,kwargs,key=None)->bool:returnresultandresult!="test"@cache(ttl="1h",condition=skit_test_result)asyncdefget():...It is also possible to cache an exception that the function can raise, to do so use special conditions (only for simple, hit and early)fromcashewsimportcache,with_exceptions,only_exceptionscache.setup("mem://")@cache(ttl="1h",condition=with_exceptions(MyException,TimeoutError))asyncdefget():...@cache(ttl="1h",condition=only_exceptions(MyException,TimeoutError))asyncdefget():...Also caching decorators have the parametertime_condition- min latency in seconds (can be set likettl)
of getting the result of a function call to be cached.fromcashewsimportcachecache.setup("mem://")@cache(ttl="1h",time_condition="3s")# to cache for 1 hour if execution takes more than 3 secondsasyncdefget():...Template KeysOften, to compose a cache key, you need all the parameters of the function call.
By default, Cashews will generate a key using the function name, module names and parametersfromcashewsimportcachecache.setup("mem://")@cache(ttl=timedelta(hours=3))asyncdefget_name(user,*args,version="v1",**kwargs):...# a key template will be "__module__.get_name:user:{user}:{__args__}:version:{version}:{__kwargs__}"awaitget_name("me",version="v2")# a key will be "__module__.get_name:user:me::version:v2"awaitget_name("me",version="v1",foo="bar")# a key will be "__module__.get_name:user:me::version:v1:foo:bar"awaitget_name("me","opt","attr",opt="opt",attr="attr")# a key will be "__module__.get_name:user:me:opt:attr:version:v1:attr:attr:opt:opt"For more advanced usage it better to define a cache key manually:fromcashewsimportcachecache.setup("mem://")@cache(ttl="2h",key="user_info:{user_id}")asyncdefget_info(user_id:str):...You may use objects in a key and access to an attribute through a template:@cache(ttl="2h",key="user_info:{user.uuid}")asyncdefget_info(user:User):...You may use built-in functions to format template values (lower,upper,len,jwt,hash)@cache(ttl="2h",key="user_info:{user.name:lower}:{password:hash(sha1)}")asyncdefget_info(user:User,password:str):...@cache(ttl="2h",key="user:{token:jwt(client_id)}")asyncdefget_user_by_token(token:str)->User:...Or define your own transformation functions:fromcashewsimportdefault_formatter,cachecache.setup("mem://")@default_formatter.register("prefix")def_prefix(value,chars=3):returnvalue[:chars].upper()@cache(ttl="2h",key="servers-user:{user.index:prefix(4)}")# a key will be "servers-user:DWQS"asyncdefget_user_servers(user):...or register type formatters:fromdecimalimportDecimalfromcashewsimportdefault_formatter,cache@default_formatter.type_format(Decimal)def_decimal(value:Decimal)->str:returnstr(value.quantize(Decimal("0.00")))@cache(ttl="2h",key="price-{item.price}:{item.currency:upper}")# a key will be "price-10.00:USD"asyncdefconvert_price(item):...Not only function arguments can participate in a key formation. Cashews have atemplate_context. You may use any variable registered in it:fromcashewsimportcache,key_context,register_key_contextcache.setup("mem://")register_key_context("client_id")@cache(ttl="2h",key="user:{client_id}")asyncdefget_current_user():pass...withkey_context(client_id=135356):awaitget_current_user()Template for a class methodfromcashewsimportcachecache.setup("mem://")classMyClass:@cache(ttl="2h")asyncdefget_name(self,user,version="v1"):...# a key template will be "__module__:MyClass.get_name:self:{self}:user:{user}:version:{version}awaitMyClass().get_name("me",version="v2")# a key will be "__module__:MyClass.get_name:self:<__module__.MyClass object at 0x105edd6a0>:user:me:version:v1"As you can see, there is an ugly reference to the instance in the key. That is not what we expect to see.
That cache will not work properly. There are 3 solutions to avoid it:define__str__magic method in our classclassMyClass:@cache(ttl="2h")asyncdefget_name(self,user,version="v1"):...def__str__(self)->str:returnself._hostawaitMyClass(host="http://example.com").get_name("me",version="v2")# a key will be "__module__:MyClass.get_name:self:http://example.com:user:me:version:v1"Set a key templateclassMyClass:@cache(ttl="2h",key="{self._host}:name:{user}:{version}")asyncdefget_name(self,user,version="v1"):...awaitMyClass(host="http://example.com").get_name("me",version="v2")# a key will be "http://example.com:name:me:v1"Usenoselfornoself_cacheif you want to excludeselffrom a keyfromcashewsimportcache,noself,noself_cachecache.setup("mem://")classMyClass:@noself(cache)(ttl="2h")asyncdefget_name(self,user,version="v1"):...@noself_cache(ttl="2h")# for python <= 3.8asyncdefget_name(self,user,version="v1"):...# a key template will be "__module__:MyClass.get_name:user:{user}:version:{version}awaitMyClass().get_name("me",version="v2")# a key will be "__module__:MyClass.get_name:user:me:version:v1"TTLCache time to live (ttl) is a required parameter for all cache decorators. TTL can be:an integer as the number of secondsatimedeltaa string like in golang e.g1d2h3m50sa callable object like a function that receivesargsandkwargsof the decorated function and returns one of the previous format for TTLExamples:fromcashewsimportcachefromdatetimeimporttimedeltacache.setup("mem://")@cache(ttl=60*10)asyncdefget(item_id:int)->Item:pass@cache(ttl=timedelta(minutes=10))asyncdefget(item_id:int)->Item:pass@cache(ttl="10m")asyncdefget(item_id:int)->Item:passdef_ttl(item_id:int)->str:return"2h"ifitem_id>10else"1h"@cache(ttl=_ttl)asyncdefget(item_id:int)->Item:passWhat can be cachedCashews mostly use built-in pickle to store data but also support other pickle-like serialization like dill.
Some types of objects are not picklable, in this case, cashews has API to define custom encoding/decoding:fromcashews.serializeimportregister_typeasyncdefmy_encoder(value:CustomType,*args,**kwargs)->bytes:...asyncdefmy_decoder(value:bytes,*args,**kwargs)->CustomType:...register_type(CustomType,my_encoder,my_decoder)Cache invalidationCache invalidation - one of the main Computer Science well-known problems.Sometimes, you want to invalidate the cache after some action is triggered.
Consider this example:fromcashewsimportcachecache.setup("mem://")@cache(ttl="1h",key="items:page:{page}")asyncdefitems(page=1):...@cache.invalidate("items:page:*")asyncdefcreate_item(item):...Here, the cache foritemswill be invalidated every timecreate_itemis called
There are two problems:with redis backend you cashews will scan a full database to get a key that match a pattern (items:page:*) - not good for performance reasonswhat if we do not specify a key for cache:@cache(ttl="1h")asyncdefitems(page=1):...Cashews provide the tag system: you can tag cache keys, so they will be stored in a separateSETto avoid high load on redis storage. To use the tags in a more efficient way please use it with the client side feature.fromcashewsimportcachecache.setup("redis://",client_side=True)@cache(ttl="1h",tags=["items","page:{page}"])asyncdefitems(page=1):...awaitcache.delete_tags("page:1")awaitcache.delete_tags("items")# low level apicache.register_tag("my_tag",key_template="key{i}")awaitcache.set("key1","value",expire="1d",tags=["my_tag"])You can invalidate future call of cache request by context manager:fromcashewsimportcache,invalidate_further@cache(ttl="3h")asyncdefitems():...asyncdefadd_item(item:Item)->List[Item]:...withinvalidate_further():awaititemsCache invalidation on code changeOften, you may face a problem with an invalid cache after the code is changed. For example:@cache(ttl=timedelta(days=1),key="user:{user_id}")asyncdefget_user(user_id):return{"name":"Dmitry","surname":"Krykov"}Then, the returned value was changed to:-return{"name":"Dmitry","surname":"Krykov"}+return{"full_name":"Dmitry Krykov"}Since the function returns a dict, there is no simple way to automatically detect
that kind of cache invalidityOne way to solve the problem is to add a prefix for this cache:@cache(ttl=timedelta(days=1),prefix="v2")asyncdefget_user(user_id):return{"full_name":"Dmitry Krykov"}but it is so easy to forget to do it...The best defense against this problem is to use your own datacontainers, likedataclasses,
with defined__repr__method.
This will add distinctness andcashewscan detect changes in such structures automatically
by checkingobject representation.fromdataclassesimportdataclassfromcashewsimportcachecache.setup("mem://")@dataclassclassUser:name:strsurname:str# or define your own class with __repr__ methodclassUser:def__init__(self,name,surname):self.name,self.surname=name,surnamedef__repr__(self):returnf"{self.name}{self.surname}"# Will detect changes of a structure@cache(ttl="1d",prefix="v2")asyncdefget_user(user_id):returnUser("Dima","Krykov")Detect the source of a resultDecorators give us a very simple API but also make it difficult to understand where
the result is coming from - cache or direct call.To solve this problemcashewshasdetectcontext manager:fromcashewsimportcachewithcache.detectasdetector:response=awaitsomething_that_use_cache()calls=detector.callsprint(calls)# >>> {"my:key": [{"ttl": 10, "name": "simple", "backend": "redis"}, ], "fail:key": [{"ttl": 10, "exc": RateLimit}, "name": "fail", "backend": "mem"],}E.g. A simple middleware to use it in a web app:@app.middleware("http")asyncdefadd_from_cache_headers(request:Request,call_next):withcache.detectasdetector:response=awaitcall_next(request)ifdetector.keys:key=list(detector.keys.keys())[0]response.headers["X-From-Cache"]=keyexpire=awaitcache.get_expire(key)response.headers["X-From-Cache-Expire-In-Seconds"]=str(expire)returnresponseMiddlewareCashews provide the interface for a "middleware" pattern:importloggingfromcashewsimportcachelogger=logging.getLogger(__name__)asyncdeflogging_middleware(call,cmd:Command,backend:Backend,*args,**kwargs):key=args[0]ifargselsekwargs.get("key",kwargs.get("pattern",""))logger.info("=> Cache request:%s",cmd.value,extra={"args":args,"cache_key":key})returnawaitcall(*args,**kwargs)cache.setup("mem://",middlewares=(logging_middleware,))CallbacksOne of the middleware that is preinstalled in cache instance isCallbackMiddleware.
This middleware also add to a cache a new interface that allow to add a function that will be called before given command will be triggeredfromcashewsimportcache,Commanddefcallback(key,result):print(f"GET key={key}")withcache.callback(callback,cmd=Command.GET):awaitcache.get("test")# also will print "GET key=test"TransactionalApplications are more often based on a database with transaction (OLTP) usage. Usually cache supports transactions poorly.
Here is just a simple example of how we can make our cache inconsistent:asyncdefmy_handler():asyncwithdb.transaction():awaitdb.insert(user)awaitcache.set(f"key:{user.id}",user)awaitapi.service.register(user)Here the API call may fail, the database transaction will rollback, but the cache will not.
Of course, in this code, we can solve it by moving the cache call outside transaction, but in real code it may not so easy.
Another case: we want to make bulk operations with a group of keys to keep it consistent:asyncdeflogin(user,token,session):...old_session=awaitcache.get(f"current_session:{user.id}")awaitcache.incr(f"sessions_count:{user.id}")awaitcache.set(f"current_session:{user.id}",session)awaitcache.set(f"token:{token.id}",user)returnold_sessionHere we want to have some way to protect our code from race conditions and do operations with cache simultaneously.Cashews support transaction operations::warning: **Warning: transaction operations areset,set_many,delete,delete_many,delete_matchandincrfromcashewsimportcache...@cache.transaction()asyncdefmy_handler():asyncwithdb.transaction():awaitdb.insert(user)awaitcache.set(f"key:{user.id}",user)awaitapi.service.register(user)# orasyncdeflogin(user,token,session):asyncwithcache.transaction()astx:old_session=awaitcache.get(f"current_session:{user.id}")awaitcache.incr(f"sessions_count:{user.id}")awaitcache.set(f"current_session:{user.id}",session)awaitcache.set(f"token:{token.id}",user)if...:tx.rollback()returnold_sessionTransactions in cashews support different modes of "isolation"fast (0-7% overhead) - memory based, can't protect of race conditions, but may use for atomicitylocked (default - 4-9% overhead) - use kind of shared lock per cache key (in case of redis or disk backend), protect of race conditionsserializable (7-50% overhead) - use global shared lock - one transaction per time (almost useless)fromcashewsimportcache,TransactionMode...@cache.transaction(TransactionMode.SERIALIZABLE,timeout=1)asyncdefmy_handler():...ContribThis library is framework agnostic, but includes several "batteries" for most popular tools.FastapiYou may find a few middlewares useful that can help you to control a cache in you web application based on fastapi.CacheEtagMiddleware- middleware add Etag and check 'If-None-Match' header based on EtagCacheRequestControlMiddleware- middleware check and addCache-ControlheaderCacheDeleteMiddleware- clear cache for an endpoint based onClear-Site-DataheaderExample:fromfastapiimportFastAPI,Header,Queryfromfastapi.responsesimportStreamingResponsefromcashewsimportcachefromcashews.contrib.fastapiimport(CacheDeleteMiddleware,CacheEtagMiddleware,CacheRequestControlMiddleware,cache_control_ttl,)app=FastAPI()app.add_middleware(CacheDeleteMiddleware)app.add_middleware(CacheEtagMiddleware)app.add_middleware(CacheRequestControlMiddleware)metrics_middleware=create_metrics_middleware()cache.setup(os.environ.get("CACHE_URI","redis://"))@app.get("/")@cache.failover(ttl="1h")@cache(ttl=cache_control_ttl(default="4m"),key="simple:{user_agent:hash}",time_condition="1s")asyncdefsimple(user_agent:str=Header("No")):...@app.get("/stream")@cache(ttl="1m",key="stream:{file_path}")asyncdefstream(file_path:str=Query(__file__)):returnStreamingResponse(_read_file(file_path=file_path))asyncdef_read_file(_read_file):...Also cashews can cache stream responsesPrometheusYou can easily provide metrics using the Prometheus middleware.fromcashewsimportcachefromcashews.contrib.prometheusimportcreate_metrics_middlewaremetrics_middleware=create_metrics_middleware(with_tag=False)cache.setup("redis://",middlewares=(metrics_middleware,))DevelopmentSetupClone the project.After creating a virtual environment, installpre-commit:pipinstallpre-commit&&pre-commitinstall--install-hooksTestsTo run tests you can usetox:pipinstalltox
tox-epy//testsforinmemorybackend
tox-epy-diskcache//testsfordiskcachebackend
tox-epy-redis//testsforredisbackend-youneedtorunredis
tox-epy-integration//testsforintegrationswithaiohttpandfastapi

tox//torunalltestsforallpythonthatisinstalledonyourmachineOr usepytest, but 2 tests always fail, it is OK:pipinstall.[tests,redis,diskcache,speedup]fastapiaiohttprequests

pytest//runalltestswithallbackends
pytest-m"not redis"//alltestswithouttestsforredisbackend