gusutabopb / aioinflux

Asynchronous Python client for InfluxDB
MIT License
159 stars 31 forks source link

Dynamically Add Extra Tags to User Object #27

Open iwoloschin opened 5 years ago

iwoloschin commented 5 years ago

Is it possible to add extra tags to a user object directly from the the client's .write() method? I'd like to be able to add extra tags when calling client.write(data, **extra_tags), but this appears to only be possible with a dictionary, not a user defined class.

iwoloschin commented 5 years ago

It appears I can potentially just "redecorate" if needed, after checking if I need extra tags, but before using my custom object. This should work for me, but going to leave this open as this seems like potentially a terrible idea that could easily break :).

gusutabopb commented 4 years ago

Yeah, it's not really possible when writing user-defined classes (or raw lineprotocol). It's only possible when writing dataframes or mappings (dictionaries). For dataframes it's possible because effectively the equivalent of the to_lineprotocol method of user-defined classes is generated on the fly for every write. As for mappings it works because the serialization uses a totally different (more naive, slower) approach than the one for user-defined classes.

See this for more details: https://github.com/gusutabopb/aioinflux/blob/0bc49c497d2cacb3381d12ab0ff20f15c8e89d8a/aioinflux/serialization/__init__.py#L9

I think the best thing I could do is update .write docstring and/or raise a warning about it. Currently this is not documented AND ignored silently which is not a very nice API 😅

For future reference, here's a workaround you could try:

Assuming you have a user-defined class like below:

from aioinflux import *
from typing import NamedTuple
import time

@lineprotocol
class Foo(NamedTuple):
    time: TIMEINT
    x: INT
    y: INT

print(Foo(time.time_ns(), 1, 2).to_lineprotocol())
# b'Foo x=1i,y=2i 1591019472699583000'

You can "redecorate" your class by getting the arguments used in the first decoration from Foo.to_lineprotocol.opts (this is actually not documented, but perhaps should), modifying it to whatever you want and then using the decorator again (but as a regular function):

from copy import deepcopy
opts = deepcopy(Foo.to_lineprotocol.opts)  # deep copy to avoid modyfing the original `extra_tags` dict)
opts['extra_tags'].update({'foo': 'boo'})
print(lineprotocol(Foo, **opts)(1, 2, 3).to_lineprotocol())
b'Foo,foo=boo x=2i,y=3i 1591020382248841000'

Alternatively, in case you know then tag field key beforehand, you could make that a None value by default and use rm_none=True when decorating the function (it makes to_lineprotocol a bit slower though).

@lineprotocol(rm_none=True)
class Foo(NamedTuple):
    time: TIMEINT
    x: INT
    mynullabletag: TAG = None

print(Foo(time.time_ns(), 1).to_lineprotocol())
# b'Foo x=1i 1591020531182752000'

print(Foo(time.time_ns(), 1, 'mytag').to_lineprotocol())
# b'Foo,mynullabletag=mytag x=1i 1591020544735611000'