Skip to content

Commit

Permalink
feat: Add UidBone (#1131)
Browse files Browse the repository at this point in the history
Proposal for #1117

---------

Co-authored-by: Sven Eberth <[email protected]>
Co-authored-by: Jan Max Meyer <[email protected]>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 0c1da24 commit 4a756e5
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 7 deletions.
2 changes: 2 additions & 0 deletions src/viur/core/bones/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from .text import TextBone
from .treeleaf import TreeLeafBone
from .treenode import TreeNodeBone
from .uid import UidBone
from .uri import UriBone
from .user import UserBone

Expand Down Expand Up @@ -78,6 +79,7 @@
"TextBone",
"TreeLeafBone",
"TreeNodeBone",
"UidBone",
"UniqueLockMethod",
"UniqueValue",
"UserBone",
Expand Down
9 changes: 2 additions & 7 deletions src/viur/core/bones/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,8 @@ def __init__(
if compute:
if not isinstance(compute, Compute):
raise TypeError("compute must be an instanceof of Compute")

if not isinstance(compute.fn, t.Callable):
raise ValueError("'compute.fn' must be callable")
# When readOnly is None, handle flag automatically
if readOnly is None:
self.readOnly = True
Expand Down Expand Up @@ -918,12 +919,6 @@ def transact():
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 = {}
Expand Down
97 changes: 97 additions & 0 deletions src/viur/core/bones/uid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import time
import typing as t
from viur.core import db
from viur.core.bones.base import BaseBone, Compute, ComputeInterval, ComputeMethod, UniqueValue, UniqueLockMethod


def generate_number(db_key: db.Key) -> int:
"""
The generate_number method generates a leading number that is always unique per entry.
"""

def transact(_key: db.Key):
for i in range(3):
try:
if db_obj := db.Get(_key):
db_obj["count"] += 1
else:
db_obj = db.Entity(_key)
db_obj["count"] = 0
db.Put(db_obj)
break
except db.CollisionError: # recall the function
time.sleep(i + 1)
else:
raise ValueError("Can't set the Uid")
return db_obj["count"]

if db.IsInTransaction():
return transact(db_key)
else:
return db.RunInTransaction(transact, db_key)


def generate_uid(skel, bone):
db_key = db.Key("viur-uids", f"{skel.kindName}-{bone.name}-uid")
count_value = generate_number(db_key)
if bone.fillchar:
length_to_fill = bone.length - len(bone.pattern)
res = str(count_value).rjust(length_to_fill, bone.fillchar)
return bone.pattern.replace("*", res)
else:
return bone.pattern.replace("*", str(count_value))


class UidBone(BaseBone):
"""
The "UidBone" represents a data field that contains text values.
"""
type = "uid"

def __init__(
self,
*,
generate_fn: t.Callable = generate_uid,
fillchar: str = "*",
length: int = 13,
pattern: str | t.Callable | None = "*",
**kwargs
):
"""
Initializes a new UidBone.
:param generate_fn: The compute function to calculate the unique value,
:param fillchar The char that are filed in when the uid has not the length.
:param length: The length allowed for values of this bone.
:param pattern: The pattern for this Bone. "*" will be replaced with the uid value.
:param kwargs: Inherited arguments from the BaseBone.
"""

super().__init__(
compute=Compute(fn=generate_fn, interval=ComputeInterval(ComputeMethod.Once)),
unique=UniqueValue(UniqueLockMethod.SameValue, False, "Unique Value already in use"),
**kwargs
)
if self.multiple or self.languages:
raise ValueError("UidBone cannot be multiple or translated")

if not self.readOnly:
raise ValueError("UidBone must be read-only")

self.fillchar = str(fillchar)
self.length = length
if isinstance(pattern, t.Callable):
pattern = pattern()
self.pattern = str(pattern)
if self.pattern.count("*") != 1:
raise ValueError("Only one wildcard (*) is allowed and required in the pattern")
if len(self.fillchar) != 1:
raise ValueError("Only one char is allowed as fillchar")

def structure(self) -> dict:
ret = super().structure() | {
"fillchar": self.fillchar,
"length": self.length,
"pattern": self.pattern
}
return ret

0 comments on commit 4a756e5

Please sign in to comment.