Skip to content

Commit

Permalink
Merge pull request #189 from Krukov/key-context
Browse files Browse the repository at this point in the history
key context
  • Loading branch information
Krukov authored Jan 7, 2024
2 parents 3720979 + 0aa7d20 commit 95e16bd
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 61 deletions.
138 changes: 95 additions & 43 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ async def get():

### Template Keys

Often, to compose a key, you need all the parameters of the function call.
Often, 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 parameters

```python
Expand All @@ -537,7 +537,99 @@ await get_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"
```

The same with a class method
For more advanced usage it better to define a cache key manually:

```python
from cashews import cache

cache.setup("mem://")

@cache(ttl="2h", key="user_info:{user_id}")
async def get_info(user_id: str):
...

```

You may use objects in a key and access to an attribute through a template:

```python

@cache(ttl="2h", key="user_info:{user.uuid}")
async def get_info(user: User):
...

```

You may use built-in functions to format template values (`lower`, `upper`, `len`, `jwt`, `hash`)

```python

@cache(ttl="2h", key="user_info:{user.name:lower}:{password:hash(sha1)}")
async def get_info(user: User, password: str):
...


@cache(ttl="2h", key="user:{token:jwt(client_id)}")
async def get_user_by_token(token: str) -> User:
...

```

Or define your own transformation functions:

```python
from cashews import default_formatter, cache

cache.setup("mem://")

@default_formatter.register("prefix")
def _prefix(value, chars=3):
return value[:chars].upper()


@cache(ttl="2h", key="servers-user:{user.index:prefix(4)}") # a key will be "servers-user:DWQS"
async def get_user_servers(user):
...

```

or register type formatters:

```python
from decimal import Decimal
from cashews import default_formatter, cache

@default_formatter.type_format(Decimal)
def _decimal(value: Decimal) -> str:
return str(value.quantize(Decimal("0.00")))


@cache(ttl="2h", key="price-{item.price}:{item.currency:upper}") # a key will be "price-10.00:USD"
async def convert_price(item):
...

```

Not only function arguments can participate in a key formation. Cashews have a `template_context`. You may use any variable registered in it:

```python
from cashews import cache, key_context, register_key_context

cache.setup("mem://")
register_key_context("client_id")


@cache(ttl="2h", key="user:{client_id}")
async def get_current_user():
pass

...
with key_context(client_id=135356):
await get_current_user()

```

#### Template for a class method

```python
from cashews import cache
Expand Down Expand Up @@ -611,46 +703,6 @@ await MyClass().get_name("me", version="v2")
# a key will be "__module__:MyClass.get_name:user:me:version:v1"
```

Sometimes you may need to format the parameters or define your
own template for the key and Cashews allows you to do this:

```python
from cashews import default_formatter, cache

cache.setup("mem://")

@cache.failover(key="name:{user.uid}")
async def get_name(user, version="v1"):
...

await get_name(user, version="v2")
# a key will be "fail:name:me"

@cache.hit(key="user:{token:jwt(user_name)}", prefix="new")
async def get_name(token):
...

await get_name(".....")
# a key will be "new:user:alex"


@default_formatter.register("upper")
def _upper(value):
return value.upper()

@default_formatter.type_format(Decimal)
def _decimal(value: Decimal) -> str:
return value.quantize(Decimal("0.00"))


@cache(key="price-{item.price}:{item.currency:upper}")
async def get_price(item):
...

await get_name(item)
# a key will be "price-10.00:USD"
```

### TTL

Cache time to live (`ttl`) is a required parameter for all cache decorators. TTL can be:
Expand Down Expand Up @@ -742,7 +794,7 @@ async def items(page=1):
```

Cashews 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/)
to avoid high load on redis storage. To use the tags in a more efficient way please use it with the client side feature
to avoid high load on redis storage. To use the tags in a more efficient way please use it with the client side feature.

```python
from cashews import cache
Expand Down
4 changes: 4 additions & 0 deletions cashews/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from .formatter import default_formatter
from .helpers import add_prefix, all_keys_lower, memory_limit
from .key import get_cache_key_template, noself
from .key_context import context as key_context
from .key_context import register as register_key_context
from .validation import invalidate_further
from .wrapper import Cache, TransactionMode, register_backend

