influxdb3-python 0.17.0__tar.gz → 0.18.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 (78) hide show
  1. influxdb3_python-0.18.0/Examples/query_with_middleware.py +33 -0
  2. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/PKG-INFO +4 -1
  3. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/README.md +3 -0
  4. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb3_python.egg-info/PKG-INFO +4 -1
  5. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb3_python.egg-info/SOURCES.txt +1 -0
  6. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/exceptions/exceptions.py +32 -0
  7. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/query/query_api.py +14 -1
  8. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/version.py +1 -1
  9. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write_api.py +5 -8
  10. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_api_client.py +52 -0
  11. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_influxdb_client_3_integration.py +99 -2
  12. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_polars.py +0 -2
  13. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_query.py +41 -2
  14. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/__init__.py +0 -0
  15. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/basic_ssl_example.py +0 -0
  16. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/batching_example.py +0 -0
  17. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/cloud_dedicated_query.py +0 -0
  18. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/cloud_dedicated_write.py +0 -0
  19. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/config.py +0 -0
  20. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/flight_options_example.py +0 -0
  21. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/handle_http_error.py +0 -0
  22. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/handle_query_error.py +0 -0
  23. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/pandas_write.py +0 -0
  24. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/query_async.py +0 -0
  25. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/query_type.py +0 -0
  26. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/Examples/timeouts.py +0 -0
  27. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/LICENSE +0 -0
  28. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb3_python.egg-info/dependency_links.txt +0 -0
  29. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb3_python.egg-info/requires.txt +0 -0
  30. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb3_python.egg-info/top_level.txt +0 -0
  31. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/__init__.py +0 -0
  32. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/exceptions/__init__.py +0 -0
  33. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/py.typed +0 -0
  34. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/query/__init__.py +0 -0
  35. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/read_file.py +0 -0
  36. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/__init__.py +0 -0
  37. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/_sync/__init__.py +0 -0
  38. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/_sync/api_client.py +0 -0
  39. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/_sync/rest.py +0 -0
  40. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/__init__.py +0 -0
  41. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/_base.py +0 -0
  42. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/influxdb_client.py +0 -0
  43. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/logging_handler.py +0 -0
  44. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/util/__init__.py +0 -0
  45. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/util/date_utils.py +0 -0
  46. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/util/date_utils_pandas.py +0 -0
  47. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/util/helpers.py +0 -0
  48. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/util/multiprocessing_helper.py +0 -0
  49. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/warnings.py +0 -0
  50. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write/__init__.py +0 -0
  51. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write/dataframe_serializer.py +0 -0
  52. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write/point.py +0 -0
  53. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write/polars_dataframe_serializer.py +0 -0
  54. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/client/write/retry.py +0 -0
  55. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/configuration.py +0 -0
  56. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/domain/__init__.py +0 -0
  57. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/domain/write_precision.py +0 -0
  58. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/domain/write_precision_converter.py +0 -0
  59. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/extras.py +0 -0
  60. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/rest.py +0 -0
  61. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/service/__init__.py +0 -0
  62. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/service/_base_service.py +0 -0
  63. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/service/signin_service.py +0 -0
  64. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/service/signout_service.py +0 -0
  65. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/influxdb_client_3/write_client/service/write_service.py +0 -0
  66. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/pyproject.toml +0 -0
  67. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/setup.cfg +0 -0
  68. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/setup.py +0 -0
  69. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_dataframe_serializer.py +0 -0
  70. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_date_helper.py +0 -0
  71. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_deep_merge.py +0 -0
  72. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_flush.py +0 -0
  73. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_influxdb_client_3.py +0 -0
  74. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_merge_options.py +0 -0
  75. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_point.py +0 -0
  76. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_write_file.py +0 -0
  77. {influxdb3_python-0.17.0 → influxdb3_python-0.18.0}/tests/test_write_local_server.py +0 -0
  78. {influxdb3_python-0.17.0 → influxdb3_python-0.18.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.18.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
@@ -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>
@@ -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>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdb3-python
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -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>
@@ -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.18.0'
4
4
  USER_AGENT = f'influxdb3-python/{VERSION}'
@@ -296,9 +296,7 @@ class WriteApi(_BaseWriteApi):
296
296
 
297
297
  if self._write_options.write_type is WriteType.asynchronous:
298
298
  message = """The 'WriteType.asynchronous' is deprecated and will be removed in future major version.
299
-
300
- You can use native asynchronous version of the client:
301
- - https://influxdb-client.readthedocs.io/en/stable/usage.html#how-to-use-asyncio
299
+ You can use native asynchronous version of the client:
302
300
  """
303
301
  # TODO above message has link to Influxdb2 API __NOT__ Influxdb3 API !!! - illustrates different API
304
302
  warnings.warn(message, DeprecationWarning)
@@ -393,12 +391,9 @@ You can use native asynchronous version of the client:
393
391
 
394
392
  _async_req = True if self._write_options.write_type == WriteType.asynchronous else False
395
393
 
396
- # Filter out serializer-specific kwargs before passing to _post_write
397
- http_kwargs = {k: v for k, v in kwargs.items() if k not in SERIALIZER_KWARGS}
398
-
399
394
  def write_payload(payload):
400
395
  final_string = b'\n'.join(payload[1])
401
- return self._post_write(_async_req, bucket, org, final_string, payload[0], no_sync, **http_kwargs)
396
+ return self._post_write(_async_req, bucket, org, final_string, payload[0], no_sync, **kwargs)
402
397
 
403
398
  results = list(map(write_payload, payloads.items()))
404
399
  if not _async_req:
@@ -588,11 +583,13 @@ You can use native asynchronous version of the client:
588
583
  return _BatchResponse(data=batch_item)
589
584
 
590
585
  def _post_write(self, _async_req, bucket, org, body, precision, no_sync, **kwargs):
586
+ # Filter out serializer-specific kwargs before passing to _post_write
587
+ http_kwargs = {k: v for k, v in kwargs.items() if k not in SERIALIZER_KWARGS}
591
588
  return self._write_service.post_write(org=org, bucket=bucket, body=body, precision=precision,
592
589
  no_sync=no_sync,
593
590
  async_req=_async_req,
594
591
  content_type="text/plain; charset=utf-8",
595
- **kwargs)
592
+ **http_kwargs)
596
593
 
