Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add compute-feature to BaseBone #639

Merged
merged 30 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9e66e10
Add computed for basebone
ArneGudermann Jan 25, 2023
3a867ac
Add unserilized support
ArneGudermann Jan 27, 2023
aaadab2
PEP 8 corrections
ArneGudermann Jan 30, 2023
4a0f296
Add render for numericbones
ArneGudermann Jan 30, 2023
c209447
Merge branch 'develop' into feature/computed
ArneGudermann Jan 31, 2023
c522364
Add Compute dataclass
ArneGudermann Feb 1, 2023
556f037
Set default mthod for ThresholdValue
ArneGudermann Feb 1, 2023
a57fb52
PEP 8 corrections
ArneGudermann Feb 1, 2023
4e7152a
Add support of ThresholdMethods.Once
ArneGudermann Feb 1, 2023
b489c88
Replace until by liftime
ArneGudermann Feb 17, 2023
43b63e1
Update core/bones/base.py
ArneGudermann Feb 23, 2023
38f06dc
Merge branch 'develop' into feature/computed
ArneGudermann Mar 1, 2023
e21afdc
Add computed in structure
ArneGudermann Mar 1, 2023
ec0c036
Add Onstore
ArneGudermann Mar 1, 2023
339c5b8
Add Checks
ArneGudermann Mar 1, 2023
ce080ed
Move last_updated
ArneGudermann Mar 1, 2023
fcf5ee4
PEP8 corrections
ArneGudermann Mar 1, 2023
8ed2fbe
Update core/bones/base.py
ArneGudermann Mar 9, 2023
20620a5
Apply suggestions from code review
ArneGudermann Mar 9, 2023
22cafdd
Apply suggestions from code review
ArneGudermann Mar 13, 2023
46ac64d
Add value calc in serilaces
ArneGudermann Mar 13, 2023
e834240
Merge branch 'develop' into feature/computed
phorward Apr 3, 2023
5e44ace
Merge branch 'develop' into feature/computed
phorward Apr 3, 2023
bbb939d
First rework draft
phorward Apr 3, 2023
20b825a
Some renaming and refactoring
phorward Apr 4, 2023
a101a3e
Fixing linter errors
phorward Apr 4, 2023
0efe11a
pep-8?
phorward Apr 4, 2023
825e4dd
Update core/bones/base.py
ArneGudermann Apr 14, 2023
e305c7c
Apply suggestions from code review
ArneGudermann Apr 17, 2023
c444091
Merge branch 'develop' into feature/computed
phorward May 19, 2023
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
19 changes: 16 additions & 3 deletions core/bones/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
from .base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity, UniqueValue, UniqueLockMethod, MultipleConstraints
from .base import (
BaseBone,
Compute,
ComputeInterval,
ComputeMethod,
MultipleConstraints,
ReadFromClientError,
ReadFromClientErrorSeverity,
UniqueLockMethod,
UniqueValue,
)
from .boolean import BooleanBone
from .captcha import CaptchaBone
from .color import ColorBone
Expand Down Expand Up @@ -30,6 +40,9 @@
"BooleanBone",
"CaptchaBone",
"ColorBone",
"Compute",
ArneGudermann marked this conversation as resolved.
Show resolved Hide resolved
"ComputeInterval",
"ComputeMethod",
"CredentialBone",
"DateBone",
"EmailBone",
Expand Down Expand Up @@ -79,10 +92,10 @@ def __init__(self, *args, **kwargs):

return __init__

