rele icon indicating copy to clipboard operation
rele copied to clipboard

Add support for arbitrary deserializers

Open csaroff opened this issue 2 years ago • 9 comments

:tophat: What?

Allow custom deserializers other than the default json deserializer.

:thinking: Why?

Some of GCP's pub/sub notifications have messages that are simple strings which can't be deserialized as json.

For example, notifications that a record was inserted into the dicom store comes through with a string indicating the path to that record and nothing more.

:link: https://github.com/mercadona/rele/issues/229

csaroff avatar Sep 30 '22 02:09 csaroff

@andrewgy8 Any other thoughts?

csaroff avatar Oct 12 '22 06:10 csaroff

What do you think about changing the proposal to rely on an interface?

This would provide some benefits like:

  • Anyone could provide a particular deserializer implementation
  • deserializer.deserialize() could rise a DeserializingException and then avoid catching a generic exception
  • Avoid passing a lambda as a default argument
  • As we will be calling a method, we don't need to check if its a callable anymore
class DeserializingException(Exception)

class JsonDeserializer:
  def deserialize(message): 
    try:
      return json.loads(x.data.decode("utf-8"))
    except json.JSONDecodeError as e:
      raise DeserializingException(e) 

Then in the subscription's constructor would look like this:

def __init__(
        self,
        func,
        topic,
        prefix="",
        suffix="",
        filter_by=None,
        backend_filter_by=None,
        deserializer=JsonDeserializer,
    ):
        ...
        self._deserializer = deserializer
        ...

And finally Callback.__call__:

def __call__(self, message):
       ...
        try:
            data = self._subscription._deserializer.deserialize(message)
        except DeseralizingException as e:
          ...

jonasae avatar Oct 13 '22 17:10 jonasae

I like the exception handling where we catch a DeserializationException internally and ask users to raise that exception within their deserializer.

I don't really see the point of wrapping it in a class though. Seems slightly more complicated and I don't see what it adds.

csaroff avatar Oct 13 '22 19:10 csaroff

I like the exception handling where we catch a DeserializationException internally and ask users to raise that exception within their deserializer.

I don't really see the point of wrapping it in a class though. Seems slightly more complicated and I don't see what it adds.

Setting aside the benefits of using interfaces when injecting dependencies that might have different implementations and must fulfil a contract, It is slightly more complicated.

At least we are removing the responsibility of checking if the passed argument is a callable (the _init_deserializer method won't be needed).

In terms of lines of code, both solutions are similar ;-)

jonasae avatar Oct 14 '22 15:10 jonasae

Id say both solutions are identical. In the end they are callables. You could imagine passing a function, class or lambda in there would be perfectly feasible. And they would all work.

The other argument to keeping it a generic callable rather than a specific interface is the parity with the filter_by argument in the sub decorator. Which can also be any callable. https://mercadonarele.readthedocs.io/en/latest/guides/filters.html#filter-by-parameter

andrewgy8 avatar Oct 14 '22 15:10 andrewgy8

At least we are removing the responsibility of checking if the passed argument is a callable (the _init_deserializer method won't be needed).

Maybe I'm missing something, but are we?

If someone passed in a string or an int as a deserializer, wouldn't they just get an exception when we try to call their deserialize method?

>>> deserializer = 1
>>> deserializer.deserialize()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute 'deserialize'

csaroff avatar Oct 14 '22 18:10 csaroff

Id say both solutions are identical. In the end they are callables. You could imagine passing a function, class or lambda in there would be perfectly feasible. And they would all work.

The other argument to keeping it a generic callable rather than a specific interface is the parity with the filter_by argument in the sub decorator. Which can also be any callable. https://mercadonarele.readthedocs.io/en/latest/guides/filters.html#filter-by-parameter

Good point. For consistency sake, I'm buying it. But please, don't pass a lambda directly in the definition as a default callable.

jonasae avatar Oct 17 '22 13:10 jonasae

Maybe I'm missing something, but are we?

If someone passed in a string or an int as a deserializer, wouldn't they just get an exception when we try to call their deserialize method?

No, you are not. In fact, it's me who missed the parenthesis in the proposal. I meant to pass an instance, not a class name.

jonasae avatar Oct 17 '22 13:10 jonasae

Looks like master received a force-push last month. I rebased my branch so I think the changes should be good to go!

csaroff avatar Apr 19 '23 21:04 csaroff