influxdb3-python 0.17.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.19.0/Examples/query_with_middleware.py +33 -0
  2. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/PKG-INFO +31 -4
  3. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/README.md +28 -1
  4. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/PKG-INFO +31 -4
  5. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/SOURCES.txt +1 -0
  6. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/exceptions/exceptions.py +32 -0
  7. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/query/query_api.py +14 -1
  8. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/version.py +1 -1
  9. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/_base.py +2 -1
  10. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/dataframe_serializer.py +7 -6
  11. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/point.py +50 -4
  12. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/polars_dataframe_serializer.py +33 -22
  13. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write_api.py +19 -10
  14. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/write_precision.py +5 -5
  15. influxdb3_python-0.19.0/pyproject.toml +3 -0
  16. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/setup.py +2 -2
  17. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_api_client.py +52 -2
  18. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_dataframe_serializer.py +24 -0
  19. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_influxdb_client_3.py +63 -5
  20. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_influxdb_client_3_integration.py +99 -2
  21. influxdb3_python-0.19.0/tests/test_point.py +67 -0
  22. influxdb3_python-0.19.0/tests/test_polars.py +213 -0
  23. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_query.py +45 -5
  24. influxdb3_python-0.17.0/pyproject.toml +0 -3
  25. influxdb3_python-0.17.0/tests/test_point.py +0 -14
  26. influxdb3_python-0.17.0/tests/test_polars.py +0 -101
  27. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/__init__.py +0 -0
  28. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/basic_ssl_example.py +0 -0
  29. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/batching_example.py +0 -0
  30. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/cloud_dedicated_query.py +0 -0
  31. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/cloud_dedicated_write.py +0 -0
  32. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/config.py +0 -0
  33. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/flight_options_example.py +0 -0
  34. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/handle_http_error.py +0 -0
  35. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/handle_query_error.py +0 -0
  36. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/pandas_write.py +0 -0
  37. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/query_async.py +0 -0
  38. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/query_type.py +0 -0
  39. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/Examples/timeouts.py +0 -0
  40. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/LICENSE +0 -0
  41. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/dependency_links.txt +0 -0
  42. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/requires.txt +0 -0
  43. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb3_python.egg-info/top_level.txt +0 -0
  44. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/__init__.py +0 -0
  45. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/exceptions/__init__.py +0 -0
  46. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/py.typed +0 -0
  47. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/query/__init__.py +0 -0
  48. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/read_file.py +0 -0
  49. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/__init__.py +0 -0
  50. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/__init__.py +0 -0
  51. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/api_client.py +0 -0
  52. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/_sync/rest.py +0 -0
  53. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/__init__.py +0 -0
  54. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/influxdb_client.py +0 -0
  55. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/logging_handler.py +0 -0
  56. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/__init__.py +0 -0
  57. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/date_utils.py +0 -0
  58. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/date_utils_pandas.py +0 -0
  59. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/helpers.py +0 -0
  60. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/util/multiprocessing_helper.py +0 -0
  61. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/warnings.py +0 -0
  62. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/__init__.py +0 -0
  63. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/client/write/retry.py +0 -0
  64. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/configuration.py +0 -0
  65. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/__init__.py +0 -0
  66. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/domain/write_precision_converter.py +0 -0
  67. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/extras.py +0 -0
  68. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/rest.py +0 -0
  69. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/__init__.py +0 -0
  70. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/_base_service.py +0 -0
  71. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/signin_service.py +0 -0
  72. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/signout_service.py +0 -0
  73. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/influxdb_client_3/write_client/service/write_service.py +0 -0
  74. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/setup.cfg +0 -0
  75. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_date_helper.py +0 -0
  76. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_deep_merge.py +0 -0
  77. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_flush.py +0 -0
  78. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_merge_options.py +0 -0
  79. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_write_file.py +0 -0
  80. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_write_local_server.py +0 -0
  81. {influxdb3_python-0.17.0 → influxdb3_python-0.19.0}/tests/test_write_precision_converter.py +0 -0
@@ -0,0 +1,33 @@
1
+ from pyarrow import flight
2
+
3
+ from config import Config
4
+ from influxdb_client_3 import InfluxDBClient3, flight_client_options
5
+
6
+
7
+ # This middleware will add an additional attribute `some-attribute` to the header
8
+ class ModifyHeaderClientMiddleware(flight.ClientMiddleware):
9
+ def sending_headers(self):
10
+ return {
11
+ "some-attribute": "some-value",
12
+ }
13
+
14
+ def received_headers(self, headers):
15
+ pass
16
+
17
+
18
+ class ModifyHeaderClientMiddlewareFactory(flight.ClientMiddlewareFactory):
19
+ def start_call(self, info):
20
+ return ModifyHeaderClientMiddleware()
21
+
22
+
23
+ config = Config()
24
+ middleware = [ModifyHeaderClientMiddlewareFactory()]
25
+ client = InfluxDBClient3(
26
+ host=config.host,
27
+ token=config.token,
28
+ database=config.database,
29
+ flight_client_options=flight_client_options(middleware=middleware)
30
+ )
31
+
32
+ df = client.query(query="select * from cpu11 limit 10", mode="pandas")
33
+ print(len(df))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdb3-python
3
- Version: 0.17.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
@@ -53,6 +53,9 @@ Dynamic: summary
53
53
  </p>