Expand Down Expand Up @@ -74,4 +76,6 @@
"Cache",
"TransactionMode",
"register_backend",
"key_context",
"register_key_context",
]
8 changes: 4 additions & 4 deletions cashews/contrib/fastapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from cashews import Cache, Command, cache, invalidate_further
from cashews._typing import TTL

_CACHE_MAX_AGE: ContextVar[int] = ContextVar("cache_control_max_age")
_cache_max_age: ContextVar[int] = ContextVar("cache_control_max_age")

_CACHE_CONTROL_HEADER = "Cache-Control"
_AGE_HEADER = "Age"
Expand All @@ -35,7 +35,7 @@

def cache_control_ttl(default: TTL):
def _ttl(*args, **kwargs):
return _CACHE_MAX_AGE.get(default)
return _cache_max_age.get(default)

return _ttl

Expand Down Expand Up @@ -78,12 +78,12 @@ def max_age(self, cache_control_value: str | None):
_max_age = self._get_max_age(cache_control_value)
reset_token = None
if _max_age:
reset_token = _CACHE_MAX_AGE.set(_max_age)
reset_token = _cache_max_age.set(_max_age)
try:
yield
finally:
if reset_token:
_CACHE_MAX_AGE.reset(reset_token)
_cache_max_age.reset(reset_token)

def _to_disable(self, cache_control_value: str | None) -> tuple[Command, ...]:
if cache_control_value == _NO_CACHE:
Expand Down
7 changes: 5 additions & 2 deletions cashews/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from string import Formatter
from typing import Any, Callable, Dict, Iterable, Pattern, Tuple

from . import key_context
from ._typing import KeyOrTemplate, KeyTemplate

TemplateValue = str
Expand Down Expand Up @@ -156,8 +157,10 @@ def _upper(value: TemplateValue) -> TemplateValue:
return value.upper()


def template_to_pattern(template: KeyTemplate, _formatter=_ReplaceFormatter(), **values) -> KeyOrTemplate:
return _formatter.format(template, **values)
def default_format(template: KeyTemplate, **values) -> KeyOrTemplate:
_template_context = key_context.get()
_template_context.update(values)
return default_formatter.format(template, **_template_context)


def _re_default(field_name):
Expand Down
7 changes: 4 additions & 3 deletions cashews/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from typing import TYPE_CHECKING, Any, Callable, Container, Dict, Iterable, Tuple

from .exceptions import WrongKeyError
from .formatter import _ReplaceFormatter, default_formatter, template_to_pattern
from .formatter import _ReplaceFormatter, default_format
from .key_context import get as get_key_context

