Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/viur/core/bones/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1211,7 +1211,7 @@ def buildDBSort(
)
prop = orderby_prop

query.order((prop + postfix, utils.parse.sortorder(params.get("orderdir"))))
query.order((prop + postfix, db.SortOrder.from_str(params.get("orderdir"))))

return query

Expand Down
2 changes: 1 addition & 1 deletion src/viur/core/bones/relational.py
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ def buildDBSort(
else:
path = f"{name}.{_type}.{param}"

order = utils.parse.sortorder(params.get("orderdir"))
order = db.SortOrder.from_str(params.get("orderdir"))
query = query.order((path, order))

if self.multiple:
Expand Down
35 changes: 20 additions & 15 deletions src/viur/core/db/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import typing as t

from viur.core.config import conf
from viur.core import utils
from .types import Entity, Key

MEMCACHE_MAX_BATCH_SIZE = 30
Expand Down Expand Up @@ -37,7 +38,7 @@
]


def get(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> t.Union[Entity, list[Entity], None]:
def get(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> t.Union[Entity, list[Entity], None]:
"""
Reads data form the memcache.
:param keys: Unique identifier(s) for one or more entry(s).
Expand All @@ -48,23 +49,24 @@ def get(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> t.U
return None

namespace = namespace or MEMCACHE_NAMESPACE
single_request = not isinstance(keys, (list, tuple, set))
if single_request:
keys = [keys]
if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
Comment on lines +52 to +53
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
keys = utils.ensure_iterable(keys)

keys = [str(key) for key in keys] # Enforce that all keys are strings
cached_data = {}
cached_data_result = {}
result = []
try:
while keys:
cached_data |= conf.db.memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace)
if cached_data := conf.db_memcache_client.get_multi(keys[:MEMCACHE_MAX_BATCH_SIZE], namespace=namespace):
cached_data_result |= cached_data
keys = keys[MEMCACHE_MAX_BATCH_SIZE:]
except Exception as e:
logging.error(f"""Failed to get keys form the memcache with {e=}""")
for key, value in cached_data.items():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached data might be uninizialized!

entity = Entity(Key(key))
entity = Entity(Key.from_legacy_urlsafe(key))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these always str-keys?

entity |= value
result.append(entity)
if single_request:

if len(result) == 1:
return result[0] if result else None
return result if result else None

Expand All @@ -83,18 +85,19 @@ def put(
"""
if not check_for_memcache():
return False

if not data:
return False
namespace = namespace or MEMCACHE_NAMESPACE
timeout = timeout or MEMCACHE_TIMEOUT
if isinstance(timeout, datetime.timedelta):
timeout = timeout.total_seconds()

if isinstance(data, (list, tuple, set)):
data = {item.key: item for item in data}
elif isinstance(data, Entity):
data = {data.key: data}
elif not isinstance(data, dict):
raise TypeError(f"Invalid type {type(data)}. Expected a db.Entity, list or dict.")

# Add only values to cache <= MEMMAX_SIZE (1.000.000)
data = {str(key): value for key, value in data.items() if get_size(value) <= MEMCACHE_MAX_SIZE}

Expand All @@ -110,18 +113,19 @@ def put(
return False


def delete(keys: t.Union[Key, list[Key]], namespace: t.Optional[str] = None) -> None:
def delete(keys: t.Union[Key, t.Iterable[Key]], namespace: t.Optional[str] = None) -> None:
"""
Deletes an Entry form memcache.
:param keys: Unique identifier(s) for one or more entry(s).
:param namespace: Optional namespace to use.
"""
if not check_for_memcache():
return None

if not keys:
return None
namespace = namespace or MEMCACHE_NAMESPACE
if not isinstance(keys, list):
keys = [keys]
if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
Comment on lines +127 to +128
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
keys = utils.ensure_iterable(keys)

keys = [str(key) for key in keys] # Enforce that all keys are strings
try:
while keys:
Expand Down Expand Up @@ -160,8 +164,9 @@ def get_size(obj: t.Any) -> int:

def check_for_memcache() -> bool:
if conf.db.memcache_client is None:
logging.warning(f"""conf.db.memcache_client is 'None'. It can not be used.""")
# logging.warning(f"""conf.db.memcache_client is 'None'. It can not be used.""")
return False

init_testbed()
return True

Expand Down
50 changes: 40 additions & 10 deletions src/viur/core/db/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

from .overrides import entity_from_protobuf, key_from_protobuf
from .types import Entity, Key, QueryDefinition, SortOrder, current_db_access_log
from . import cache
from viur.core.config import conf
from viur.core import utils

# patching our key and entity classes
datastore.helpers.key_from_protobuf = key_from_protobuf
Expand All @@ -35,7 +37,7 @@ def AllocateIDs(kind_name):
return allocate_ids(kind_name)[0]


def get(keys: t.Union[Key, t.List[Key]]) -> t.Union[t.List[Entity], Entity, None]:
def get(keys: t.Union[Key, t.Iterable[Key]]) -> t.Union[t.List[Entity], Entity, None]:
"""
Retrieves an entity (or a list thereof) from datastore.
If only a single key has been given we'll return the entity or none in case the key has not been found,
Expand All @@ -45,12 +47,30 @@ def get(keys: t.Union[Key, t.List[Key]]) -> t.Union[t.List[Entity], Entity, None
"""
_write_to_access_log(keys)

if isinstance(keys, (list, set, tuple)):
res_list = list(__client__.get_multi(keys))
res_list.sort(key=lambda k: keys.index(k.key) if k else -1)
return res_list
keys = utils.ensure_iterable(keys)
if not keys:
return None

return __client__.get(keys)
cached = cache.get(keys) or ()

if len(keys) == 1:
if cached:
return cached

if res := __client__.get(keys[0]):
cache.put(res)

return res

keys = {key: cached.get(key) for key in keys}

uncached = __client__.get_multi((k for k, v in keys.items() if v is None))
cache.put(uncached)

for e in uncached:
keys[e.key] = e

return list(keys.values())


@deprecated(version="3.8.0", reason="Use 'db.get' instead")
Expand All @@ -65,6 +85,7 @@ def put(entities: t.Union[Entity, t.List[Entity]]):
:param entities: The entities to be saved to the datastore.
"""
_write_to_access_log(entities)
cache.put(entities)
if isinstance(entities, Entity):
return __client__.put(entities)

Expand All @@ -76,15 +97,24 @@ def Put(entities: t.Union[Entity, t.List[Entity]]) -> t.Union[Entity, None]:
return put(entities)


def delete(keys: t.Union[Entity, t.List[Entity], Key, t.List[Key]]):
def delete(keys: t.Union[Entity, t.Iterable[Entity], Key, t.Iterable[Key]]):
"""
Deletes the entities with the given key(s) from the datastore.
:param keys: A Key (or a t.List of Keys) to delete
"""

if not keys:
return None
_write_to_access_log(keys)
if not isinstance(keys, (set, list, tuple)):
return __client__.delete(keys)

if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
Comment on lines +109 to +110
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not isinstance(keys, (tuple, list, set)):
keys = (keys,)
keys = utils.ensure_iterable(keys)

keys = [k.key if isinstance(k, Entity) else k for k in keys]
if not keys:
return
Comment on lines +112 to +113
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is tested twice, the keys list cannot be empty when coming to this line

Suggested change
if not keys:
return


cache.delete(keys)
if len(keys) == 1:
return __client__.delete(keys[0])

return __client__.delete_multi(keys)

Expand Down
19 changes: 19 additions & 0 deletions src/viur/core/db/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@


class SortOrder(enum.Enum):
"""
Defines possible types of sort orders for queries.
"""

Ascending = 1
"""Sort A->Z"""
Descending = 2
Expand All @@ -34,6 +38,21 @@ class SortOrder(enum.Enum):
InvertedDescending = 4
"""Fetch A->Z, then flip the results (useful in pagination)"""

@classmethod
def from_str(cls, ident: str | int) -> SortOrder:
"""
Parses a string defining a sort order into a db.SortOrder.
"""
match str(ident or "").lower():
case "desc" | "descending" | "1":
return SortOrder.Descending
case "inverted_asc" | "inverted_ascending" | "2":
return SortOrder.InvertedAscending
case "inverted_desc" | "inverted_descending" | "3":
return SortOrder.InvertedDescending
case _: # everything else
return SortOrder.Ascending


class Key(Datastore_key):
"""
Expand Down
5 changes: 3 additions & 2 deletions src/viur/core/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import warnings
from collections.abc import Iterable

from viur.core import current, db
from viur.core import current
from viur.core.config import conf
from deprecated.sphinx import deprecated
from . import json, parse, string # noqa: used by external imports
Expand Down Expand Up @@ -89,14 +89,15 @@ def seoUrlToFunction(module: str, function: str, render: t.Optional[str] = None)


@deprecated(version="3.8.0", reason="Use 'db.normalize_key' instead")
def normalizeKey(key: t.Union[None, db.Key]) -> t.Union[None, db.Key]:
def normalizeKey(key: t.Union[None, "db.Key"]) -> t.Union[None, "db.Key"]:
"""
Normalizes a datastore key (replacing _application with the current one)

:param key: Key to be normalized.

:return: Normalized key in string representation.
"""
from viur.core import db # let this be slow...
db.normalize_key(key)


Expand Down
16 changes: 0 additions & 16 deletions src/viur/core/utils/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
ViUR utility functions regarding parsing.
"""
import typing as t
from viur.core import db
import datetime


Expand All @@ -21,21 +20,6 @@ def bool(value: t.Any, truthy_values: t.Iterable[str] = ("true", "yes", "1")) ->
return str(value).strip().lower() in truthy_values


def sortorder(ident: str | int) -> db.SortOrder:
"""
Parses a string defining a sort order into a db.SortOrder.
"""
match str(ident or "").lower():
case "desc" | "descending" | "1":
return db.SortOrder.Descending
case "inverted_asc" | "inverted_ascending" | "2":
return db.SortOrder.InvertedAscending
case "inverted_desc" | "inverted_descending" | "3":
return db.SortOrder.InvertedDescending
case _: # everything else
return db.SortOrder.Ascending


def timedelta(value: datetime.timedelta | int | float | str) -> datetime.timedelta:
"""
Parse a value into a timedelta object.
Expand Down