597
594
  def _to_response(self, data: _BatchItem, delay: timedelta):
598
595
 
@@ -124,6 +124,58 @@ class ApiClientTests(unittest.TestCase):
124
124
  self._test_api_error(response_body)
125
125
  self.assertEqual(response_body, err.exception.message)
126
126
 
127
+ def test_api_error_v3_with_detail(self):
128
+ cases = [
129
+ # all details available
130
+ (
131
+ "two-line details",
132
+ '{"error":"partial write of line protocol occurred","data":['
133
+ '{"error_message":"invalid column type for column \'v\', expected iox::column_type::field::float, '
134
+ 'got iox::column_type::field::uinteger","line_number":2,"original_line":"**.DBG.remote_***"},'
135
+ '{"error_message":"invalid column type for column \'v\', expected iox::column_type::field::float, '
136
+ 'got iox::column_type::field::uinteger","line_number":3,"original_line":"***.INF.remote_***"}'
137
+ ']}',
138
+ "partial write of line protocol occurred:\n"
139
+ "\tline 2: invalid column type for column 'v', expected iox::column_type::field::float, "
140
+ "got iox::column_type::field::uinteger (**.DBG.remote_***)\n"
141
+ "\tline 3: invalid column type for column 'v', expected iox::column_type::field::float, "
142
+ "got iox::column_type::field::uinteger (***.INF.remote_***)",
143
+ ),
144
+ # error_message only (no line_number/original_line)
145
+ (
146
+ "message-only detail",
147
+ '{"error":"partial write of line protocol occurred","data":['
148
+ '{"error_message":"only error message"}]}',
149
+ "partial write of line protocol occurred:\n"
150
+ "\tonly error message",
151
+ ),
152
+ # non-dict item in data list is skipped
153
+ (
154
+ "non-dict item skipped",
155
+ '{"error":"partial write of line protocol occurred","data":[null,'
156
+ '{"error_message":"bad line","line_number":2,"original_line":"bad lp"}]}',
157
+ "partial write of line protocol occurred:\n"
158
+ "\tline 2: bad line (bad lp)",
159
+ ),
160
+ # details empty -> return error_text
161
+ (
162
+ "no detail fields",
163
+ '{"error":"partial write of line protocol occurred","data":[{"line_number":2}]}',
164
+ "partial write of line protocol occurred",
165
+ ),
166
+ # data is not a dict when resolving fallback keys
167
+ (
168
+ "data not dict for fallback",
169
+ '{"error":"data not list","data":"oops"}',
170
+ "data not list",
171
+ ),
172
+ ]
173
+ for name, response_body, expected in cases:
174
+ with self.subTest(name):
175
+ with self.assertRaises(InfluxDBError) as err:
176
+ self._test_api_error(response_body)
177
+ self.assertEqual(expected, err.exception.message)
178
+
127
179
  def test_api_error_headers(self):
128
180
  body = '{"error": "test error"}'
129
181
  body_dic = json.loads(body)
@@ -1,16 +1,18 @@
1
1
  import logging
2
2
  import os
3
- import pyarrow
4
- import pytest
5
3
  import random
6
4
  import string
7
5
  import time
8
6
  import unittest
9
7
 
8
+ import pandas as pd
9
+ import pyarrow
10
+ import pytest
10
11
  from urllib3.exceptions import MaxRetryError, TimeoutError as Url3TimeoutError