if TYPE_CHECKING: # pragma: no cover
from ._typing import Key, KeyOrTemplate, KeyTemplate
Expand Down Expand Up @@ -36,7 +37,7 @@ def get_cache_key(
kwargs = kwargs or {}
key_values = _get_call_values(func, args, kwargs)
_key_template = template or get_cache_key_template(func)
return template_to_pattern(_key_template, _formatter=default_formatter, **key_values)
return default_format(_key_template, **key_values)


def get_func_params(func: Callable) -> Iterable[str]:
Expand Down Expand Up @@ -115,7 +116,7 @@ def _default(name):
return "*"

check = _ReplaceFormatter(default=_default)
check.format(key, **func_params)
check.format(key, **{**get_key_context(), **func_params})
if errors:
raise WrongKeyError(f"Wrong parameter placeholder '{errors}' in the key ")

Expand Down
26 changes: 26 additions & 0 deletions cashews/key_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from __future__ import annotations

from contextlib import contextmanager
from contextvars import ContextVar
from typing import Any, Iterator

_template_context: ContextVar[dict[str, Any]] = ContextVar("template_context", default={})


@contextmanager
def context(**values) -> Iterator[None]:
new_context = {**_template_context.get(), **values}
token = _template_context.set(new_context)
try:
yield
finally:
_template_context.reset(token)


def get():
return {**_template_context.get()}


def register(*names: str) -> None:
new_names = {name: "" for name in names}
_template_context.set({**new_names, **_template_context.get()})
4 changes: 2 additions & 2 deletions cashews/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from contextlib import contextmanager
from contextvars import ContextVar
from functools import wraps
from typing import Any, Dict, Optional
from typing import Any, Dict, Iterator, Optional

from ._typing import AsyncCallable_T
from .backends.interface import _BackendInterface
Expand Down Expand Up @@ -41,7 +41,7 @@ async def _wrap(*args, **kwargs):


@contextmanager
def invalidate_further():
def invalidate_further() -> Iterator[None]:
_INVALIDATE_FURTHER.set(True)
try:
yield
Expand Down
4 changes: 2 additions & 2 deletions cashews/wrapper/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from cashews._typing import TTL, Key, KeyOrTemplate, OnRemoveCallback, Tag, Tags, Value
from cashews.backends.interface import Backend
from cashews.exceptions import TagNotRegisteredError
from cashews.formatter import default_formatter, template_to_pattern, template_to_re_pattern
from cashews.formatter import default_format, template_to_re_pattern

from .commands import CommandWrapper

Expand All @@ -24,7 +24,7 @@ def get_key_tags(self, key: Key) -> Tags:
match = self._match_patterns(key, patterns)
if match:
group_dict = {k: v if v is not None else "" for k, v in match.groupdict().items()}
tag = template_to_pattern(tag, _formatter=default_formatter, **group_dict)
tag = default_format(tag, **group_dict)
tags.append(tag)
return tags

Expand Down
14 changes: 12 additions & 2 deletions tests/test_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from cashews.exceptions import WrongKeyError
from cashews.formatter import default_formatter
from cashews.key import get_cache_key, get_cache_key_template
from cashews.key_context import context as key_context
from cashews.key_context import register as register_context
from cashews.ttl import ttl_to_seconds


Expand Down Expand Up @@ -161,13 +163,20 @@ async def func(user):
"{kwarg1:jwt(user)}",
"test",
),
(
(),
{"kwarg": "1"},
"{context_value:upper}:{kwarg}",
"CONTEXT:1",
),
),
)
def test_cache_key_args_kwargs(args, kwargs, template, key):
async def func(arg1, arg2, *args, kwarg1=None, kwarg2=b"true", **kwargs):
...

assert get_cache_key(func, template, args=args, kwargs=kwargs) == key
with key_context(context_value="context", kwarg1="test"):
assert get_cache_key(func, template, args=args, kwargs=kwargs) == key


@pytest.mark.parametrize(
Expand All @@ -179,10 +188,11 @@ async def func(arg1, arg2, *args, kwarg1=None, kwarg2=b"true", **kwargs):
(Klass.method, None, "tests.test_key:Klass.method:self:{self}:a:{a}:k:{k}"),
(Klass.method, "key:{k}:{self.data.test}", "key:{k}:{self.data.test}"),
(func2, "key:{k}", "key:{k}"),
(func3, "key:{k:len}:{k:hash(md5)}", "key:{k:len}:{k:hash(md5)}"),
(func3, "key:{k:len}:{k:hash(md5)}:{val}", "key:{k:len}:{k:hash(md5)}:{val}"),
),
)
def test_get_key_template(func, key, template):
register_context("val", "k")
assert get_cache_key_template(func, key) == template


Expand Down
Loading

0 comments on commit 95e16bd

Please sign in to comment.