54
54
 
55
55
  <p align="center">
56
+ <a href="https://influxdb3-python.readthedocs.io/en/latest/">
57
+ <img src="https://img.shields.io/readthedocs/influxdb3-python/latest" alt="Readthedocs document">
58
+ </a>
56
59
  <a href="https://pypi.org/project/influxdb3-python/">
57
60
  <img src="https://img.shields.io/pypi/v/influxdb3-python.svg" alt="PyPI version">
58
61
  </a>
@@ -95,7 +98,7 @@ pip install influxdb3-python
95
98
 
96
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.
97
100
 
98
- *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+*
99
102
 
100
103
  # Usage
101
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.
@@ -123,6 +126,30 @@ You can write data using the Point class, or supplying line protocol.
123
126
  point = Point("measurement").tag("location", "london").field("temperature", 42)
124
127
  client.write(point)
125
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
+ ```
126
153
  ### Using Line Protocol
127
154
  ```python
128
155
  point = "measurement fieldname=0"
@@ -4,6 +4,9 @@
4
4
  </p>
5
5
 
6
6
  <p align="center">
7
+ <a href="https://influxdb3-python.readthedocs.io/en/latest/">
8
+ <img src="https://img.shields.io/readthedocs/influxdb3-python/latest" alt="Readthedocs document">
9
+ </a>
7
10
  <a href="https://pypi.org/project/influxdb3-python/">
8
11
  <img src="https://img.shields.io/pypi/v/influxdb3-python.svg" alt="PyPI version">
9
12
  </a>
@@ -46,7 +49,7 @@ pip install influxdb3-python
46
49
 
47
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.
48
51
 
49
- *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+*
50
53
 
51
54
  # Usage
52
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.
@@ -74,6 +77,30 @@ You can write data using the Point class, or supplying line protocol.
74
77
  point = Point("measurement").tag("location", "london").field("temperature", 42)
75
78
  client.write(point)
76
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
+ ```
77
104
  ### Using Line Protocol
78
105
  ```python
79
106
  point = "measurement fieldname=0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdb3-python
3
- Version: 0.17.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
@@ -53,6 +53,9 @@ Dynamic: summary
53
53
  </p>
54
54
 
55
55
  <p align="center">
56
+ <a href="https://influxdb3-python.readthedocs.io/en/latest/">
57
+ <img src="https://img.shields.io/readthedocs/influxdb3-python/latest" alt="Readthedocs document">
58
+ </a>
56
59
  <a href="https://pypi.org/project/influxdb3-python/">
57
60
  <img src="https://img.shields.io/pypi/v/influxdb3-python.svg" alt="PyPI version">
58
61
  </a>
@@ -95,7 +98,7 @@ pip install influxdb3-python
95
98
 
96
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.
97
100
 
98
- *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+*
99
102
 
100
103
  # Usage
101
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.
@@ -123,6 +126,30 @@ You can write data using the Point class, or supplying line protocol.
123
126
  point = Point("measurement").tag("location", "london").field("temperature", 42)
124
127
  client.write(point)
125
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
+ ```
126
153
  ### Using Line Protocol
127
154
  ```python
128
155
  point = "measurement fieldname=0"
@@ -14,6 +14,7 @@ Examples/handle_query_error.py
14
14
  Examples/pandas_write.py
15
15
  Examples/query_async.py
16
16
  Examples/query_type.py
17
+ Examples/query_with_middleware.py
17
18
  Examples/timeouts.py
18
19
  influxdb3_python.egg-info/PKG-INFO
19
20
  influxdb3_python.egg-info/SOURCES.txt
@@ -63,9 +63,41 @@ class InfluxDBError(InfluxDB3ClientError):
63
63
  def get(d, key):
64
64
  if not key or d is None:
65
65
  return d
66
+ if not isinstance(d, dict):
67
+ return None
66
68
  return get(d.get(key[0]), key[1:])
67
69
  try:
68
70
  node = json.loads(response.data)