11
12
 
12
13
  from influxdb_client_3 import InfluxDBClient3, write_client_options, WriteOptions, \
13
14
  WriteType, InfluxDB3ClientQueryError
15
+ from influxdb_client_3.write_client.rest import ApiException
14
16
  from influxdb_client_3.exceptions import InfluxDBError
15
17
  from tests.util import asyncio_run, lp_to_py_object
16
18
 
@@ -48,6 +50,68 @@ class TestInfluxDBClient3Integration(unittest.TestCase):
48
50
  if self.client:
49
51
  self.client.close()
50
52
 
53
+ def test_write_dataframe(self):
54
+ measurement = f'test{random_hex(3)}'.lower()
55
+ df = pd.DataFrame({
56
+ 'time': pd.to_datetime(['2024-01-01', '2024-01-02']),
57
+ 'city': ['London', 'Paris'],
58
+ 'temperature': [15.0, 18.5]
59
+ })
60
+ self.client.write_dataframe(df, measurement=measurement, timestamp_column='time', tags=['city'])
61
+ self.client.flush()
62
+
63
+ result = self.client.query(query=f"select * from {measurement}", mode="pandas")
64
+
65
+ self.assertIsNotNone(result)
66
+ self.assertEqual(2, len(result.get('city')))
67
+ self.assertEqual(2, len(result.get('temperature')))
68
+
69
+ def test_write_dataframe_with_batch(self):
70
+ self.client = InfluxDBClient3(host=self.host,
71
+ database=self.database,
72
+ token=self.token,
73
+ write_client_options=write_client_options(
74
+ write_options=WriteOptions(batch_size=100)
75
+ ))
76
+ measurement = f'test{random_hex(3)}'.lower()
77
+ df = pd.DataFrame({
78
+ 'time': pd.to_datetime(['2024-01-01', '2024-01-02']),
79
+ 'city': ['London', 'Paris'],
80
+ 'temperature': [15.0, 18.5]
81
+ })
82
+ self.client.write_dataframe(
83
+ df,
84
+ measurement=measurement,
85
+ timestamp_column='time',
86
+ tags=['city']
87
+ )
88
+ self.client.flush()
89
+
90
+ result = self.client.query(query=f"select * from {measurement}", mode="pandas")
91
+
92
+ self.assertIsNotNone(result)
93
+ self.assertEqual(2, len(result.get('city')))
94
+ self.assertEqual(2, len(result.get('temperature')))
95
+
96
+ def test_write_csv_file_with_batch(self):
97
+ client = InfluxDBClient3(host=self.host,
98
+ database=self.database,
99
+ token=self.token,
100
+ write_client_options=write_client_options(
101
+ write_options=WriteOptions(batch_size=100)
102
+ ))
103
+ measurement = f'test{random_hex(3)}'.lower()
104
+ client.write_file(
105
+ measurement_name=measurement,
106
+ file='tests/data/iot.csv',
107
+ timestamp_column='time', tag_columns=["name"])
108
+ client.flush()
109
+
110
+ result = client.query(query=f"select * from {measurement}", mode="pandas")
111
+ self.assertIsNotNone(result)
112
+ self.assertEqual(3, len(result.get('building')))
113
+ self.assertEqual(3, len(result.get('temperature')))
114
+
51
115
  def test_write_and_query(self):
52
116
  test_id = time.time_ns()
53
117
  self.client.write(f"integration_test_python,type=used value=123.0,test_id={test_id}i")
@@ -61,6 +125,39 @@ class TestInfluxDBClient3Integration(unittest.TestCase):
61
125
  self.assertEqual(test_id, df['test_id'][0])
62
126
  self.assertEqual(123.0, df['value'][0])
63
127
 
128
+ def test_v3_error(self):
129
+ measurement = f'test{random_hex(3)}'.lower()
130
+ lp = "\n".join([
131
+ f"{measurement} v=1i 1770291280",
132
+ f"{measurement} v=1 1770291281",
133
+ ])
134
+
135
+ with InfluxDBClient3(
136
+ host=self.host,
137
+ database=self.database,
138
+ token=self.token,
139
+ write_client_options=write_client_options(
140
+ write_options=WriteOptions(
141
+ write_type=WriteType.synchronous,
142
+ no_sync=True
143
+ )
144
+ )
145
+ ) as client:
146
+ try:
147
+ client.write(lp)
148
+ self.fail("Expected InfluxDBError from invalid line protocol.")
149
+ except ApiException as err:
150
+ if "Server doesn't support write with no_sync=true" in str(err):
151
+ self.skipTest("no_sync not supported by this server.")
152
+ msg = err.message
153
+ self.assertIn("partial write of line protocol occurred", msg)
154
+ self.assertIn((
155
+ "invalid column type for column 'v', expected iox::column_type::field::integer, "
156
+ "got iox::column_type::field::float"
157
+ ), msg)
158
+ self.assertIn("line 2", msg)
159
+ self.assertIn(measurement, msg)
160
+
64
161
  def test_auth_error_token(self):