locals()[__old_cls_name] = type(__old_cls_name, (__cls, ), {
locals()[__old_cls_name] = type(__old_cls_name, (__cls,), {
"__init__": __generate_deprecation_constructor(__cls, __cls_name, __old_cls_name)
})

#print(__old_cls_name, "installed as ", locals()[__old_cls_name], issubclass(locals()[__old_cls_name], __cls))
# print(__old_cls_name, "installed as ", locals()[__old_cls_name], issubclass(locals()[__old_cls_name], __cls))

__all__ = __all
145 changes: 140 additions & 5 deletions core/bones/base.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import copy
import hashlib
import inspect
import logging
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from enum import Enum
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union

from viur.core import db
from viur.core import db, utils
from viur.core.config import conf

__systemIsIntitialized_ = False
Expand Down Expand Up @@ -59,13 +61,34 @@ class MultipleConstraints: # Used to define constraints on multiple bones
preventDuplicates: bool = False # Prevent the same value of being used twice


class ComputeMethod(Enum):
Always = 0 # Always compute on deserialization
Lifetime = 1 # Update only when given lifetime is outrun; value is only being stored when the skeleton is written
Once = 2 # Compute only once
OnWrite = 3 # Compute before written


@dataclass
class ComputeInterval:
method: ComputeMethod = ComputeMethod.Always
lifetime: timedelta = None # defines a timedelta until which the value stays valid (`ComputeMethod.Lifetime`)


@dataclass
class Compute:
fn: callable # the callable computing the value
interval: ComputeInterval = ComputeInterval() # the value caching interval
raw: bool = True # defines whether the value returned by fn is used as is, or is passed through bone.fromClient


class BaseBone(object):
type = "hidden"
isClonedInstance = False

def __init__(
self,
*,
compute: Compute = None,
defaultValue: Any = None,
descr: str = "",
getEmptyValueFunc: callable = None,
Expand All @@ -74,7 +97,7 @@ def __init__(
languages: Union[None, List[str]] = None,
multiple: Union[bool, MultipleConstraints] = False,
params: Dict = None,
readOnly: bool = False,
readOnly: bool = None, # fixme: Rename into readonly (all lowercase!) soon.
phorward marked this conversation as resolved.
Show resolved Hide resolved
required: Union[bool, List[str], Tuple[str]] = False,
searchable: bool = False,
unique: Union[None, UniqueValue] = None,
Expand Down Expand Up @@ -102,6 +125,7 @@ def __init__(
protect the value from beeing exposed in a template, nor from being transferred to the
client (ie to the admin or as hidden-value in html-forms)
Again: This is just a hint. It cannot be used as a security precaution.
:param compute: If set the bonevalue will be computed in the given method.
phorward marked this conversation as resolved.
Show resolved Hide resolved

.. NOTE::
The kwarg 'multiple' is not supported by all bones
Expand All @@ -113,7 +137,7 @@ def __init__(
self.params = params or {}
self.multiple = multiple
self.required = required
self.readOnly = readOnly
self.readOnly = bool(readOnly)
self.searchable = searchable
self.visible = visible
self.indexed = indexed
Expand Down Expand Up @@ -167,6 +191,26 @@ def __init__(
if getEmptyValueFunc:
self.getEmptyValue = getEmptyValueFunc

if compute:
if not isinstance(compute, Compute):
raise TypeError("compute must be an instanceof of Compute")

# When readOnly is None, handle flag automatically
if readOnly is None:
self.readOnly = True
if not self.readOnly:
raise ValueError("'compute' can only be used with bones configured as `readOnly=True`")

if (
compute.interval.method == ComputeMethod.Lifetime
and not isinstance(compute.interval.lifetime, timedelta)
):
raise ValueError(
f"'compute' is configured as ComputeMethod.Lifetime, but {compute.interval.lifetime=} was specified"
)

self.compute = compute

def setSystemInitialized(self):
"""
Can be overridden to initialize properties that depend on the Skeleton system being initialized
Expand Down Expand Up @@ -438,6 +482,30 @@ def serialize(self, skel: 'SkeletonInstance', name: str, parentIndexed: bool) ->

:param name: The property-name this bone has in its Skeleton (not the description!)
"""
# Handle compute on write
if self.compute:
match self.compute.interval.method:
case ComputeMethod.OnWrite:
skel.accessedValues[name] = self._compute(skel, name)

case ComputeMethod.Lifetime:
now = utils.utcNow()

last_update = \
skel.accessedValues.get(f"_viur_compute_{name}_") \
or skel.dbEntity.get(f"_viur_compute_{name}_")

if not last_update or last_update + self.compute.interval.lifetime < now:
skel.accessedValues[name] = self._compute(skel, name)
skel.dbEntity[f"_viur_compute_{name}_"] = now

case ComputeMethod.Once:
if name not in skel.dbEntity:
skel.accessedValues[name] = self._compute(skel, name)

# logging.debug(f"WRITE {name=} {skel.accessedValues=}")
# logging.debug(f"WRITE {name=} {skel.dbEntity=}")
phorward marked this conversation as resolved.
Show resolved Hide resolved

if name in skel.accessedValues:
newVal = skel.accessedValues[name]
if self.languages and self.multiple:
Expand Down Expand Up @@ -493,13 +561,51 @@ def unserialize(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) ->
"""
if name in skel.dbEntity:
loadVal = skel.dbEntity[name]
elif conf.get("viur.viur2import.blobsource") and any(
[x.startswith("%s." % name) for x in skel.dbEntity.keys()]):
elif (
# fixme: Remove this piece of sh*t at least with VIUR4
# We're importing from an old ViUR2 instance - there may only be keys prefixed with our name
conf.get("viur.viur2import.blobsource") and any([x.startswith("%s." % name) for x in skel.dbEntity])
# ... or computed
or self.compute
):
loadVal = None
else:
skel.accessedValues[name] = self.getDefaultValue(skel)
return False

# Is this value computed?
# In this case, check for configured compute method and if recomputation is required.
# Otherwise, the value from the DB is used as is.
if self.compute:
match self.compute.interval.method:
# Computation is bound to a lifetime?
case ComputeMethod.Lifetime:
now = utils.utcNow()

# check if lifetime exceeded
last_update = skel.dbEntity.get(f"_viur_compute_{name}_")
skel.accessedValues[f"_viur_compute_{name}_"] = last_update or now

# logging.debug(f"READ {name=} {skel.dbEntity=}")
# logging.debug(f"READ {name=} {skel.accessedValues=}")
phorward marked this conversation as resolved.
Show resolved Hide resolved

if not last_update or last_update + self.compute.interval.lifetime <= now:
# if so, recompute and refresh updated value
skel.accessedValues[name] = self._compute(skel, name)
return True

# Compute on every deserialization
case ComputeMethod.Always:
skel.accessedValues[name] = self._compute(skel, name)
return True

# Only compute once when loaded value is empty
case ComputeMethod.Once:
if loadVal is None:
skel.accessedValues[name] = self._compute(skel, name)
return True

# unserialize value to given config
if self.languages and self.multiple:
res = {}
if isinstance(loadVal, dict) and "_viurLanguageWrapper_" in loadVal:
Expand Down Expand Up @@ -571,6 +677,7 @@ def unserialize(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) ->
loadVal = loadVal[0]
if loadVal is not None:
res = self.singleValueUnserialize(loadVal)

skel.accessedValues[name] = res
return True

Expand Down Expand Up @@ -883,6 +990,26 @@ def iter_bone_value(
else:
yield None, None, value

def _compute(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str):
"""Performs the evaluation of a bone configured as compute"""

if "skel" not in inspect.signature(self.compute.fn).parameters:
# call without any arguments
ret = self.compute.fn()
else:
# otherwise, call with a clone of the skeleton
cloned_skel = skel.clone()
cloned_skel[name] = None # remove bone to avoid endless recursion
ret = self.compute.fn(skel=cloned_skel)
phorward marked this conversation as resolved.
Show resolved Hide resolved

if self.compute.raw:
return self.singleValueUnserialize(ret)
phorward marked this conversation as resolved.
Show resolved Hide resolved

if errors := self.fromClient(skel, name, {name: ret}):
raise ValueError(f"Computed value fromClient failed with {errors!r}")

return skel[name]

def structure(self) -> dict:
"""
Describes the bone and its settings as an JSON-serializable dict.
Expand Down Expand Up @@ -914,4 +1041,12 @@ def structure(self) -> dict:
}
else:
ret["multiple"] = self.multiple
if self.compute:
ret["compute"] = {
"method": self.compute.interval.method.name
}

if self.compute.interval.lifetime:
ret["compute"]["lifetime"] = str(self.compute.interval.lifetime)
ArneGudermann marked this conversation as resolved.
Show resolved Hide resolved

return ret
2 changes: 1 addition & 1 deletion core/skeleton.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ def txnUpdate(dbKey, mergeFrom):
oldUniqueValues = dbObj["viur"]["%s_uniqueIndexValue" % key]

# Merge the values from mergeFrom in
if key in skel.accessedValues:
if key in skel.accessedValues or bone.compute: # We can have a computed value on store
# bone.mergeFrom(skel.valuesCache, key, mergeFrom)
bone.serialize(skel, key, True)
elif key not in skel.dbEntity: # It has not been written and is not in the database
Expand Down