71
+ if isinstance(node, dict):
72
+ # InfluxDB v3 error format: { "code": "...", "message": "..." }
73
+ code = node.get("code")
74
+ message = node.get("message")
75
+ if message:
76
+ return f"{code}: {message}" if code else message
77
+ # InfluxDB v3 write error format:
78
+ # {
79
+ # "error": "...",
80
+ # "data": [ { "error_message": "...", "line_number": 2, "original_line": "..." }, ... ]
81
+ # }
82
+ error_text = node.get("error")
83
+ data = node.get("data")
84
+ if error_text and isinstance(data, list):
85
+ details = []
86
+ for item in data:
87
+ if not isinstance(item, dict):
88
+ continue
89
+ line_number = item.get("line_number")
90
+ error_message = item.get("error_message")
91
+ original_line = item.get("original_line")
92
+ if line_number is not None and error_message and original_line:
93
+ details.append(
94
+ f"\tline {line_number}: {error_message} ({original_line})"
95
+ )
96
+ elif error_message:
97
+ details.append(f"\t{error_message}")
98
+ if details:
99
+ return error_text + ":\n" + "\n".join(details)
100
+ return error_text
69
101
  for key in [['message'], ['data', 'error_message'], ['error']]:
70
102
  value = get(node, key)
71
103
  if value is not None:
@@ -20,6 +20,7 @@ class QueryApiOptions(object):
20
20
  flight_client_options (dict): base set of flight client options passed to internal pyarrow.flight.FlightClient
21
21
  timeout(float): timeout in seconds to wait for a response
22
22
  disable_grpc_compression (bool): disable gRPC compression for query responses
23
+ middleware (list): list of middleware functions to be applied to Flight calls
23
24
  """
24
25
  _DEFAULT_TIMEOUT = 300.0
25
26
  tls_root_certs: bytes = None
@@ -28,13 +29,15 @@ class QueryApiOptions(object):
28
29
  flight_client_options: dict = None
29
30
  timeout: float = None
30
31
  disable_grpc_compression: bool = False
32
+ middleware: list = None
31
33
 
32
34
  def __init__(self, root_certs_path: str,
33
35
  verify: bool,
34
36
  proxy: str,
35
37
  flight_client_options: dict,
36
38
  timeout: float = _DEFAULT_TIMEOUT,
37
- disable_grpc_compression: bool = False):
39
+ disable_grpc_compression: bool = False,
40
+ middleware: list = None):
38
41
  """
39
42
  Initialize a set of QueryApiOptions
40
43
 
@@ -45,6 +48,7 @@ class QueryApiOptions(object):
45
48
  to be passed to internal pyarrow.flight.FlightClient.
46
49
  :param timeout: timeout in seconds to wait for a response.
47
50
  :param disable_grpc_compression: disable gRPC compression for query responses.
51
+ :param middleware: list of middleware functions to be applied to Flight calls.
48
52
  """
49
53
  if root_certs_path:
50
54
  self.tls_root_certs = self._read_certs(root_certs_path)
@@ -53,6 +57,7 @@ class QueryApiOptions(object):
53
57
  self.flight_client_options = flight_client_options
54
58
  self.timeout = timeout
55
59
  self.disable_grpc_compression = disable_grpc_compression
60
+ self.middleware = middleware
56
61
 
57
62
  def _read_certs(self, path: str) -> bytes:
58
63
  with open(path, "rb") as certs_file:
@@ -81,6 +86,7 @@ class QueryApiOptionsBuilder(object):
81
86
  _flight_client_options: dict = None
82
87
  _timeout: float = None
83
88
  _disable_grpc_compression: bool = False
89
+ _middleware: list = None
84
90
 
85
91
  def root_certs(self, path: str):
86
92
  self._root_certs_path = path
@@ -107,6 +113,10 @@ class QueryApiOptionsBuilder(object):
107
113
  self._disable_grpc_compression = disable
108
114
  return self
109
115
 
116
+ def middleware(self, middleware: list):
117
+ self._middleware = middleware
118
+ return self
119
+
110
120
  def build(self) -> QueryApiOptions:
111
121
  """Build a QueryApiOptions object with previously set values"""
112
122
  return QueryApiOptions(
@@ -116,6 +126,7 @@ class QueryApiOptionsBuilder(object):
116
126
  flight_client_options=self._flight_client_options,
117
127
  timeout=self._timeout,
118
128
  disable_grpc_compression=self._disable_grpc_compression,
129
+ middleware=self._middleware
119
130
  )
120
131
 
121
132
 
@@ -181,6 +192,8 @@ class QueryApi(object):
181
192
  self._flight_client_options["generic_options"].append(
182
193
  ("grpc.compression_enabled_algorithms_bitset", 1)
183
194
  )
195
+ if options.middleware:
196
+ self._flight_client_options["middleware"] = options.middleware
184
197
  if self._proxy:
185
198
  self._flight_client_options["generic_options"].append(("grpc.http_proxy", self._proxy))
186
199
  self._flight_client = FlightClient(connection_string, **self._flight_client_options)
@@ -1,4 +1,4 @@
1
1
  """Version of the Client that is used in User-Agent header."""
2
2
 
3
- VERSION = '0.17.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]] != ""