Repository: influxdata/influxdb-client-python

Issue:
Add uint64 support to line protocol serializer
<!--

Thank you for suggesting an idea to improve this client. 

* Please add a :+1: or comment on a similar existing feature request instead of opening a new one.
    * https://github.com/influxdata/influxdb-client-python/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+is%3Aclosed+sort%3Aupdated-desc+label%3A%22enhancement%22+

-->

__Proposal:__
Add support for 64 bit unsigned integers to Point.to_line_protocol()

__Current behavior:__
InfluxDB 2.x supports 64 bit unsigned integers, which the [Line Protocol documentation](https://docs.influxdata.com/influxdb/v2.0/reference/syntax/line-protocol/#data-types-and-format) says should be suffixed with a "u". Point.to_line_protocol (via calling [_append_fields()](https://github.com/influxdata/influxdb-client-python/blob/4150bf712b416cb9c26173009640da1a03d34786/influxdb_client/client/write/point.py#L248-L249) only appends an "i" for integer field values. This works fine for positive integers smaller than max signed int32, but for positive integers between max signed int32 and max unsigned int64, InfluxDB 2.x will reject as "value out of range."

__Desired behavior:__
Perhaps the simplest and backward compatible solution would be to use a "u" suffix for integer values > max signed int32. I don't have a good sense, though, of whether this would behave expectedly on InfluxDB 1.x when uint support is not enabled.

__Alternatives considered:__
1. Append a "u" for all positive integers, regardless of size. I don't know for sure, but I'd worry that influx will have typing problems if you mix unsigned and signed integers for the same field.
2. Write my own line protocol serializer for my specific use case so that I can have deliberate control of the suffixes based on my specific definitions of my measurements.

__Use case:__
Metrics collector that receives a stream of incoming data, translates it to line protocol, and sends it to a local Telegraf socket listener for ingestion into a remote InfluxDB.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82c701b..244bb58 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ### Features
 1. [#536](https://github.com/influxdata/influxdb-client-python/pull/536): Query to `CSV` skip empty lines
+1. [#538](https://github.com/influxdata/influxdb-client-python/pull/538): Configure types of `integer` fields when initializing `Point` from `dict` structure
 
 ## 1.35.0 [2022-12-01]
 
diff --git a/influxdb_client/client/write/point.py b/influxdb_client/client/write/point.py
index 3bee403..60ce7c4 100644
--- a/influxdb_client/client/write/point.py
+++ b/influxdb_client/client/write/point.py
@@ -99,6 +99,26 @@ class Point(object):
                                         record_tag_keys=["location", "version"],
                                         record_field_keys=["pressure", "temperature"])
 
+        Int Types:
+            The following example shows how to configure the types of integers fields.
+            It is useful when you want to serialize integers always as ``float`` to avoid ``field type conflict``
+            or use ``unsigned 64-bit integer`` as the type for serialization.
+
+            .. code-block:: python
+
+                # Use custom dictionary structure
+                dict_structure = {
+                    "measurement": "h2o_feet",
+                    "tags": {"location": "coyote_creek"},
+                    "fields": {
+                        "water_level": 1.0,
+                        "some_counter": 108913123234
+                    },
+                    "time": 1
+                }
+
+                point = Point.from_dict(dict_structure, field_types={"some_counter": "uint"})
+
         :param dictionary: dictionary for serialize into data Point
         :param write_precision: sets the precision for the supplied time values
         :key record_measurement_key: key of dictionary with specified measurement
@@ -106,8 +126,14 @@ class Point(object):
         :key record_time_key: key of dictionary with specified timestamp
         :key record_tag_keys: list of dictionary keys to use as a tag
         :key record_field_keys: list of dictionary keys to use as a field
+        :key field_types: optional dictionary to specify types of serialized fields. Currently, is supported customization for integer types.
+                          Possible integers types:
+                            - ``int`` - serialize integers as "**Signed 64-bit integers**" - ``9223372036854775807i`` (default behaviour)
+                            - ``uint`` - serialize integers as "**Unsigned 64-bit integers**" - ``9223372036854775807u``
+                            - ``float`` - serialize integers as "**IEEE-754 64-bit floating-point numbers**". Useful for unify number types in your pipeline to avoid field type conflict - ``9223372036854775807``
+                          The ``field_types`` can be also specified as part of incoming dictionary. For more info see an example above.
         :return: new data point
-        """
+        """  # noqa: E501
         measurement_ = kwargs.get('record_measurement_name', None)
         if measurement_ is None:
             measurement_ = dictionary[kwargs.get('record_measurement_key', 'measurement')]
@@ -134,6 +160,19 @@ class Point(object):
         record_time_key = kwargs.get('record_time_key', 'time')
         if record_time_key in dictionary:
             point.time(dictionary[record_time_key], write_precision=write_precision)
+
+        _field_types = kwargs.get('field_types', {})
+        if 'field_types' in dictionary:
+            _field_types = dictionary['field_types']
+        # Map API fields types to Line Protocol types postfix:
+        # - int: 'i'
+        # - uint: 'u'
+        # - float: ''
+        point._field_types = dict(map(
+            lambda item: (item[0], 'i' if item[1] == 'int' else 'u' if item[1] == 'uint' else ''),
+            _field_types.items()
+        ))
+
         return point
 
     def __init__(self, measurement_name):
@@ -143,6 +182,7 @@ class Point(object):
         self._name = measurement_name
         self._time = None
         self._write_precision = DEFAULT_WRITE_PRECISION
+        self._field_types = {}
 
     def time(self, time, write_precision=DEFAULT_WRITE_PRECISION):
         """
@@ -190,7 +230,7 @@ The output Line protocol will be interpret as a comment by InfluxDB. For more in
 """
             warnings.warn(message, SyntaxWarning)
         _tags = _append_tags(self._tags)
-        _fields = _append_fields(self._fields)
+        _fields = _append_fields(self._fields, self._field_types)
         if not _fields:
             return ""
         _time = _append_time(self._time, self._write_precision if precision is None else precision)
@@ -227,7 +267,7 @@ def _append_tags(tags):
     return f"{',' if _return else ''}{','.join(_return)} "
 
 
-def _append_fields(fields):
+def _append_fields(fields, field_types):
     _return = []
 
     for field, value in sorted(fields.items()):
@@ -246,7 +286,8 @@ def _append_fields(fields):
                 s = s[:-2]
             _return.append(f'{_escape_key(field)}={s}')
         elif (isinstance(value, int) or _np_is_subtype(value, 'int')) and not isinstance(value, bool):
-            _return.append(f'{_escape_key(field)}={str(value)}i')
+            _type = field_types.get(field, "i")
+            _return.append(f'{_escape_key(field)}={str(value)}{_type}')
         elif isinstance(value, bool):
             _return.append(f'{_escape_key(field)}={str(value).lower()}')
         elif isinstance(value, str):


Test patch:
diff --git a/tests/test_point.py b/tests/test_point.py
index 51552f0..cee602d 100644
--- a/tests/test_point.py
+++ b/tests/test_point.py
@@ -482,6 +482,63 @@ class PointTest(unittest.TestCase):
                                 record_field_keys=["pressure", "temperature"])
         self.assertEqual("sensor_pt859,location=warehouse_125 pressure=125i", point.to_line_protocol())
 
+    def test_from_dictionary_uint(self):
+        dict_structure = {
+            "measurement": "h2o_feet",
+            "tags": {"location": "coyote_creek"},
+            "fields": {
+                "water_level": 1.0,
+                "some_counter": 108913123234
+            },
+            "time": 1
+        }
+        point = Point.from_dict(dict_structure, field_types={"some_counter": "uint"})
+        self.assertEqual("h2o_feet,location=coyote_creek some_counter=108913123234u,water_level=1 1",
+                         point.to_line_protocol())
+
+    def test_from_dictionary_int(self):
+        dict_structure = {
+            "measurement": "h2o_feet",
+            "tags": {"location": "coyote_creek"},
+            "fields": {
+                "water_level": 1.0,
+                "some_counter": 108913123234
+            },
+            "time": 1
+        }
+        point = Point.from_dict(dict_structure, field_types={"some_counter": "int"})
+        self.assertEqual("h2o_feet,location=coyote_creek some_counter=108913123234i,water_level=1 1",
+                         point.to_line_protocol())
+
+    def test_from_dictionary_float(self):
+        dict_structure = {
+            "measurement": "h2o_feet",
+            "tags": {"location": "coyote_creek"},
+            "fields": {
+                "water_level": 1.0,
+                "some_counter": 108913123234
+            },
+            "time": 1
+        }
+        point = Point.from_dict(dict_structure, field_types={"some_counter": "float"})
+        self.assertEqual("h2o_feet,location=coyote_creek some_counter=108913123234,water_level=1 1",
+                         point.to_line_protocol())
+
+    def test_from_dictionary_float_from_dict(self):
+        dict_structure = {
+            "measurement": "h2o_feet",
+            "tags": {"location": "coyote_creek"},
+            "fields": {
+                "water_level": 1.0,
+                "some_counter": 108913123234
+            },
+            "field_types": {"some_counter": "float"},
+            "time": 1
+        }
+        point = Point.from_dict(dict_structure)
+        self.assertEqual("h2o_feet,location=coyote_creek some_counter=108913123234,water_level=1 1",
+                         point.to_line_protocol())
+
     def test_static_measurement_name(self):
         dictionary = {
             "name": "sensor_pt859",