influxdb3-python 0.18.0__tar.gz → 0.19.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/PKG-INFO +28 -4
  2. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/README.md +25 -1
  3. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/PKG-INFO +28 -4
  4. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/version.py +1 -1
  5. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/_base.py +2 -1
  6. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/dataframe_serializer.py +7 -6
  7. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/point.py +50 -4
  8. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/polars_dataframe_serializer.py +33 -22
  9. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write_api.py +14 -2
  10. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/write_precision.py +5 -5
  11. influxdb3_python-0.19.0/pyproject.toml +3 -0
  12. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/setup.py +2 -2
  13. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_api_client.py +0 -2
  14. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_dataframe_serializer.py +24 -0
  15. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_influxdb_client_3.py +63 -5
  16. influxdb3_python-0.19.0/tests/test_point.py +67 -0
  17. influxdb3_python-0.19.0/tests/test_polars.py +213 -0
  18. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_query.py +4 -3
  19. influxdb3_python-0.18.0/pyproject.toml +0 -3
  20. influxdb3_python-0.18.0/tests/test_point.py +0 -14
  21. influxdb3_python-0.18.0/tests/test_polars.py +0 -99
  22. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/__init__.py +0 -0
  23. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/basic_ssl_example.py +0 -0
  24. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/batching_example.py +0 -0
  25. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/cloud_dedicated_query.py +0 -0
  26. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/cloud_dedicated_write.py +0 -0
  27. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/config.py +0 -0
  28. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/flight_options_example.py +0 -0
  29. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/handle_http_error.py +0 -0
  30. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/handle_query_error.py +0 -0
  31. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/pandas_write.py +0 -0
  32. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/query_async.py +0 -0
  33. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/query_type.py +0 -0
  34. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/query_with_middleware.py +0 -0
  35. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/Examples/timeouts.py +0 -0
  36. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/LICENSE +0 -0
  37. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/SOURCES.txt +0 -0
  38. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/dependency_links.txt +0 -0
  39. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/requires.txt +0 -0
  40. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/top_level.txt +0 -0
  41. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/__init__.py +0 -0
  42. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/exceptions/__init__.py +0 -0
  43. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/exceptions/exceptions.py +0 -0
  44. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/py.typed +0 -0
  45. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/query/__init__.py +0 -0
  46. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/query/query_api.py +0 -0
  47. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/read_file.py +0 -0
  48. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/__init__.py +0 -0
  49. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/__init__.py +0 -0
  50. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/api_client.py +0 -0
  51. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/rest.py +0 -0
  52. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/__init__.py +0 -0
  53. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/influxdb_client.py +0 -0
  54. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/logging_handler.py +0 -0
  55. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/__init__.py +0 -0
  56. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/date_utils.py +0 -0
  57. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/date_utils_pandas.py +0 -0
  58. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/helpers.py +0 -0
  59. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/multiprocessing_helper.py +0 -0
  60. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/warnings.py +0 -0
  61. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/__init__.py +0 -0
  62. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/retry.py +0 -0
  63. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/configuration.py +0 -0
  64. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/__init__.py +0 -0
  65. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/write_precision_converter.py +0 -0
  66. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/extras.py +0 -0
  67. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/rest.py +0 -0
  68. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/__init__.py +0 -0
  69. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/_base_service.py +0 -0
  70. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/signin_service.py +0 -0
  71. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/signout_service.py +0 -0
  72. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/write_service.py +0 -0
  73. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/setup.cfg +0 -0
  74. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_date_helper.py +0 -0
  75. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_deep_merge.py +0 -0
  76. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_flush.py +0 -0
  77. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_influxdb_client_3_integration.py +0 -0
  78. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_merge_options.py +0 -0
  79. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_write_file.py +0 -0
  80. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_write_local_server.py +0 -0
  81. {influxdb3_python-0.18.0 → influxdb3_python-0.19.0}/tests/test_write_precision_converter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdb3-python
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Community Python client for InfluxDB 3.0
5
5
  Home-page: https://github.com/InfluxCommunity/influxdb3-python
