{"id":13815061,"url":"https://github.com/Krukov/cashews","last_synced_at":"2025-05-15T06:34:06.143Z","repository":{"id":36497941,"uuid":"227915294","full_name":"Krukov/cashews","owner":"Krukov","description":"Cache with async power","archived":false,"fork":false,"pushed_at":"2025-03-17T19:48:50.000Z","size":972,"stargazers_count":476,"open_issues_count":21,"forks_count":29,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-05-14T15:14:05.382Z","etag":null,"topics":["aiohttp","asycnio","asynchronous","asyncio","cache","cache-control","cache-stampede","caching","dog-piling","fastapi","python","redis"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Krukov.png","metadata":{"files":{"readme":"Readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-12-13T20:17:25.000Z","updated_at":"2025-05-12T15:13:57.000Z","dependencies_parsed_at":"2024-01-07T23:29:10.951Z","dependency_job_id":"99c0633d-1b3f-459d-8b97-4379abdc7b43","html_url":"https://github.com/Krukov/cashews","commit_stats":{"total_commits":415,"total_committers":18,"mean_commits":"23.055555555555557","dds":0.7108433734939759,"last_synced_commit":"3f2bbcbe88e54fe2a6806ae9908a61a3cc515298"},"previous_names":[],"tags_count":42,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Krukov%2Fcashews","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Krukov%2Fcashews/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Krukov%2Fcashews/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Krukov%2Fcashews/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Krukov","download_url":"https://codeload.github.com/Krukov/cashews/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254289115,"owners_count":22046033,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aiohttp","asycnio","asynchronous","asyncio","cache","cache-control","cache-stampede","caching","dog-piling","fastapi","python","redis"],"created_at":"2024-08-04T04:02:54.100Z","updated_at":"2025-05-15T06:34:06.101Z","avatar_url":"https://github.com/Krukov.png","language":"Python","readme":"\u003ch1 align=\"center\"\u003e🥔 CASHEWS 🥔\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cem\u003eAsync cache framework with simple API to build fast and reliable applications\u003c/em\u003e\n\u003c/p\u003e\n\n```bash\npip install cashews\npip install cashews[redis]\npip install cashews[diskcache]\npip install cashews[dill] # can cache in redis more types of objects\npip install cashews[speedup] # for bloom filters\n```\n\n---\n\n## Why\n\nCache plays a significant role in modern applications and everybody wants to use all the power of async programming and cache.\nThere are a few advanced techniques with cache and async programming that can help you build simple, fast,\nscalable and reliable applications. This library intends to make it easy to implement such techniques.\n\n## Features\n\n- Easy to configure and use\n- Decorator-based API, decorate and play\n- Different cache strategies out-of-the-box\n- Support for multiple storage backends ([In-memory](#in-memory), [Redis](#redis), [DiskCache](diskcache))\n- Set TTL as a string (\"2h5m\"), as `timedelta` or use a function in case TTL depends on key parameters\n- Transactionality\n- Middlewares\n- Client-side cache (10x faster than simple cache with redis)\n- Bloom filters\n- Different cache invalidation techniques (time-based or tags)\n- Cache any objects securely with pickle (use [secret](#redis))\n- 2x faster than `aiocache` (with client side caching)\n\n## Usage Example\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")  # configure as in-memory cache, but redis/diskcache is also supported\n\n# use a decorator-based API\n@cache(ttl=\"3h\", key=\"user:{request.user.uid}\")\nasync def long_running_function(request):\n    ...\n\n# or for fine-grained control, use it directly in a function\nasync def cache_using_function(request):\n    await cache.set(key=request.user.uid, value=request.user, expire=\"20h\")\n    ...\n```\n\nMore examples [here](https://github.com/Krukov/cashews/tree/master/examples)\n\n## Table of Contents\n\n- [Configuration](#configuration)\n- [Available Backends](#available-backends)\n- [Basic API](#basic-api)\n- [Disable Cache](#disable-cache)\n- [Strategies](#strategies)\n  - [Cache condition](#cache-condition)\n  - [Keys templating](#template-keys)\n  - [TTL](#ttl)\n  - [What can be cached](#what-can-be-cached)\n- [Cache Invalidation](#cache-invalidation)\n  - [Cache invalidation on code change](#cache-invalidation-on-code-change)\n- [Detect the source of a result](#detect-the-source-of-a-result)\n- [Middleware](#middleware)\n- [Callbacks](#callbacks)\n- [Transactional mode](#transactional)\n- [Contrib](#contrib)\n  - [Fastapi](#fastapi)\n  - [Prometheus](#prometheus)\n\n### Configuration\n\n`cashews` provides a default cache, that you can setup in two different ways:\n\n```python\nfrom cashews import cache\n\n# via url\ncache.setup(\"redis://0.0.0.0/?db=1\u0026socket_connect_timeout=0.5\u0026suppress=0\u0026secret=my_secret\u0026enable=1\")\n# or via kwargs\ncache.setup(\"redis://0.0.0.0/\", db=1, wait_for_connection_timeout=0.5, suppress=False, secret=b\"my_key\", enable=True)\n```\n\nAlternatively, you can create a cache instance yourself:\n\n```python\nfrom cashews import Cache\n\ncache = Cache()\ncache.setup(...)\n```\n\nOptionally, you can disable cache with `disable`/`enable` parameter (see [Disable Cache](#disable-cache)):\n\n```python\ncache.setup(\"redis://redis/0?enable=1\")\ncache.setup(\"mem://?size=500\", disable=True)\ncache.setup(\"mem://?size=500\", enable=False)\n```\n\nYou can setup different Backends based on a prefix:\n\n```python\ncache.setup(\"redis://redis/0\")\ncache.setup(\"mem://?size=500\", prefix=\"user\")\n\nawait cache.get(\"accounts\")  # will use the redis backend\nawait cache.get(\"user:1\")  # will use the memory backend\n```\n\n### Available Backends\n\n#### In-memory\n\nThe in-memory cache uses fixed-sized LRU dict to store values. It checks expiration on `get`\nand periodically purge expired keys.\n\n```python\ncache.setup(\"mem://\")\ncache.setup(\"mem://?check_interval=10\u0026size=10000\")\n```\n\n#### Redis\n\n_Requires [redis](https://github.com/redis/redis-py) package._\\\n\nThis will use Redis as a storage.\n\nThis backend uses [pickle](https://docs.python.org/3/library/pickle.html) module to serialize\nvalues, but the cashes can store values with sha1-keyed hash.\n\nUse `secret` and `digestmod` parameters to protect your application from security vulnerabilities.\n\nThe `digestmod` is a hashing algorithm that can be used: `sum`, `md5` (default), `sha1` and `sha256`\n\nThe `secret` is a salt for a hash.\n\nPickle can't serialize any type of object. In case you need to store more complex types\n\nyou can use [dill](https://github.com/uqfoundation/dill) - set `pickle_type=\"dill\"`.\nDill is great, but less performance.\nIf you need complex serializer for [sqlalchemy](https://docs.sqlalchemy.org/en/14/core/serializer.html) objects you can set `pickle_type=\"sqlalchemy\"`\nUse `json` also an option to serialize/deserialize an object, but it very limited (`pickle_type=\"json\"`)\n\nAny connection errors are suppressed, to disable it use `suppress=False` - a `CacheBackendInteractionError` will be raised\n\nIf you would like to use [client-side cache](https://redis.io/topics/client-side-caching) set `client_side=True`\n\nClient side cache will add `cashews:` prefix for each key, to customize it use `client_side_prefix` option.\n\n```python\ncache.setup(\"redis://0.0.0.0/?db=1\u0026minsize=10\u0026suppress=false\u0026secret=my_secret\", prefix=\"func\")\ncache.setup(\"redis://0.0.0.0/2\", password=\"my_pass\", socket_connect_timeout=0.1, retry_on_timeout=True, secret=\"my_secret\")\ncache.setup(\"redis://0.0.0.0\", client_side=True, client_side_prefix=\"my_prefix:\", pickle_type=\"dill\")\n```\n\nFor using secure connections to redis (over ssl) uri should have `rediss` as schema\n\n```python\ncache.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\",)\n```\n\n#### DiskCache\n\n_Requires [diskcache](https://github.com/grantjenks/python-diskcache) package._\n\nThis will use local sqlite databases (with shards) as storage.\n\nIt 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.\nAlso, it is a good choice for client side local storage.\n\nYou can setup disk cache with [FanoutCache parameters](http://www.grantjenks.com/docs/diskcache/api.html#fanoutcache)\n\n** Warning ** `cache.scan` and `cache.get_match` does not work with this storage (works only if shards are disabled)\n\n```python\ncache.setup(\"disk://\")\ncache.setup(\"disk://?directory=/tmp/cache\u0026timeout=1\u0026shards=0\")  # disable shards\nGb = 1073741824\ncache.setup(\"disk://\", size_limit=3 * Gb, shards=12)\n```\n\n### Basic API\n\nThere are a few basic methods to work with cache:\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")  # configure as in-memory cache\n\nawait cache.set(key=\"key\", value=90, expire=\"2h\", exist=None)  # -\u003e bool\nawait cache.set_raw(key=\"key\", value=\"str\")  # -\u003e bool\nawait cache.set_many({\"key1\": value, \"key2\": value})  # -\u003e None\n\nawait cache.get(\"key\", default=None)  # -\u003e Any\nawait cache.get_or_set(\"key\", default=awaitable_or_callable, expire=\"1h\")  # -\u003e Any\nawait cache.get_raw(\"key\") # -\u003e Any\nawait cache.get_many(\"key1\", \"key2\", default=None)  # -\u003e tuple[Any]\nasync for key, value in cache.get_match(\"pattern:*\", batch_size=100):\n    ...\n\nawait cache.incr(\"key\") # -\u003e int\nawait cache.exists(\"key\") # -\u003e bool\n\nawait cache.delete(\"key\")\nawait cache.delete_many(\"key1\", \"key2\")\nawait cache.delete_match(\"pattern:*\")\n\nasync for key in cache.scan(\"pattern:*\"):\n    ...\n\nawait cache.expire(\"key\", timeout=10)\nawait cache.get_expire(\"key\")  # -\u003e int seconds to expire\n\nawait cache.ping(message=None)  # -\u003e bytes\nawait cache.clear()\n\nawait cache.is_locked(\"key\", wait=60)  # -\u003e bool\nasync with cache.lock(\"key\", expire=10):\n    ...\nawait cache.set_lock(\"key\", value=\"value\", expire=60)  # -\u003e bool\nawait cache.unlock(\"key\", \"value\")  # -\u003e bool\n\nawait cache.get_keys_count()  # -\u003e int - total number of keys in cache\nawait cache.close()\n```\n\n### Disable Cache\n\nCache can be disabled not only at setup, but also in runtime. Cashews allow you to disable/enable any call of cache or specific commands:\n\n```python\nfrom cashews import cache, Command\n\ncache.setup(\"mem://\")  # configure as in-memory cache\n\ncache.disable(Command.DELETE)\ncache.disable()\ncache.enable(Command.GET, Command.SET)\ncache.enable()\n\nwith cache.disabling():\n  ...\n```\n\n### Strategies\n\n- [Simple cache](#simple-cache)\n- [Fail cache (Failover cache)](#fail-cache-failover-cache)\n- [Hit cache](#hit-cache)\n- [Early](#early)\n- [Soft](#soft)\n- [Async Iterators](#iterators)\n- [Locked](#locked)\n- [Rate limit](#rate-limit)\n- [Circuit breaker](#circuit-breaker)\n\n#### Simple cache\n\nThis is a typical cache strategy: execute, store and return from cache until it expires.\n\n```python\nfrom datetime import timedelta\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache(ttl=timedelta(hours=3), key=\"user:{request.user.uid}\")\nasync def long_running_function(request):\n    ...\n```\n\n#### Fail cache (Failover cache)\n\nReturn cache result, if one of the given exceptions is raised (at least one function\ncall should succeed prior to that).\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n# note: the key will be \"__module__.get_status:name:{name}\"\n@cache.failover(ttl=\"2h\", exceptions=(ValueError, MyException))\nasync def get_status(name):\n    value = await api_call()\n    return {\"status\": value}\n```\n\nIf exceptions didn't get will catch all exceptions or use default if it is set by:\n\n```python\ncache.set_default_fail_exceptions(ValueError, MyException)\n```\n\n#### Hit cache\n\nExpire cache after given numbers of call `cache_hits`.\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache.hit(ttl=\"2h\", cache_hits=100, update_after=2)\nasync def get(name):\n    value = await api_call()\n    return {\"status\": value}\n```\n\n#### Early\n\nCache strategy that tries to solve [Cache stampede problem](https://en.wikipedia.org/wiki/Cache_stampede)\nwith a hot cache recalculating result in a background.\n\n```python\nfrom cashews import cache  # or: from cashews import early\n\n# if you call this function after 7 min, cache will be updated in a background\n@cache.early(ttl=\"10m\", early_ttl=\"7m\")\nasync def get(name):\n    value = await api_call()\n    return {\"status\": value}\n```\n\n#### Soft\n\nLike a simple cache, but with a fail protection base on soft ttl.\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n# if you call this function after 7 min, cache will be updated and return a new result.\n# If it fail on recalculation will return current cached value (if it is not more than 10 min old)\n@cache.soft(ttl=\"10m\", soft_ttl=\"7m\")\nasync def get(name):\n    value = await api_call()\n    return {\"status\": value}\n```\n\n#### Iterators\n\nAll upper decorators can be used only with coroutines. Cashing async iterators works differently.\nTo cache async iterators use `iterator` decorator\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n\n@cache.iterator(ttl=\"10m\", key=\"get:{name}\")\nasync def get(name):\n    async for item in get_pages(name):\n        yield ...\n\n```\n\n#### Locked\n\nDecorator that can help you to solve [Cache stampede problem](https://en.wikipedia.org/wiki/Cache_stampede).\nLock the following function calls until the first one is finished.\nThis guarantees exactly one function call for given ttl.\n\n\u003e :warning: \\*\\*Warning: this decorator will not cache the result\n\u003e To do it you can combine this decorator with any cache decorator or use parameter `lock=True` with `@cache()`\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache.locked(ttl=\"10s\")\nasync def get(name):\n    value = await api_call()\n    return {\"status\": value}\n```\n\n#### Rate limit\n\nRate limit for a function call: if rate limit is reached raise an `RateLimitError` exception.\n\n\u003e :warning: \\*\\*Warning: this decorator will not cache the result\n\u003e To do it you can combine this decorator with any cache failover decorator`\n\n```python\nfrom cashews import cache, RateLimitError\n\ncache.setup(\"mem://\")\n\n# no more than 10 calls per minute or ban for 10 minutes - raise RateLimitError\n@cache.rate_limit(limit=10, period=\"1m\", ttl=\"10m\")\nasync def get(name):\n    value = await api_call()\n    return {\"status\": value}\n\n\n\n# no more than 100 calls in 10 minute window. if rate limit will rich -\u003e return from cache\n@cache.failover(ttl=\"10m\", exceptions=(RateLimitError, ))\n@cache.slice_rate_limit(limit=100, period=\"10m\")\nasync def get_next(name):\n    value = await api_call()\n    return {\"status\": value}\n\n```\n\n#### Circuit breaker\n\nCircuit breaker pattern. Count the number of failed calls and if the error rate reaches the specified value, it will raise `CircuitBreakerOpen` exception\n\n\u003e :warning: \\*\\*Warning: this decorator will not cache the result\n\u003e To do it you can combine this decorator with any cache failover decorator`\n\n```python\nfrom cashews import cache, CircuitBreakerOpen\n\ncache.setup(\"mem://\")\n\n@cache.circuit_breaker(errors_rate=10, period=\"1m\", ttl=\"5m\")\nasync def get(name):\n    ...\n\n\n@cache.failover(ttl=\"10m\", exceptions=(CircuitBreakerOpen, ))\n@cache.circuit_breaker(errors_rate=10, period=\"10m\", ttl=\"5m\", half_open_ttl=\"1m\")\nasync def get_next(name):\n    ...\n\n```\n\n#### Bloom filter (experimental)\n\nSimple Bloom filter:\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache.bloom(capacity=10_000, false_positives=1)\nasync def email_exists(email: str) -\u003e bool:\n    ...\n\nfor email in all_users_emails:\n    await email_exists.set(email)\n\nawait email_exists(\"example@example.com\")\n```\n\n### Cache condition\n\nBy default, any successful result of the function call is stored, even if it is a `None`.\nCaching decorators have the parameter - `condition`, which can be:\n\n- a callable object that receives the result of a function call or an exception, args, kwargs and a cache key\n- a string: \"not_none\" or \"skip_none\" to do not cache `None` values in\n\n```python\nfrom cashews import cache, NOT_NONE\n\ncache.setup(\"mem://\")\n\n@cache(ttl=\"1h\", condition=NOT_NONE)\nasync def get():\n    ...\n\n\ndef skit_test_result(result, args, kwargs, key=None) -\u003e bool:\n    return result and result != \"test\"\n\n@cache(ttl=\"1h\", condition=skit_test_result)\nasync def get():\n    ...\n\n```\n\nIt is also possible to cache an exception that the function can raise, to do so use special conditions (only for simple, hit and early)\n\n```python\nfrom cashews import cache, with_exceptions, only_exceptions\n\ncache.setup(\"mem://\")\n\n@cache(ttl=\"1h\", condition=with_exceptions(MyException, TimeoutError))\nasync def get():\n    ...\n\n\n@cache(ttl=\"1h\", condition=only_exceptions(MyException, TimeoutError))\nasync def get():\n    ...\n\n```\n\nAlso caching decorators have the parameter `time_condition` - min latency in seconds (can be set like `ttl`)\nof getting the result of a function call to be cached.\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache(ttl=\"1h\", time_condition=\"3s\")  # to cache for 1 hour if execution takes more than 3 seconds\nasync def get():\n    ...\n```\n\n### Template Keys\n\nOften, to compose a cache key, you need all the parameters of the function call.\nBy default, Cashews will generate a key using the function name, module names and parameters\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache(ttl=timedelta(hours=3))\nasync def get_name(user, *args, version=\"v1\", **kwargs):\n    ...\n\n# a key template will be \"__module__.get_name:user:{user}:{__args__}:version:{version}:{__kwargs__}\"\n\nawait get_name(\"me\", version=\"v2\")\n# a key will be \"__module__.get_name:user:me::version:v2\"\nawait get_name(\"me\", version=\"v1\", foo=\"bar\")\n# a key will be \"__module__.get_name:user:me::version:v1:foo:bar\"\nawait get_name(\"me\", \"opt\", \"attr\", opt=\"opt\", attr=\"attr\")\n# a key will be \"__module__.get_name:user:me:opt:attr:version:v1:attr:attr:opt:opt\"\n```\n\nFor more advanced usage it better to define a cache key manually:\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache(ttl=\"2h\", key=\"user_info:{user_id}\")\nasync def get_info(user_id: str):\n    ...\n\n```\n\nYou may use objects in a key and access to an attribute through a template:\n\n```python\n\n@cache(ttl=\"2h\", key=\"user_info:{user.uuid}\")\nasync def get_info(user: User):\n    ...\n\n```\n\nYou may use built-in functions to format template values (`lower`, `upper`, `len`, `jwt`, `hash`)\n\n```python\n\n@cache(ttl=\"2h\", key=\"user_info:{user.name:lower}:{password:hash(sha1)}\")\nasync def get_info(user: User, password: str):\n    ...\n\n\n@cache(ttl=\"2h\", key=\"user:{token:jwt(client_id)}\")\nasync def get_user_by_token(token: str) -\u003e User:\n    ...\n\n```\n\nOr define your own transformation functions:\n\n```python\nfrom cashews import default_formatter, cache\n\ncache.setup(\"mem://\")\n\n@default_formatter.register(\"prefix\")\ndef _prefix(value, chars=3):\n    return value[:chars].upper()\n\n\n@cache(ttl=\"2h\", key=\"servers-user:{user.index:prefix(4)}\")  # a key will be \"servers-user:DWQS\"\nasync def get_user_servers(user):\n    ...\n\n```\n\nor register type formatters:\n\n```python\nfrom decimal import Decimal\nfrom cashews import default_formatter, cache\n\n@default_formatter.type_format(Decimal)\ndef _decimal(value: Decimal) -\u003e str:\n    return str(value.quantize(Decimal(\"0.00\")))\n\n\n@cache(ttl=\"2h\", key=\"price-{item.price}:{item.currency:upper}\")  # a key will be \"price-10.00:USD\"\nasync def convert_price(item):\n    ...\n\n```\n\nNot only function arguments can participate in a key formation. Cashews have a `template_context', use `@:get` in a template to paste variable from a context:\n\n```python\nfrom cashews import cache, key_context\n\ncache.setup(\"mem://\")\n\n\n@cache(ttl=\"2h\", key=\"user:{@:get(client_id)}\")\nasync def get_current_user():\n  pass\n\n...\nwith key_context(client_id=135356):\n    await get_current_user()\n\n```\n\n#### Template for a class method\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\nclass MyClass:\n\n    @cache(ttl=\"2h\")\n    async def get_name(self, user, version=\"v1\"):\n         ...\n\n# a key template will be \"__module__:MyClass.get_name:self:{self}:user:{user}:version:{version}\n\nawait MyClass().get_name(\"me\", version=\"v2\")\n# a key will be \"__module__:MyClass.get_name:self:\u003c__module__.MyClass object at 0x105edd6a0\u003e:user:me:version:v1\"\n```\n\nAs you can see, there is an ugly reference to the instance in the key. That is not what we expect to see.\nThat cache will not work properly. There are 3 solutions to avoid it:\n\n1. define `__str__` magic method in our class\n\n```python\n\nclass MyClass:\n\n    @cache(ttl=\"2h\")\n    async def get_name(self, user, version=\"v1\"):\n         ...\n\n    def __str__(self) -\u003e str:\n        return self._host\n\nawait MyClass(host=\"http://example.com\").get_name(\"me\", version=\"v2\")\n# a key will be \"__module__:MyClass.get_name:self:http://example.com:user:me:version:v1\"\n```\n\n2. Set a key template\n\n```python\nclass MyClass:\n\n    @cache(ttl=\"2h\", key=\"{self._host}:name:{user}:{version}\")\n    async def get_name(self, user, version=\"v1\"):\n         ...\n\nawait MyClass(host=\"http://example.com\").get_name(\"me\", version=\"v2\")\n# a key will be \"http://example.com:name:me:v1\"\n```\n\n3. Use `noself` or `noself_cache` if you want to exclude `self` from a key\n\n```python\nfrom cashews import cache, noself, noself_cache\n\ncache.setup(\"mem://\")\n\nclass MyClass:\n\n    @noself(cache)(ttl=\"2h\")\n    async def get_name(self, user, version=\"v1\"):\n         ...\n\n# a key template will be \"__module__:MyClass.get_name:user:{user}:version:{version}\n\nawait MyClass().get_name(\"me\", version=\"v2\")\n# a key will be \"__module__:MyClass.get_name:user:me:version:v1\"\n```\n\n### TTL\n\nCache time to live (`ttl`) is a required parameter for all cache decorators. TTL can be:\n\n- an integer as the number of seconds\n- a `timedelta`\n- a string like in golang e.g `1d2h3m50s`\n- a callable object like a function that receives `args` and `kwargs` of the decorated function and returns one of the previous format for TTL\n\nExamples:\n\n```python\nfrom cashews import cache\nfrom datetime import timedelta\n\ncache.setup(\"mem://\")\n\n@cache(ttl=60 * 10)\nasync def get(item_id: int) -\u003e Item:\n    pass\n\n@cache(ttl=timedelta(minutes=10))\nasync def get(item_id: int) -\u003e Item:\n    pass\n\n@cache(ttl=\"10m\")\nasync def get(item_id: int) -\u003e Item:\n    pass\n\ndef _ttl(item_id: int) -\u003e str:\n    return \"2h\" if item_id \u003e 10 else \"1h\"\n\n@cache(ttl=_ttl)\nasync def get(item_id: int) -\u003e Item:\n    pass\n```\n\n### What can be cached\n\nCashews mostly use built-in pickle to store data but also support other pickle-like serialization like dill.\nSome types of objects are not picklable, in this case, cashews has API to define custom encoding/decoding:\n\n```python\nfrom cashews.serialize import register_type\n\n\nasync def my_encoder(value: CustomType, *args, **kwargs) -\u003e bytes:\n    ...\n\n\nasync def my_decoder(value: bytes, *args, **kwargs) -\u003e CustomType:\n    ...\n\n\nregister_type(CustomType, my_encoder, my_decoder)\n```\n\n### Cache invalidation\n\nCache invalidation - one of the main Computer Science well-known problems.\n\nSometimes, you want to invalidate the cache after some action is triggered.\nConsider this example:\n\n```python\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@cache(ttl=\"1h\", key=\"items:page:{page}\")\nasync def items(page=1):\n    ...\n\n@cache.invalidate(\"items:page:*\")\nasync def create_item(item):\n   ...\n```\n\nHere, the cache for `items` will be invalidated every time `create_item` is called\nThere are two problems:\n\n1. with redis backend you cashews will scan a full database to get a key that match a pattern (`items:page:*`) - not good for performance reasons\n2. what if we do not specify a key for cache:\n\n```python\n@cache(ttl=\"1h\")\nasync def items(page=1):\n    ...\n```\n\nCashews provide the tag system: you can tag cache keys, so they will be stored in a separate [SET](https://redis.io/docs/data-types/sets/)\nto avoid high load on redis storage. To use the tags in a more efficient way please use it with the client side feature.\n\n\u003e :warning: \\*\\*Warning: Tags require setting up default cache or cache for tags prefix\n\u003e ```python\n\u003e from cashews import cache\n\u003e cache.setup(...)\n\u003e # or\n\u003e cache.setup_tags_backend(...)\n\u003e ```\n\n\n```python\nfrom cashews import cache\n\ncache.setup(\"redis://\", client_side=True)\n\n@cache(ttl=\"1h\", tags=[\"items\", \"page:{page}\"])\nasync def items(page=1):\n    ...\n\n\nawait cache.delete_tags(\"page:1\")\nawait cache.delete_tags(\"items\")\n\n# low level api\ncache.register_tag(\"my_tag\", key_template=\"key{i}\")\n\nawait cache.set(\"key1\", \"value\", expire=\"1d\", tags=[\"my_tag\"])\n```\n\nYou can invalidate future call of cache request by context manager:\n\n```python\nfrom cashews import cache, invalidate_further\n\n@cache(ttl=\"3h\")\nasync def items():\n    ...\n\nasync def add_item(item: Item) -\u003e List[Item]:\n    ...\n    with invalidate_further():\n        await items\n```\n\n#### Cache invalidation on code change\n\nOften, you may face a problem with an invalid cache after the code is changed. For example:\n\n```python\n@cache(ttl=timedelta(days=1), key=\"user:{user_id}\")\nasync def get_user(user_id):\n    return {\"name\": \"Dmitry\", \"surname\": \"Krykov\"}\n```\n\nThen, the returned value was changed to:\n\n```bash\n-    return {\"name\": \"Dmitry\", \"surname\": \"Krykov\"}\n+    return {\"full_name\": \"Dmitry Krykov\"}\n```\n\nSince the function returns a dict, there is no simple way to automatically detect\nthat kind of cache invalidity\n\nOne way to solve the problem is to add a prefix for this cache:\n\n```python\n@cache(ttl=timedelta(days=1), prefix=\"v2\")\nasync def get_user(user_id):\n    return {\"full_name\": \"Dmitry Krykov\"}\n```\n\nbut it is so easy to forget to do it...\n\nThe best defense against this problem is to use your own datacontainers, like\n[dataclasses](https://docs.python.org/3/library/dataclasses.html),\nwith defined `__repr__` method.\nThis will add distinctness and `cashews` can detect changes in such structures automatically\nby checking [object representation](https://docs.python.org/3/reference/datamodel.html#object.__repr__).\n\n```python\nfrom dataclasses import dataclass\n\nfrom cashews import cache\n\ncache.setup(\"mem://\")\n\n@dataclass\nclass User:\n    name: str\n    surname: str\n\n# or define your own class with __repr__ method\n\nclass User:\n\n    def __init__(self, name, surname):\n        self.name, self.surname = name, surname\n\n    def __repr__(self):\n        return f\"{self.name} {self.surname}\"\n\n# Will detect changes of a structure\n@cache(ttl=\"1d\", prefix=\"v2\")\nasync def get_user(user_id):\n    return User(\"Dima\", \"Krykov\")\n```\n\n### Detect the source of a result\n\nDecorators give us a very simple API but also make it difficult to understand where\nthe result is coming from - cache or direct call.\n\nTo solve this problem `cashews` has `detect` context manager:\n\n```python\nfrom cashews import cache\n\nwith cache.detect as detector:\n    response = await something_that_use_cache()\n    calls = detector.calls\n\nprint(calls)\n# \u003e\u003e\u003e {\"my:key\": [{\"ttl\": 10, \"name\": \"simple\", \"backend\": \"redis\"}, ], \"fail:key\": [{\"ttl\": 10, \"exc\": RateLimit}, \"name\": \"fail\", \"backend\": \"mem\"],}\n```\n\nE.g. A simple middleware to use it in a web app:\n\n```python\n@app.middleware(\"http\")\nasync def add_from_cache_headers(request: Request, call_next):\n    with cache.detect as detector:\n        response = await call_next(request)\n        if detector.calls:\n            key = list(detector.calls.keys())[0]\n            response.headers[\"X-From-Cache\"] = key\n            expire = await cache.get_expire(key)\n            response.headers[\"X-From-Cache-Expire-In-Seconds\"] = str(expire)\n    return response\n```\n\n### Middleware\n\nCashews provide the interface for a \"middleware\" pattern:\n\n```python\nimport logging\nfrom cashews import cache\n\nlogger = logging.getLogger(__name__)\n\n\nasync def logging_middleware(call, cmd: Command, backend: Backend, *args, **kwargs):\n    key = args[0] if args else kwargs.get(\"key\", kwargs.get(\"pattern\", \"\"))\n    logger.info(\"=\u003e Cache request: %s \", cmd.value, extra={\"args\": args, \"cache_key\": key})\n    return await call(*args, **kwargs)\n\n\ncache.setup(\"mem://\", middlewares=(logging_middleware, ))\n```\n\n#### Callbacks\n\nOne of the middleware that is preinstalled in cache instance is `CallbackMiddleware`.\nThis middleware also add to a cache a new interface that allow to add a function that will be called before given command will be triggered\n\n```python\nfrom cashews import cache, Command\n\n\ndef callback(key, result):\n  print(f\"GET key={key}\")\n\nwith cache.callback(callback, cmd=Command.GET):\n    await cache.get(\"test\")  # also will print \"GET key=test\"\n\n```\n\n### Transactional\n\nApplications are more often based on a database with transaction (OLTP) usage. Usually cache supports transactions poorly.\nHere is just a simple example of how we can make our cache inconsistent:\n\n```python\nasync def my_handler():\n    async with db.transaction():\n        await db.insert(user)\n        await cache.set(f\"key:{user.id}\", user)\n        await api.service.register(user)\n```\n\nHere the API call may fail, the database transaction will rollback, but the cache will not.\nOf course, in this code, we can solve it by moving the cache call outside transaction, but in real code it may not so easy.\nAnother case: we want to make bulk operations with a group of keys to keep it consistent:\n\n```python\nasync def login(user, token, session):\n    ...\n    old_session = await cache.get(f\"current_session:{user.id}\")\n    await cache.incr(f\"sessions_count:{user.id}\")\n    await cache.set(f\"current_session:{user.id}\", session)\n    await cache.set(f\"token:{token.id}\", user)\n    return old_session\n```\n\nHere we want to have some way to protect our code from race conditions and do operations with cache simultaneously.\n\nCashews support transaction operations:\n\n  \u003e :warning: \\*\\*Warning: transaction operations are `set`, `set_many`, `delete`, `delete_many`, `delete_match` and `incr`\n\n```python\nfrom cashews import cache\n...\n\n@cache.transaction()\nasync def my_handler():\n    async with db.transaction():\n        await db.insert(user)\n        await cache.set(f\"key:{user.id}\", user)\n        await api.service.register(user)\n\n# or\nasync def login(user, token, session):\n    async with cache.transaction() as tx:\n        old_session = await cache.get(f\"current_session:{user.id}\")\n        await cache.incr(f\"sessions_count:{user.id}\")\n        await cache.set(f\"current_session:{user.id}\", session)\n        await cache.set(f\"token:{token.id}\", user)\n        if ...:\n            tx.rollback()\n    return old_session\n\n```\n\nTransactions in cashews support different modes of \"isolation\"\n\n- fast (0-7% overhead) - memory based, can't protect of race conditions, but may use for atomicity\n- locked (default - 4-9% overhead) - use kind of shared lock per cache key (in case of redis or disk backend), protect of race conditions\n- serializable (7-50% overhead) - use global shared lock - one transaction per time (almost useless)\n\n```python\nfrom cashews import cache, TransactionMode\n...\n\n@cache.transaction(TransactionMode.SERIALIZABLE, timeout=1)\nasync def my_handler():\n   ...\n```\n\n### Contrib\n\nThis library is framework agnostic, but includes several \"batteries\" for most popular tools.\n\n#### Fastapi\n\nYou may find a few middlewares useful that can help you to control a cache in you web application based on fastapi.\n\n1. `CacheEtagMiddleware` - middleware add Etag and check 'If-None-Match' header based on Etag\n2. `CacheRequestControlMiddleware` - middleware check and add `Cache-Control` header\n3. `CacheDeleteMiddleware` - clear cache for an endpoint based on `Clear-Site-Data` header\n\n\u003e :warning: \\*\\*Warning: CacheEtagMiddleware requires setting up default cache or cache with prefix \"fastapi:\"\n\u003e ```python\n\u003e from cashews import cache\n\u003e cache.setup(...)\n\u003e # or\n\u003e cache.setup(..., prefix=\"fastapi:\")\n\u003e ```\n\nExample:\n\n```python\nfrom fastapi import FastAPI, Header, Query\nfrom fastapi.responses import StreamingResponse\n\nfrom cashews import cache\nfrom cashews.contrib.fastapi import (\n    CacheDeleteMiddleware,\n    CacheEtagMiddleware,\n    CacheRequestControlMiddleware,\n    cache_control_ttl,\n)\n\napp = FastAPI()\napp.add_middleware(CacheDeleteMiddleware)\napp.add_middleware(CacheEtagMiddleware)\napp.add_middleware(CacheRequestControlMiddleware)\nmetrics_middleware = create_metrics_middleware()\ncache.setup(os.environ.get(\"CACHE_URI\", \"redis://\"))\n\n\n\n@app.get(\"/\")\n@cache.failover(ttl=\"1h\")\n@cache(ttl=cache_control_ttl(default=\"4m\"), key=\"simple:{user_agent:hash}\", time_condition=\"1s\")\nasync def simple(user_agent: str = Header(\"No\")):\n    ...\n\n\n@app.get(\"/stream\")\n@cache(ttl=\"1m\", key=\"stream:{file_path}\")\nasync def stream(file_path: str = Query(__file__)):\n    return StreamingResponse(_read_file(file_path=file_path))\n\n\nasync def _read_file(_read_file):\n    ...\n\n```\n\nAlso cashews can cache stream responses\n\n#### Prometheus\n\nYou can easily provide metrics using the Prometheus middleware.\n\n```python\nfrom cashews import cache\nfrom cashews.contrib.prometheus import create_metrics_middleware\n\nmetrics_middleware = create_metrics_middleware(with_tag=False)\ncache.setup(\"redis://\", middlewares=(metrics_middleware,))\n\n```\n\n## Development\n\n### Setup\n\n- Clone the project.\n- After creating a virtual environment, install [pre-commit](https://pre-commit.com/):\n  ```shell\n  pip install pre-commit \u0026\u0026 pre-commit install --install-hooks\n  ```\n\n### Tests\n\nTo run tests you can use `tox`:\n\n```shell\npip install tox\ntox -e py  // tests for inmemory backend\ntox -e py-diskcache  // tests for diskcache backend\ntox -e py-redis  // tests for redis backend  - you need to run redis\ntox -e py-integration  // tests for integrations with aiohttp and fastapi\n\ntox // to run all tests for all python that is installed on your machine\n```\n\nOr use `pytest`, but 2 tests always fail, it is OK:\n\n```shell\npip install .[tests,redis,diskcache,speedup] fastapi aiohttp requests httpx SQLAlchemy prometheus-client\n\npytest // run all tests with all backends\npytest -m \"not redis\" // all tests without tests for redis backend\n```\n","funding_links":[],"categories":["Python"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKrukov%2Fcashews","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FKrukov%2Fcashews","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FKrukov%2Fcashews/lists"}