Files
flucto-heisskleber/docs/serialization.md
Felix Weiler 98099f5b00 Refactor heisskleber core, remove synchronous implementations (#156)
* #129 AsyncTcpSource enhancements
- retry connection on startup (behavior is configurable)
- reconnect if data receiving fails (EOF received)
- add Python logging
- add unit tests

* remove syncronous implementations.

* WIP: Refactor packer/unpacker

* Refactor type hints and topic handling in console sink.

* Remove comma from tcp config enum definitions

* Remove references to deleted synchronous classes.

* Hopefully stable interface for Packer and Unpacker.

* WIP: Working with protocols and generics

* Finalized Sink, Source definition.

* Rename mqtt source and sink files

* Rename mqtt publisher and subscriber.

* Fix start function to async.

* Update documentation.

* Remove recursion from udp source.

* rename unpack to unpacker, stay consistent.

* Renaming in tests.

* Make MqttSource generic.

* Configure pyproject.toml to move to uv

* Add nox support.

* Update documentation with myst-parser and sphinx.

* Mess with autogeneration of __call__ signatures.

* Add dynamic versioning to hatch

* Asyncio wrapper for pyserial.

* Add docstrings for serial sink and source.

* Refactor config handling (#171)

* Removes deprecated "verbose" and "print_std" parameters

* Adds class methods for config generation from dictionary or file (yaml or json at this point)

* Run-time type checking via __post_init__() function

* Add serial dependency.

* WIP

* Move broker to bin/

* Update docs.

* WIP: Need to update docstrings to make ruff happy.

* Move source files to src/

* Fix tests for TcpSource.

* WIP: Remove old tests.

* Fix docstrings in mqtt classes.

* Make default tcp unpacker json_unpacker.

* No failed tests if there are no tests

* Update test pipeline

* Update ruff pre-commit

* Updated ruff formatting

* Format bin/

* Fix type hints

* No type checking

* Make stop() async

* Only test on ubuntu for now

* Don't be so strict about sphinx warnings.

* Rename TestConf for pytest naming compability.

* Install package in editable mode for ci tests.

* Update dependencies for docs generation.

* Add keepalive and will to mqtt, fixes #112.

* Update readme to reflect changes in usage.

* Requested fixes for console adapters.

* Raise correct errors in unpacker and packer.

* Correct logger name for mqtt sink.

* Add config options for stopbits and parity to Serial.

* Remove exception logging call from yaml parser.

* Add comments to clear up very implicit test.

* Rename Sink -> Sender, Source -> Receiver.

* Rename sink and source in tests.

* Fix tests.

---------

Co-authored-by: Adrian Weiler <a.weiler@aldea.de>
2024-12-09 19:32:34 +01:00

2.8 KiB

Serialization

Implementing a custom Packer

The packer class is defined in heisskleber.core.packer.py as a Protocol see PEP 544.

    T = TypeVar("T", contravariant=True)

    class Packer(Protocol[T]):
        def __call__(self, data: T) -> bytes:
            pass

Users can create custom Packer classes with variable input data, either as callable classes, subclasses of the packer class or functions. Please note, that to satisfy type checking engines, the argument must be named data, but being Python, it's obviously not enforced at runtime. The AsyncSink's type is defined by the concrete packer implementation. So if your Packer packs strings to bytes, the AsyncSink will be of type AsyncSink[str], indicating that the send function takes strings only, see example below:

    from heisskleber import MqttSink, MqttConf

    def string_packer(data: str) -> bytes:
        return data.encode("ascii")

    async def main():
        sink = MqttSink(MqttConf(), packer = string_packer)
        await sink.send("Hi there!") # This is fine
        await sink.send({"data": 3.14}) # Type checker will complain

Heisskleber comes with default packers, such as the JSON_Packer, which can be importet as json_packer from heisskleber.core and is the default value for most Sinks.

Implementing a custom Unpacker

The unpacker's responsibility is creating usable data from serialized byte strings. This may be a serialized json string which is unpacked into a dictionary, but could be anything the user defines. In heisskleber.core.unpacker.py the Unpacker Protocol is defined.

    class Unpacker(Protocol[T]):
        def __call__(self, payload: bytes) -> tuple[T, dict[str, Any]]:
            pass

Here, the payload is fixed to be of type bytes and the return type is a combination of a user-defined data type and a dictionary of meta-data.

.. note::
Please Note: The extra dictionary may be updated by the Source, e.g. the MqttSource will add a "topic" field, received from the mqtt node.

The receive function of an AsyncSource object will have its return type informed by the signature of the unpacker.

    from heisskleber import MqttSource, MqttConf
    import time

    def csv_unpacker(payload: bytes) -> tuple[list[str], dict[str, Any]]:
        # Unpack a utf-8 encoded csv string, such as b'1,42,3.14,100.0' to [1.0, 42.0, 3.14, 100.0]
        # Adds some exemplary meta data
        return [float(chunk) for chunk in payload.decode().split(",")], {"processed_at": time.time()}

    async def main():
        sub = MqttSource(MqttConf, unpacker = csv_unpacker)
        data, extra = await sub.receive()
        assert isinstance(data, list[str]) # passes

Error handling

To be implemented...