65
162
  self.client = InfluxDBClient3(host=self.host, database=self.database, token='fake token')
66
163
  test_id = time.time_ns()
@@ -96,6 +96,4 @@ class TestWritePolars(unittest.TestCase):
96
96
  async_req=ANY,
97
97
  content_type=ANY,
98
98
  urlopen_kw=ANY,
99
- data_frame_measurement_name='measurement',
100
- data_frame_timestamp_column='time',
101
99
  body=b'measurement temperature=22.4 1722470400000000000\nmeasurement temperature=21.8 1722474000000000000')
@@ -12,7 +12,7 @@ from pyarrow.flight import (
12
12
  Ticket
13
13
  )
14
14
 
15
- from influxdb_client_3 import InfluxDBClient3
15
+ from influxdb_client_3 import InfluxDBClient3, flight_client_options
16
16
  from influxdb_client_3.query.query_api import QueryApiOptionsBuilder, QueryApi
17
17
  from influxdb_client_3.version import USER_AGENT
18
18
  from tests.util import asyncio_run
@@ -25,7 +25,8 @@ from tests.util.mocks import (
25
25
  HeaderCheckServerMiddlewareFactory,
26
26
  NoopAuthHandler,
27
27
  get_req_headers,
28
- set_req_headers
28
+ set_req_headers, ModifyHeaderClientMiddlewareFactory,
29
+ HeaderCheckServerMiddlewareFactory1
29
30
  )
30
31
 
31
32
 
@@ -175,11 +176,13 @@ Aw==
175
176
  cert_chain = 'mTLS_explicit_chain'
176
177
  self.create_cert_file(cert_file)
177
178
  test_flight_client_options = {'private_key': private_key, 'cert_chain': cert_chain}
179
+ middleware = [ModifyHeaderClientMiddlewareFactory()]
178
180
  options = QueryApiOptionsBuilder()\
179
181
  .proxy(proxy_name) \
180
182
  .root_certs(cert_file) \
181
183
  .tls_verify(False) \
182
184
  .flight_client_options(test_flight_client_options) \
185
+ .middleware(middleware) \
183
186
  .build()
184
187
 
185
188
  client = QueryApi(connection,
@@ -195,6 +198,7 @@ Aw==
195
198
  assert client._flight_client_options['private_key'] == private_key
196
199
  assert client._flight_client_options['cert_chain'] == cert_chain
197
200
  assert client._proxy == proxy_name
201
+ assert client._flight_client_options['middleware'] == middleware
198
202
  fc_opts = client._flight_client_options
199
203
  assert dict(fc_opts['generic_options'])['grpc.secondary_user_agent'].startswith('influxdb3-python/')
200
204
  assert dict(fc_opts['generic_options'])['grpc.http_proxy'] == proxy_name
@@ -311,6 +315,41 @@ Aw==
311
315
  assert _req_headers['authorization'] == [f"Bearer {token}"]
312
316
  set_req_headers({})
313
317
 
318
+ def test_query_with_middleware_success(self):
319
+ with HeaderCheckFlightServer(
320
+ auth_handler=NoopAuthHandler(),
321
+ middleware={"check": HeaderCheckServerMiddlewareFactory1()}) as server:
322
+
323
+ middleware = [ModifyHeaderClientMiddlewareFactory()]
324
+ client = InfluxDBClient3(
325
+ host=f'http://localhost:{server.port}',
326
+ org='test_org',
327
+ databse='test_db',
328
+ token='TEST_TOKEN',
329
+ flight_client_options=flight_client_options(middleware=middleware)
330
+ )
331
+
332
+ df = client.query(query='SELECT * FROM test', mode="pandas")
333
+ self.assertIsNotNone(df)
334
+
335
+ def test_query_with_missing_middleware(self):
336
+ with HeaderCheckFlightServer(
337
+ auth_handler=NoopAuthHandler(),
338
+ middleware={"check": HeaderCheckServerMiddlewareFactory1()}) as server:
339
+
340
+ client = InfluxDBClient3(
341
+ host=f'http://localhost:{server.port}',
342
+ org='test_org',
343
+ databse='test_db',
344
+ token='TEST_TOKEN'
345
+ )
346
+
347
+ try:
348
+ client.query(query='SELECT * FROM test', mode="pandas")
349
+ self.fail("Should have failed due to missing middleware")
350
+ except Exception as e:
351
+ assert "Invalid header value from middleware" in str(e)
352
+
314
353
  @asyncio_run
315
354
  async def test_query_async_pandas(self):
316
355
  with ConstantFlightServer() as server: