Skip to content

Commit

Permalink
feat: ModelTopicConsumer.get_defaults will skip fields not defined …
Browse files Browse the repository at this point in the history
…on the model.

fix: `AbstractModelTestCase` creates now unique model per Test class.
  • Loading branch information
bodja committed Nov 18, 2024
1 parent 8c20b01 commit 1925bc8
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 18 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 0.5.8 (2024-11-19)
* `ModelTopicConsumer.get_defaults` will skip fields not defined on the model.

## 0.5.7 (2024-11-18)
* `@substitute_error` now shows error message of the original error.

Expand All @@ -19,7 +22,6 @@
* `ModelTopicConsumer.sync` returns now the results of the `update_or_create` method.
* Add `days_from_epoch_to_date` function to convert `io.debezium.time.Date` to python `datetime.date`.


## 0.4.1 (2024-09-17)
* Support string-based delete keys in DbzModelTopicConsumer

Expand Down
14 changes: 9 additions & 5 deletions django_kafka/tests/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db import connection
from django.db.models import Model
from django.db.models.base import ModelBase
from django.test import TestCase


Expand All @@ -9,11 +10,14 @@ class AbstractModelTestCase(TestCase):

@classmethod
def setUpClass(cls):
class TestModel(cls.abstract_model):
class Meta:
app_label = "django_kafka"

cls.model = TestModel
class Meta:
app_label = cls.__module__

cls.model = ModelBase(
f'__Test{cls.abstract_model.__name__}__',
(cls.abstract_model,),
{'__module__': cls.abstract_model.__module__, "Meta": Meta},
)

with connection.schema_editor() as editor:
editor.create_model(cls.model)
Expand Down
45 changes: 37 additions & 8 deletions django_kafka/tests/topic/test_model.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
from unittest import mock

from django.db.models import Model
from django.test import TestCase
from django.db import models

from django_kafka.connect.models import KafkaConnectSkipModel
from django_kafka.exceptions import DjangoKafkaError
from django_kafka.tests.models import AbstractModelTestCase
from django_kafka.topic.model import ModelTopicConsumer


class TestModelTopicConsumer(TestCase):
class ModelTopicConsumerTestCase(AbstractModelTestCase):
abstract_model = models.Model
model: type[KafkaConnectSkipModel]

def _get_model_topic_consumer(self):
class SomeModelTopicConsumer(ModelTopicConsumer):
name = "name"
model = Model
model = self.model

def get_lookup_kwargs(self, model, key, value) -> dict:
return {}
Expand All @@ -25,15 +28,21 @@ def is_deletion(self, *args, **kwargs):
def test_get_defaults(self):
topic_consumer = self._get_model_topic_consumer()

defaults = topic_consumer.get_defaults(model=Model, value={"name": 1})
class SomeModel(models.Model):
name = models.CharField()

class Meta:
abstract = True

defaults = topic_consumer.get_defaults(model=SomeModel, value={"name": 1})

self.assertEqual(defaults, {"name": 1})

def test_get_defaults__adds_kafka_skip(self):
topic_consumer = self._get_model_topic_consumer()

class KafkaConnectSkip(KafkaConnectSkipModel):
pass
name = models.CharField()

defaults = topic_consumer.get_defaults(
model=KafkaConnectSkip, value={"name": 1}
Expand All @@ -45,10 +54,16 @@ def test_get_defaults__calls_transform_attr(self):
topic_consumer = self._get_model_topic_consumer()
topic_consumer.transform_name = mock.Mock(return_value=("name_new", 2))

defaults = topic_consumer.get_defaults(model=Model, value={"name": 1})
class SomeModel(models.Model):
name = models.CharField()

class Meta:
abstract = True

defaults = topic_consumer.get_defaults(model=SomeModel, value={"name": 1})

topic_consumer.transform_name.assert_called_once_with(
topic_consumer.model,
SomeModel,
"name",
1,
)
Expand Down Expand Up @@ -129,3 +144,17 @@ def test_consume(self):
msg_key,
msg_value,
)

def test_model_has_field(self):
class BookModel(models.Model):
name = models.CharField(max_length=100)
author = models.CharField(max_length=100)

topic_consumer = self._get_model_topic_consumer()
topic_consumer.model = BookModel

fields = ("id", "name", "author")
for field in fields:
self.assertTrue(topic_consumer.model_has_field(BookModel, field))

self.assertFalse(topic_consumer.model_has_field(BookModel, "not_defined_field"))
13 changes: 9 additions & 4 deletions django_kafka/topic/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ def get_defaults(self, model, value) -> dict:
value fields can be transformed by defining a transform_{attr} method.
"""
defaults = {}
for attr, attr_value in value.items():
if transform_method := getattr(self, "transform_" + attr, None):
new_attr, new_value = transform_method(model, attr, attr_value)
for field_name, field_value in value.items():
if not self.model_has_field(model, field_name):
continue
if transform_method := getattr(self, "transform_" + field_name, None):
new_attr, new_value = transform_method(model, field_name, field_value)
defaults[new_attr] = new_value
else:
defaults[attr] = attr_value
defaults[field_name] = field_value

if issubclass(model, KafkaConnectSkipModel):
defaults["kafka_skip"] = True
Expand Down Expand Up @@ -61,6 +63,9 @@ def get_model(self, key, value) -> Type[Model]:
"Cannot obtain model: either define a default model or override get_model",
)

def model_has_field(self, model: Type[Model], field_name: str) -> bool:
return field_name in (field.column for field in model._meta.fields)

def consume(self, msg):
key = self.deserialize(msg.key(), MessageField.KEY, msg.headers())
value = self.deserialize(msg.value(), MessageField.VALUE, msg.headers())
Expand Down

0 comments on commit 1925bc8

Please sign in to comment.