6
6
  Author: InfluxData
@@ -8,13 +8,13 @@ Author-email: contact@influxdata.com
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3.8
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
- Requires-Python: >=3.8
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: reactivex>=4.0.4
@@ -98,7 +98,7 @@ pip install influxdb3-python
98
98
 
99
99
  Note: This does not include Pandas support. If you would like to use key features such as `to_pandas()` and `write_file()` you will need to install `pandas` separately.
100
100
 
101
- *Note: Please make sure you are using 3.6 or above. For the best performance use 3.11+*
101
+ *Note: Please make sure you are using 3.9 or above. For the best performance use 3.11+*
102
102
 
103
103
  # Usage
104
104
  One of the easiest ways to get started is to checkout the ["Pokemon Trainer Cookbook"](https://github.com/InfluxCommunity/influxdb3-python/blob/main/Examples/pokemon-trainer/cookbook.ipynb). This scenario takes you through the basics of both the client library and Pyarrow.
@@ -126,6 +126,30 @@ You can write data using the Point class, or supplying line protocol.
126
126
  point = Point("measurement").tag("location", "london").field("temperature", 42)
127
127
  client.write(point)
128
128
  ```
129
+
130
+ ### Control tag order for first-write column order (InfluxDB 3 Enterprise)
131
+ ```python
132
+ from influxdb_client_3 import InfluxDBClient3, Point, WriteOptions, WriteType, write_client_options
133
+
134
+ point = Point("cpu") \
135
+ .tag("host", "server-a") \
136
+ .tag("region", "us-east") \
137
+ .tag("rack", "r1") \
138
+ .field("usage", 0.42)
139
+
140
+ write_options = WriteOptions(
141
+ write_type=WriteType.synchronous,
142
+ tag_order=["region", "host"],
143
+ )
144
+
145
+ client = InfluxDBClient3(
146
+ token="your-token",
147
+ host="your-host",
148
+ database="your-database",
149
+ write_client_options=write_client_options(write_options=write_options),
150
+ )
151
+ client.write(point)
152
+ ```
129
153
  ### Using Line Protocol
130
154
  ```python
131
155
  point = "measurement fieldname=0"
@@ -49,7 +49,7 @@ pip install influxdb3-python
49
49
 
50
50
  Note: This does not include Pandas support. If you would like to use key features such as `to_pandas()` and `write_file()` you will need to install `pandas` separately.
51
51
 
52
- *Note: Please make sure you are using 3.6 or above. For the best performance use 3.11+*
52
+ *Note: Please make sure you are using 3.9 or above. For the best performance use 3.11+*
53
53
 
54
54
  # Usage
55
55
  One of the easiest ways to get started is to checkout the ["Pokemon Trainer Cookbook"](https://github.com/InfluxCommunity/influxdb3-python/blob/main/Examples/pokemon-trainer/cookbook.ipynb). This scenario takes you through the basics of both the client library and Pyarrow.
@@ -77,6 +77,30 @@ You can write data using the Point class, or supplying line protocol.
77
77
  point = Point("measurement").tag("location", "london").field("temperature", 42)
78
78
  client.write(point)
79
79
  ```
80
+
81
+ ### Control tag order for first-write column order (InfluxDB 3 Enterprise)
82
+ ```python
83
+ from influxdb_client_3 import InfluxDBClient3, Point, WriteOptions, WriteType, write_client_options
84
+
85
+ point = Point("cpu") \
86
+ .tag("host", "server-a") \
87
+ .tag("region", "us-east") \
88
+ .tag("rack", "r1") \
89
+ .field("usage", 0.42)
90
+
91
+ write_options = WriteOptions(
92
+ write_type=WriteType.synchronous,
93
+ tag_order=["region", "host"],
94
+ )
95
+
96
+ client = InfluxDBClient3(
97
+ token="your-token",
98
+ host="your-host",
99
+ database="your-database",
100
+ write_client_options=write_client_options(write_options=write_options),
101
+ )
102
+ client.write(point)
103
+ ```
80
104
  ### Using Line Protocol
81
105
  ```python
82
106
  point = "measurement fieldname=0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdb3-python
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Community Python client for InfluxDB 3.0
5
5
  Home-page: https://github.com/InfluxCommunity/influxdb3-python
6
6
  Author: InfluxData
@@ -8,13 +8,13 @@ Author-email: contact@influxdata.com
8
8
  Classifier: Development Status :: 4 - Beta
9
9
  Classifier: Intended Audience :: Developers
10
10
  Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3.8
12
11
  Classifier: Programming Language :: Python :: 3.9
13
12
  Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
17
- Requires-Python: >=3.8
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Python: >=3.9
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: reactivex>=4.0.4
@@ -98,7 +98,7 @@ pip install influxdb3-python
98
98
 
99
99
  Note: This does not include Pandas support. If you would like to use key features such as `to_pandas()` and `write_file()` you will need to install `pandas` separately.
100
100
 
101
- *Note: Please make sure you are using 3.6 or above. For the best performance use 3.11+*
101
+ *Note: Please make sure you are using 3.9 or above. For the best performance use 3.11+*
102
102
 
103
103
  # Usage
104
104
  One of the easiest ways to get started is to checkout the ["Pokemon Trainer Cookbook"](https://github.com/InfluxCommunity/influxdb3-python/blob/main/Examples/pokemon-trainer/cookbook.ipynb). This scenario takes you through the basics of both the client library and Pyarrow.
@@ -126,6 +126,30 @@ You can write data using the Point class, or supplying line protocol.
126
126
  point = Point("measurement").tag("location", "london").field("temperature", 42)
127
127
  client.write(point)
128
128
  ```
129
+
130
+ ### Control tag order for first-write column order (InfluxDB 3 Enterprise)
131
+ ```python
132
+ from influxdb_client_3 import InfluxDBClient3, Point, WriteOptions, WriteType, write_client_options
133
+
134
+ point = Point("cpu") \
135
+ .tag("host", "server-a") \
136
+ .tag("region", "us-east") \
137
+ .tag("rack", "r1") \
138
+ .field("usage", 0.42)
139
+
140
+ write_options = WriteOptions(
141
+ write_type=WriteType.synchronous,
142
+ tag_order=["region", "host"],
143
+ )
144
+
145
+ client = InfluxDBClient3(
146
+ token="your-token",
147
+ host="your-host",
148
+ database="your-database",
149
+ write_client_options=write_client_options(write_options=write_options),
150
+ )
151
+ client.write(point)
152
+ ```
129
153
  ### Using Line Protocol
130
154
  ```python
131
155
  point = "measurement fieldname=0"
@@ -1,4 +1,4 @@
1
1
  """Version of the Client that is used in User-Agent header."""
2
2
 
3
- VERSION = '0.18.0'
3
+ VERSION = '0.19.0'
4
4
  USER_AGENT = f'influxdb3-python/{VERSION}'
@@ -246,7 +246,8 @@ class _BaseWriteApi(object):
246
246
  elif isinstance(record, Point):
247
247
  precision_from_point = kwargs.get('precision_from_point', True)
248
248
  precision = record.write_precision if precision_from_point else write_precision
249
- self._serialize(record.to_line_protocol(precision=precision), precision, payload, **kwargs)
249
+ self._serialize(record.to_line_protocol(precision=precision, tag_order=kwargs.get('tag_order')),
250
+ precision, payload, **kwargs)
250
251
 
251
252
  elif isinstance(record, dict):
252
253
  self._serialize(Point.from_dict(record, write_precision=write_precision, **kwargs),
@@ -10,7 +10,7 @@ import re
10
10
 
11
11
  from influxdb_client_3.write_client.domain import WritePrecision
12
12
  from influxdb_client_3.write_client.client.write.point import _ESCAPE_KEY, _ESCAPE_STRING, _ESCAPE_MEASUREMENT, \
13
- DEFAULT_WRITE_PRECISION
13
+ DEFAULT_WRITE_PRECISION, ordered_tag_keys
14
14
 
15
15
  logger = logging.getLogger('influxdb_client.client.write.dataframe_serializer')
16
16
 
@@ -130,8 +130,8 @@ class DataframeSerializer:
130
130
 
131
131
  # keys holds a list of string keys.
132
132
  keys = []
133
- # tags holds a list of tag f-string segments ordered alphabetically by tag key.
134
- tags = []
133
+ # tag_segments holds map of tag key -> tag f-string segment.
134
+ tag_segments = {}
135
135
  # fields holds a list of field f-string segments ordered alphabetically by field key
136
136
  fields = []
137
137
  # field_indexes holds the index into each row of all the fields.
@@ -188,7 +188,7 @@ class DataframeSerializer:
188
188
  }}"""
189
189
  else:
190
190
  key_value = f',{key_format}={{str({val_format}).translate(_ESCAPE_KEY)}}'
191
- tags.append(key_value)
191
+ tag_segments[key] = key_value
192
192
  continue
193
193
  elif timestamp_column is not None and key in timestamp_column:
194
194
  timestamp_index = field_index
@@ -225,7 +225,8 @@ class DataframeSerializer:
225
225
 
226
226
  measurement_name = str(data_frame_measurement_name).translate(_ESCAPE_MEASUREMENT)
227
227
 
228
- tags = ''.join(tags)
228
+ tag_keys = ordered_tag_keys(list(tag_segments.keys()), kwargs.get('tag_order'))
229
+ tag_string = ''.join(tag_segments[tag_key] for tag_key in tag_keys)
229
230
  fields = ''.join(fields)
230
231
  timestamp = '{p[%s].value}' % timestamp_index
231
232
  if precision == WritePrecision.US:
@@ -235,7 +236,7 @@ class DataframeSerializer:
235
236
  elif precision == WritePrecision.S:
236
237
  timestamp = '{int(p[%s].value / 1e9)}' % timestamp_index
237
238
 
238
- f = eval(f'lambda p: f"""{{measurement_name}}{tags} {fields} {timestamp}"""', {
239
+ f = eval(f'lambda p: f"""{{measurement_name}}{tag_string} {fields} {timestamp}"""', {
239
240
  'measurement_name': measurement_name,
240
241
  '_ESCAPE_KEY': _ESCAPE_KEY,
241
242
  '_ESCAPE_STRING': _ESCAPE_STRING,
@@ -3,6 +3,7 @@
3
3
  import math
4
4
  import warnings
5
5
  from builtins import int
6
+ from collections.abc import Iterable
6
7
  from datetime import datetime, timedelta, timezone
7
8
  from decimal import Decimal
8
9
  from numbers import Integral
@@ -215,11 +216,12 @@ class Point(object):
215
216
  self._fields[field] = value
216
217
  return self
217
218
 
218
- def to_line_protocol(self, precision=None):
219
+ def to_line_protocol(self, precision=None, tag_order=None):
219
220
  """
220
221
  Create LineProtocol.
221
222
 
222
223
  :param precision: required precision of LineProtocol. If it's not set then use the precision from ``Point``.
224
+ :param tag_order: optional list of tag names to prioritize in serialized output
223
225
  """
224
226
  _measurement = _escape_key(self._name, _ESCAPE_MEASUREMENT)
225
227
  if _measurement.startswith("#"):
@@ -229,7 +231,7 @@ The output Line protocol will be interpret as a comment by InfluxDB. For more in
229
231
  - https://docs.influxdata.com/influxdb/latest/reference/syntax/line-protocol/#comments
230
232
  """
231
233
  warnings.warn(message, SyntaxWarning)
232
- _tags = _append_tags(self._tags)
234
+ _tags = _append_tags(self._tags, tag_order)
233
235
  _fields = _append_fields(self._fields, self._field_types)
234
236
  if not _fields:
235
237
  return ""
@@ -252,9 +254,10 @@ The output Line protocol will be interpret as a comment by InfluxDB. For more in
252
254
  return self.to_line_protocol()
253
255
 
254
256
 
255
- def _append_tags(tags):
257
+ def _append_tags(tags, tag_order=None):
256
258
  _return = []
257
- for tag_key, tag_value in sorted(tags.items()):
259
+ for tag_key in ordered_tag_keys(sorted(tags.keys()), tag_order):
260
+ tag_value = tags.get(tag_key)
258
261
 
259
262
  if tag_value is None:
260
263
  continue
@@ -267,6 +270,49 @@ def _append_tags(tags):
267
270
  return f"{',' if _return else ''}{','.join(_return)} "
268
271
 
269
272
 
273
+ def sanitize_tag_order(tag_order):
274
+ if tag_order is None:
275
+ return []
276
+
277
+ if isinstance(tag_order, (str, bytes)):
278
+ raise TypeError("tag_order must be an iterable of strings, not str/bytes")
279
+
280
+ if not isinstance(tag_order, Iterable):
281
+ raise TypeError("tag_order must be an iterable of strings")
282
+
283
+ sanitized = []
284
+ seen = set()
285
+ for tag in tag_order:
286
+ if tag is None or tag == "":
287
+ continue
288
+ if not isinstance(tag, str):
289
+ raise TypeError("tag_order entries must be strings")
290
+ if tag in seen:
291
+ continue
292
+ seen.add(tag)
293
+ sanitized.append(tag)
294
+ return sanitized
295
+
296
+
297
+ def ordered_tag_keys(existing_keys, tag_order=None):
298
+ ordered_keys = list(existing_keys)
299
+ if not tag_order:
300
+ return ordered_keys
301
+
302
+ remaining = set(ordered_keys)
303
+ prioritized = []
304
+ for tag_key in tag_order:
305
+ if not tag_key:
306
+ continue
307
+ if tag_key not in remaining:
308
+ continue
309
+ remaining.remove(tag_key)
310
+ prioritized.append(tag_key)
311
+
312
+ prioritized.extend([tag_key for tag_key in ordered_keys if tag_key in remaining])
313
+ return prioritized
314
+
315
+
270
316
  def _append_fields(fields, field_types):
271
317
  _return = []
272
318
 
@@ -7,7 +7,8 @@ Much of the code here is inspired by that in the aioinflux packet found here: ht
7
7
  import logging
8
8
  import math
9
9
 
10
- from influxdb_client_3.write_client.client.write.point import _ESCAPE_KEY, _ESCAPE_STRING, DEFAULT_WRITE_PRECISION
10
+ from influxdb_client_3.write_client.client.write.point import _ESCAPE_KEY, _ESCAPE_STRING, DEFAULT_WRITE_PRECISION, \
11
+ ordered_tag_keys
11
12
 
12
13
  logger = logging.getLogger('influxdb_client.client.write.polars_dataframe_serializer')
13
14
 
@@ -36,6 +37,7 @@ class PolarsDataframeSerializer:
36
37
  self.chunk_size = chunk_size
37
38
  self.measurement_name = kwargs.get("data_frame_measurement_name", "measurement")
38
39
  self.tag_columns = kwargs.get("data_frame_tag_columns", [])
40
+ self.tag_order = kwargs.get("tag_order", None)
39
41
  self.timestamp_column = kwargs.get("data_frame_timestamp_column", None)
40
42
  self.timestamp_timezone = kwargs.get("data_frame_timestamp_timezone", None)
41
43
 
@@ -62,34 +64,43 @@ class PolarsDataframeSerializer:
62
64
  return str(value).translate(_ESCAPE_STRING)
63
65
 
64
66
  def to_line_protocol(self, row):
65
- # Filter out None or empty values for tags
66
- tags = ""
67
+ tag_values = {}
68
+ tag_keys = []
69
+ for col in self.tag_columns:
70
+ value = row[self.column_indices[col]]
71
+ if value is None or value == "":
72
+ continue
73
+ if col not in tag_values:
74
+ tag_keys.append(col)
75
+ tag_values[col] = value
67
76
 
77
+ if self.point_settings.defaultTags:
78
+ for key, value in self.point_settings.defaultTags.items():
79
+ if value is None or value == "":
80
+ continue
81
+ if key in tag_values:
82
+ continue
83
+ tag_keys.append(key)
84
+ tag_values[key] = value
85
+
86
+ final_tag_keys = ordered_tag_keys(tag_keys, self.tag_order)
68
87
  tags = ",".join(
69
- f'{self.escape_key(col)}={self.escape_key(row[self.column_indices[col]])}'
70
- for col in self.tag_columns
71
- if row[self.column_indices[col]] is not None and row[self.column_indices[col]] != ""
88
+ f'{self.escape_key(key)}={self.escape_key(tag_values[key])}'
89
+ for key in final_tag_keys
72
90
  )
73
91
 
74
- if self.point_settings.defaultTags:
75
- default_tags = ",".join(
76
- f'{self.escape_key(key)}={self.escape_key(value)}'
77
- for key, value in self.point_settings.defaultTags.items()
78
- )
79
- # Ensure there's a comma between existing tags and default tags if both are present
80
- if tags and default_tags:
81
- tags += ","
82
- tags += default_tags
83
-
84
92
  # add escape symbols for special characters to tags
85
93
 
86
94
  fields = ",".join(
87
- f"{col}=\"{self.escape_value(row[self.column_indices[col]])}\"" if isinstance(row[self.column_indices[col]],
88
- str)
89
- else f"{col}={str(row[self.column_indices[col]]).lower()}" if isinstance(row[self.column_indices[col]],
90
- bool) # Check for bool first
91
- else f"{col}={row[self.column_indices[col]]}i" if isinstance(row[self.column_indices[col]], int)
92
- else f"{col}={row[self.column_indices[col]]}"
95
+ f"{self.escape_key(col)}=\"{self.escape_value(row[self.column_indices[col]])}\"" if isinstance(
96
+ row[self.column_indices[col]],
97
+ str)
98
+ else f"{self.escape_key(col)}={str(row[self.column_indices[col]]).lower()}" if isinstance(
99
+ row[self.column_indices[col]],
100
+ bool) # Check for bool first
101
+ else f"{self.escape_key(col)}={row[self.column_indices[col]]}i" if isinstance(row[self.column_indices[col]],
102
+ int)
103
+ else f"{self.escape_key(col)}={row[self.column_indices[col]]}"
93
104
  for col in self.column_indices
94
105
  if col not in self.tag_columns + [self.timestamp_column] and
95
106
  row[self.column_indices[col]] is not None and row[self.column_indices[col]] != ""
@@ -22,7 +22,7 @@ from reactivex.subject import Subject
22
22
  from influxdb_client_3.write_client.client._base import _BaseWriteApi, _HAS_DATACLASS
23
23
  from influxdb_client_3.write_client.client.util.helpers import get_org_query_param
24
24
  from influxdb_client_3.write_client.client.write.dataframe_serializer import DataframeSerializer
25
- from influxdb_client_3.write_client.client.write.point import Point, DEFAULT_WRITE_PRECISION
25
+ from influxdb_client_3.write_client.client.write.point import Point, DEFAULT_WRITE_PRECISION, sanitize_tag_order
26
26
  from influxdb_client_3.write_client.client.write.retry import WritesRetry
27
27
  from influxdb_client_3.write_client.domain import WritePrecision
28
28
  from influxdb_client_3.write_client.rest import _UTF_8_encoding
@@ -43,6 +43,8 @@ SERIALIZER_KWARGS = {
43
43
  'record_time_key',
44
44
  'record_tag_keys',
45
45
  'record_field_keys',
46
+ # Point serialization-specific kwargs
47
+ 'tag_order',
46
48
  }
47
49
 
48
50
  logger = logging.getLogger('influxdb_client_3.write_client.client.write_api')
@@ -81,6 +83,7 @@ class WriteOptions(object):
81
83
  max_close_wait=300_000,
82
84
  write_precision=DEFAULT_WRITE_PRECISION,
83
85
  no_sync=DEFAULT_WRITE_NO_SYNC,
86
+ tag_order=None,
84
87
  timeout=DEFAULT_WRITE_TIMEOUT,
85
88
  write_scheduler=ThreadPoolScheduler(max_workers=1)) -> None:
86
89
  """
@@ -100,6 +103,7 @@ class WriteOptions(object):
100
103
  :param max_close_wait: the maximum time to wait for writes to be flushed if close() is called
101
104
  :param write_precision: precision to use when writing points to InfluxDB
102
105
  :param no_sync: skip waiting for WAL persistence on write
106
+ :param tag_order: optional list of tag names used to prioritize tag serialization order
103
107
  :param timeout: timeout to use when writing to the database in milliseconds. Default is 10_000
104
108
  :param write_scheduler:
105
109
  """
@@ -117,6 +121,7 @@ class WriteOptions(object):
117
121
  self.write_precision = write_precision
118
122
  self.timeout = timeout
119
123
  self.no_sync = no_sync
124
+ self.tag_order = sanitize_tag_order(tag_order)
120
125
 
121
126
  def to_retry_strategy(self, **kwargs):
122
127
  """
@@ -380,6 +385,11 @@ class WriteApi(_BaseWriteApi):
380
385
  if write_precision is None:
381
386
  write_precision = self._write_options.write_precision
382
387
 
388
+ if 'tag_order' in kwargs:
389
+ kwargs['tag_order'] = sanitize_tag_order(kwargs.get('tag_order'))
390
+ else:
391
+ kwargs['tag_order'] = self._write_options.tag_order
392
+
383
393
  if self._write_options.write_type is WriteType.batching:
384
394
  return self._write_batching(bucket, org, record,
385
395
  write_precision, **kwargs)
@@ -520,7 +530,9 @@ class WriteApi(_BaseWriteApi):
520
530
  precision, **kwargs)
521
531
 
522
532
  elif isinstance(data, Point):
523
- self._write_batching(bucket, org, data.to_line_protocol(), data.write_precision, **kwargs)
533
+ self._write_batching(bucket, org,
534
+ data.to_line_protocol(tag_order=kwargs.get('tag_order')),
535
+ data.write_precision, **kwargs)
524
536
 
525
537
  elif isinstance(data, dict):
526
538
  self._write_batching(bucket, org, Point.from_dict(data, write_precision=precision, **kwargs),
@@ -30,7 +30,7 @@ class WritePrecision(object):
30
30
  def __init__(self): # noqa: E501,D401,D403
31
31
  """WritePrecision - a model defined in OpenAPI.""" # noqa: E501 self.discriminator = None
32
32
 
33
- def to_dict(self):
33
+ def to_dict(self): # pragma: no cover
34
34
  """Return the model properties as a dict."""
35
35
  result = {}
36
36
 
@@ -54,21 +54,21 @@ class WritePrecision(object):
54
54
 
55
55
  return result
56
56
 
57
- def to_str(self):
57
+ def to_str(self): # pragma: no cover
58
58
  """Return the string representation of the model."""
59
59
  return pprint.pformat(self.to_dict())
60
60
 
61
- def __repr__(self):
61
+ def __repr__(self): # pragma: no cover
62
62
  """For `print` and `pprint`."""
63
63
  return self.to_str()
64
64
 
65
- def __eq__(self, other):
65
+ def __eq__(self, other): # pragma: no cover
66
66
  """Return true if both objects are equal."""
67
67
  if not isinstance(other, WritePrecision):
68
68
  return False
69
69
 
70
70
  return self.__dict__ == other.__dict__
71
71
 
72
- def __ne__(self, other):
72
+ def __ne__(self, other): # pragma: no cover
73
73
  """Return true if both objects are not equal."""
74
74
  return not self == other
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=82.0.1"]
3
+ build-backend = "setuptools.build_meta"
@@ -60,16 +60,16 @@ setup(
60
60
  ]
61
61
  },
62
62
  install_requires=requires,
63
- python_requires='>=3.8',
63
+ python_requires='>=3.9',
64
64
  classifiers=[
65
65
  'Development Status :: 4 - Beta',
66
66
  'Intended Audience :: Developers',
67
67
  'License :: OSI Approved :: MIT License',
68
- 'Programming Language :: Python :: 3.8',
69
68
  'Programming Language :: Python :: 3.9',
70
69
  'Programming Language :: Python :: 3.10',
71
70
  'Programming Language :: Python :: 3.11',
72
71
  'Programming Language :: Python :: 3.12',
73
72
  'Programming Language :: Python :: 3.13',
73
+ 'Programming Language :: Python :: 3.14',
74
74
  ]
75
75
  )
@@ -56,7 +56,6 @@ class ApiClientTests(unittest.TestCase):
56
56
  return response.HTTPResponse(status=200, version=4, reason="OK", decode_content=False, request_url=url)
57
57
 
58
58
  def test_default_headers(self):
59
- global _package
60
59
  conf = Configuration()
61
60
  client = ApiClient(conf,
62
61
  header_name="Authorization",
@@ -69,7 +68,6 @@ class ApiClientTests(unittest.TestCase):
69
68
  @mock.patch("influxdb_client_3.write_client._sync.rest.RESTClientObject.request",
70
69
  side_effect=mock_rest_request)
71
70
  def test_call_api(self, mock_post):
72
- global _package
73
71
  global _sentHeaders
74
72
  _sentHeaders = {}
75
73
 
@@ -276,6 +276,30 @@ class TestDataFrameSerializer(unittest.TestCase):
276
276
  self.assertEqual(1, len(points))
277
277
  self.assertEqual("h2o,a=a,b=b,c=c level=2i 1586048400000000000", points[0])
278
278
 
279
+ points = data_frame_to_list_of_points(data_frame=data_frame,
280
+ point_settings=PointSettings(),
281
+ data_frame_measurement_name='h2o',
282
+ data_frame_tag_columns={"c", "a", "b"},
283
+ tag_order=["c", "a"])
284
+
285
+ self.assertEqual(1, len(points))
286
+ self.assertEqual("h2o,c=c,a=a,b=b level=2i 1586048400000000000", points[0])
287
+
288
+ ps = PointSettings(z="from-default", c="override-ignored")
289
+ points_with_defaults = data_frame_to_list_of_points(
290
+ data_frame=data_frame,
291
+ point_settings=ps,
292
+ data_frame_measurement_name='h2o',
293
+ data_frame_tag_columns={"c", "a", "b"},
294
+ tag_order=["z", "c", "a"],
295
+ )
296
+
297
+ self.assertEqual(1, len(points_with_defaults))
298
+ self.assertEqual(
299
+ "h2o,z=from-default,c=c,a=a,b=b level=2i 1586048400000000000",
300
+ points_with_defaults[0]
301
+ )
302
+
279
303
  def test_escape_text_value(self):
280
304
  now = pd.Timestamp('2020-04-05 00:00+00:00')
281
305
  an_hour_ago = now - timedelta(hours=1)