redis-om-python icon indicating copy to clipboard operation
redis-om-python copied to clipboard

PrimaryKeyCreator, no parameter are passed to the create_pk method

Open LeMoussel opened this issue 3 years ago • 7 comments

I want to make an MurmurHash class to create a primary key for a new model instance. I did this class that adheres to the PrimaryKeyCreator protocol:

# https://github.com/hajimes/mmh3
import mmh3

class Mmh3PrimaryKey:
    """
    A client-side generated primary key that follows the MurmurHash (MurmurHash3) spec.
    https://en.wikipedia.org/wiki/MurmurHash
    """

    @staticmethod
    def create_pk(self, *args, **kwargs) -> str:
        return str('some argument in args / kwargs')

and set Meta primary_key_creator_cls like this:

class ErrorServer(HashModel):
    local_hostname: str
    class Meta:
        primary_key_creator_cls = Mmh3PrimaryKey

But when I instantiate ErrorServerclass, no parameter (len of *args, **kwargs == 0) are passed to create_pk

es = ErrorServer(local_hostname='my_hostname', param2='Test')

LeMoussel avatar Feb 16 '22 12:02 LeMoussel

Thanks, I'll have a go at looking into this, I see where primary_key_creator_cls is handled here https://github.com/redis/redis-om-python/blob/main/aredis_om/model/model.py#L856 but will need to figure out why no params are passed into create_pk.

simonprickett avatar Feb 16 '22 12:02 simonprickett

If it helps, I found that in model.py, validate_pk(cls, v) call create_pk() without any arguments. https://github.com/redis/redis-om-python/blob/main/aredis_om/model/model.py#L1136

    @validator("pk", always=True, allow_reuse=True)
    def validate_pk(cls, v):
        if not v:
            v = cls._meta.primary_key_creator_cls().create_pk()
        return v

It's call in __init__(...) with **data euqual to kwargs https://github.com/redis/redis-om-python/blob/main/aredis_om/model/model.py#L1104

    def __init__(__pydantic_self__, **data: Any) -> None:
        super().__init__(**data)

See in Pydantic.validators, validators are “class methods”, and full signature here is equal to (cls, value, values, config, field). In other word, def validate_pk(cls, v, values, **kwargs): is euqal to config, field.

LeMoussel avatar Feb 16 '22 13:02 LeMoussel

I am also interested in changing the default primary key and noticed the same behavior.

I also noticed that the custom class FieldInfo (inherited from Pydantic FieldInfo) has the attribute "primary_key" which is used in the RedisModel class to initiate the default field pk as primary key.

But apparently this attribute cannot be used elsewhere:

Class Customer(HashModel):
    email: EmailStr = Field(index=True, primary_key=True)

--
File ~/.local/lib/python3.9/site-packages/redis_om/model/model.py:1158, in RedisModel.validate_primary_key(cls)
   1156     raise RedisModelError("You must define a primary key for the model")
   1157 elif primary_keys > 1:
-> 1158     raise RedisModelError("You must define only one primary key for a model")

RedisModelError: You must define only one primary key for a model

It would be great to use this parameter like SQL Alchemy ORM to change the default behavior. If we use this parameter manually in another field (declared one time in the model), the latter must have precedence over the pk field. What do you think?

nicolas-rdgs avatar May 29 '22 19:05 nicolas-rdgs

I was having the same issue and until this is fixed you can work around it like this:

class ErrorServer(HashModel):
    local_hostname: str
es = ErrorServer(pk=Mmh3PrimaryKey.create_pk(some_arg), local_hostname='my_hostname', param2='Test') 

Mihaylov93 avatar May 31 '22 11:05 Mihaylov93

I also need to change the default pk. I use this workaround. Works fine for me.

from pydantic import root_validator

class Foo(JsonModel):
    name: str

    @root_validator(pre=True)
    def overwrite_pk(cls, values: dict[str, Any]):
        def _overwrite_pk(*args, **kwargs):
            return f"{args[0]}.{args[1]}"

        values["pk"] = _overwrite_pk(values["name"])

        return values
foo = Foo(name="test")

print(foo.pk) # -> "test"

DasLukas avatar Sep 11 '22 10:09 DasLukas

After reading the source code, I found that you can either do

    def __init__(self, **data):
        super().__init__(**data)
        self.pk = Mmh3PrimaryKey(...)

or for other use-cases, do:

class Domain(aredis_om.HashModel):
    email: str = aredis_om.Field(primary_key=True)

☝️ this works because here we pop the key from the RedisModel based on the primary_key field that we set here

We should mention it in the docs to help others looking for the same answer.

balazser avatar Apr 02 '23 12:04 balazser

For other newbies to Redis-OM: I struggled with this. As I (mis-?) understood it, Redis looked-up on a key which defined a hard-to-compute object. Here's a simple example without any searching, just to store and retrieve an object.

My model:

class SomeModel(JsonModel):
  def __init__(self, **data):
          super().__init__(**data)
          # don't want a random ULID key, wanna lookup on this pk
          primary_key_pattern = "{start}:{stop}:{sensors}"
          self.pk = primary_key_pattern.format(**data)
  start: datetime
  stop: datetime
  sensor: list
  result: Optional [str] # this is hard to compute!

An example of using it:

start = dt(2020, 1, 1)
stop = dt(2021, 1, 1)
q1 = SomeModel(
        start=start,
        stop=stop,
        sensors=["Import"],
    )
# Do the hard thang, then save it for laters
q1.result = "hard to compute!"
q1.expire(2)
q1.save()

# Soon after, I need it again
q1_again = SomeModel(
        devices=["test"],
        start=start,
        stop=stop,
        sensors=["Import"]
    )
# phew, don't need to compute
assert SomeModel.get(q1_again.pk).result == "hard to compute!"
sleep(5)
# after timeout, this will raise NotFoundError
SomeModel.get(q1_again.pk)

brettbeeson avatar Jul 16 '23 04:07 brettbeeson