From 6e5557ad3aeea47662e95a5785c7dd25a075fda9 Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Thu, 11 Aug 2022 18:33:31 +0300 Subject: [PATCH 1/6] make primary key programmable Signed-off-by: wiseaidev --- aredis_om/model/model.py | 6 ++++-- tests/test_hash_model.py | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 46857106..7f7ac317 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -1105,8 +1105,8 @@ class Config: extra = "allow" def __init__(__pydantic_self__, **data: Any) -> None: - super().__init__(**data) __pydantic_self__.validate_primary_key() + super().__init__(**data) def __lt__(self, other): """Default sort: compare primary key of models.""" @@ -1166,7 +1166,9 @@ def validate_primary_key(cls): primary_keys += 1 if primary_keys == 0: raise RedisModelError("You must define a primary key for the model") - elif primary_keys > 1: + elif primary_keys == 2: + cls.__fields__.pop('pk') + elif primary_keys > 2: raise RedisModelError("You must define only one primary key for a model") @classmethod diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index 84a05086..9d0e5590 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -46,7 +46,7 @@ class Order(BaseHashModel): created_on: datetime.datetime class Member(BaseHashModel): - id: int = Field(index=True) + id: int = Field(index=True, primary_key=True) first_name: str = Field(index=True) last_name: str = Field(index=True) email: str = Field(index=True) @@ -445,7 +445,7 @@ async def test_saves_model_and_creates_pk(m): # Save a model instance to Redis await member.save() - member2 = await m.Member.get(member.pk) + member2 = await m.Member.get(pk=member.id) assert member2 == member @@ -495,7 +495,7 @@ async def test_delete(m): ) await member.save() - response = await m.Member.delete(member.pk) + response = await m.Member.delete(pk=member.id) assert response == 1 @@ -588,8 +588,8 @@ async def test_saves_many(m): result = await m.Member.add(members) assert result == [member1, member2] - assert await m.Member.get(pk=member1.pk) == member1 - assert await m.Member.get(pk=member2.pk) == member2 + assert await m.Member.get(pk=member1.id) == member1 + assert await m.Member.get(pk=member2.id) == member2 @py_test_mark_asyncio @@ -618,14 +618,14 @@ async def test_delete_many(m): result = await m.Member.delete_many(members) assert result == 2 with pytest.raises(NotFoundError): - await m.Member.get(pk=member1.pk) + await m.Member.get(pk=member1.id) @py_test_mark_asyncio async def test_updates_a_model(members, m): member1, member2, member3 = members await member1.update(last_name="Smith") - member = await m.Member.get(member1.pk) + member = await m.Member.get(member1.id) assert member.last_name == "Smith" From 2be329f66d49d062d4cf2b4447cb5127b1a85fc5 Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Thu, 11 Aug 2022 19:40:50 +0300 Subject: [PATCH 2/6] get primary key field using the `key` method Signed-off-by: wiseaidev --- aredis_om/model/model.py | 6 +++--- tests/test_hash_model.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 7f7ac317..9952c998 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -1110,7 +1110,7 @@ def __init__(__pydantic_self__, **data: Any) -> None: def __lt__(self, other): """Default sort: compare primary key of models.""" - return self.pk < other.pk + return self.key() < other.key() def key(self): """Return the Redis key for this model.""" @@ -1149,7 +1149,7 @@ async def expire( db = self._get_db(pipeline) # TODO: Wrap any Redis response errors in a custom exception? - await db.expire(self.make_primary_key(self.pk), num_seconds) + await db.expire(self.make_primary_key(self.key()), num_seconds) @validator("pk", always=True, allow_reuse=True) def validate_pk(cls, v): @@ -1274,7 +1274,7 @@ async def delete_many( db = cls._get_db(pipeline) for chunk in ichunked(models, 100): - pks = [cls.make_primary_key(model.pk) for model in chunk] + pks = [cls.make_primary_key(model.key()) for model in chunk] await cls._delete(db, *pks) return len(models) diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index 9d0e5590..4f9ffff9 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -618,7 +618,7 @@ async def test_delete_many(m): result = await m.Member.delete_many(members) assert result == 2 with pytest.raises(NotFoundError): - await m.Member.get(pk=member1.id) + await m.Member.get(pk=member1.key()) @py_test_mark_asyncio From a8f1afe2986252e54768d65c9c52b3a948dc2dea Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Thu, 11 Aug 2022 20:09:18 +0300 Subject: [PATCH 3/6] adjust delete_many & expire methods Signed-off-by: wiseaidev --- aredis_om/model/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 9952c998..96d0dc1d 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -1149,7 +1149,7 @@ async def expire( db = self._get_db(pipeline) # TODO: Wrap any Redis response errors in a custom exception? - await db.expire(self.make_primary_key(self.key()), num_seconds) + await db.expire(self.key(), num_seconds) @validator("pk", always=True, allow_reuse=True) def validate_pk(cls, v): @@ -1274,7 +1274,7 @@ async def delete_many( db = cls._get_db(pipeline) for chunk in ichunked(models, 100): - pks = [cls.make_primary_key(model.key()) for model in chunk] + pks = [model.key() for model in chunk] await cls._delete(db, *pks) return len(models) From 5e5e117d8511da286176187b18657d8cc2885409 Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Thu, 11 Aug 2022 23:37:47 +0300 Subject: [PATCH 4/6] fix query for int primary key Signed-off-by: wiseaidev --- aredis_om/model/model.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 96d0dc1d..13560917 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -563,7 +563,10 @@ def resolve_value( separator_char, ) return "" - if separator_char in value: + if isinstance(value, int): + # This if will hit only if the field is prinary key of type int + result = f"@{field_name}:[{value} {value}]" + elif separator_char in value: # The value contains the TAG field separator. We can work # around this by breaking apart the values and unioning them # with multiple field:{} queries. From 0452bfd6ad2d649c081fc25cbd9075c26b32aa22 Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Thu, 11 Aug 2022 23:53:52 +0300 Subject: [PATCH 5/6] fix grammar Signed-off-by: wiseaidev --- aredis_om/model/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aredis_om/model/model.py b/aredis_om/model/model.py index 13560917..a6651517 100644 --- a/aredis_om/model/model.py +++ b/aredis_om/model/model.py @@ -564,7 +564,7 @@ def resolve_value( ) return "" if isinstance(value, int): - # This if will hit only if the field is prinary key of type int + # This if will hit only if the field is a primary key of type int result = f"@{field_name}:[{value} {value}]" elif separator_char in value: # The value contains the TAG field separator. We can work From 80001db5fd73f3fa76d5d65d96947c10fa95ab9f Mon Sep 17 00:00:00 2001 From: wiseaidev Date: Fri, 12 Aug 2022 09:19:53 +0300 Subject: [PATCH 6/6] add unit tests Signed-off-by: wiseaidev --- tests/test_hash_model.py | 56 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_hash_model.py b/tests/test_hash_model.py index 4f9ffff9..b2bd30ae 100644 --- a/tests/test_hash_model.py +++ b/tests/test_hash_model.py @@ -681,3 +681,59 @@ class Address(m.BaseHashModel): Address.redisearch_schema() == f"ON HASH PREFIX 1 {key_prefix} SCHEMA pk TAG SEPARATOR | a_string TAG SEPARATOR | a_full_text_string TAG SEPARATOR | a_full_text_string AS a_full_text_string_fts TEXT an_integer NUMERIC SORTABLE a_float NUMERIC" ) + + +@py_test_mark_asyncio +async def test_primary_key_model_error(m): + + class Customer(m.BaseHashModel): + id: int = Field(primary_key=True, index=True) + first_name: str = Field(primary_key=True, index=True) + last_name: str + bio: Optional[str] + + await Migrator().run() + + with pytest.raises(RedisModelError, match="You must define only one primary key for a model"): + _ = Customer( + id=0, + first_name="Mahmoud", + last_name="Harmouch", + bio="Python developer, wanna work at Redis, Inc." + ) + + +@py_test_mark_asyncio +async def test_primary_pk_exists(m): + + class Customer1(m.BaseHashModel): + id: int + first_name: str + last_name: str + bio: Optional[str] + + class Customer2(m.BaseHashModel): + id: int = Field(primary_key=True, index=True) + first_name: str + last_name: str + bio: Optional[str] + + await Migrator().run() + + customer = Customer1( + id=0, + first_name="Mahmoud", + last_name="Harmouch", + bio="Python developer, wanna work at Redis, Inc." + ) + + assert 'pk' in customer.__fields__ + + customer = Customer2( + id=1, + first_name="Kim", + last_name="Brookins", + bio="This is member 2 who can be quite anxious until you get to know them.", + ) + + assert 'pk' not in customer.__fields__