Skip to content

Commit 9184914

Browse files
committed
feat: bind query parameters (#219)
1 parent 8df7b18 commit 9184914

File tree

5 files changed

+333
-22
lines changed

5 files changed

+333
-22
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
## 1.17.0 [unreleased]
22

3+
### Features
4+
1. [#203](https://github.com/influxdata/influxdb-client-python/issues/219): Bind query parameters
5+
36
## 1.16.0 [2021-04-01]
47

58
### Features

README.rst

+32-3
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,10 @@ Queries
464464
The result retrieved by `QueryApi <https://github.com/influxdata/influxdb-client-python/blob/master/influxdb_client/client/query_api.py>`_ could be formatted as a:
465465

466466
1. Flux data structure: `FluxTable <https://github.com/influxdata/influxdb-client-python/blob/master/influxdb_client/client/flux_table.py#L5>`_, `FluxColumn <https://github.com/influxdata/influxdb-client-python/blob/master/influxdb_client/client/flux_table.py#L22>`_ and `FluxRecord <https://github.com/influxdata/influxdb-client-python/blob/master/influxdb_client/client/flux_table.py#L31>`_
467-
2. `csv.reader <https://docs.python.org/3.4/library/csv.html#reader-objects>`__ which will iterate over CSV lines
468-
3. Raw unprocessed results as a ``str`` iterator
469-
4. `Pandas DataFrame <https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html>`_
467+
2. Query bind parameters
468+
3. `csv.reader <https://docs.python.org/3.4/library/csv.html#reader-objects>`__ which will iterate over CSV lines
469+
4. Raw unprocessed results as a ``str`` iterator
470+
5. `Pandas DataFrame <https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html>`_
470471

471472
The API also support streaming ``FluxRecord`` via `query_stream <https://github.com/influxdata/influxdb-client-python/blob/master/influxdb_client/client/query_api.py#L77>`_, see example below:
472473

@@ -502,6 +503,34 @@ The API also support streaming ``FluxRecord`` via `query_stream <https://github.
502503
print()
503504
print()
504505
506+
"""
507+
Query: using Bind parameters
508+
"""
509+
510+
p = {"_start": datetime.timedelta(hours=-1),
511+
"_location": "Prague",
512+
"_desc": True,
513+
"_floatParam": 25.1,
514+
"_every": datetime.timedelta(minutes=5)
515+
}
516+
517+
tables = query_api.query('''
518+
from(bucket:"my-bucket") |> range(start: _start)
519+
|> filter(fn: (r) => r["_measurement"] == "my_measurement")
520+
|> filter(fn: (r) => r["_field"] == "temperature")
521+
|> filter(fn: (r) => r["location"] == _location and r["_value"] > _floatParam)
522+
|> aggregateWindow(every: _every, fn: mean, createEmpty: true)
523+
|> sort(columns: ["_time"], desc: _desc)
524+
''', params=p)
525+
526+
for table in tables:
527+
print(table)
528+
for record in table.records:
529+
print(str(record["_time"]) + " - " + record["location"] + ": " + str(record["_value"]))
530+
531+
print()
532+
print()
533+
505534
"""
506535
Query: using Stream
507536
"""

examples/query.py

+38-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import datetime as datetime
2+
13
from influxdb_client import InfluxDBClient, Point, Dialect
24
from influxdb_client.client.write_api import SYNCHRONOUS
35

4-
client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org")
6+
client = InfluxDBClient(url="http://localhost:8086", token="my-token", org="my-org",debug=True)
57

68
write_api = client.write_api(write_options=SYNCHRONOUS)
79
query_api = client.query_api()
@@ -28,6 +30,34 @@
2830
print()
2931
print()
3032

33+
"""
34+
Query: using Bind parameters
35+
"""
36+
37+
p = {"_start": datetime.timedelta(hours=-1),
38+
"_location": "Prague",
39+
"_desc": True,
40+
"_floatParam": 25.1,
41+
"_every": datetime.timedelta(minutes=5)
42+
}
43+
44+
tables = query_api.query('''
45+
from(bucket:"my-bucket") |> range(start: _start)
46+
|> filter(fn: (r) => r["_measurement"] == "my_measurement")
47+
|> filter(fn: (r) => r["_field"] == "temperature")
48+
|> filter(fn: (r) => r["location"] == _location and r["_value"] > _floatParam)
49+
|> aggregateWindow(every: _every, fn: mean, createEmpty: true)
50+
|> sort(columns: ["_time"], desc: _desc)
51+
''', params=p)
52+
53+
for table in tables:
54+
print(table)
55+
for record in table.records:
56+
print(str(record["_time"]) + " - " + record["location"] + ": " + str(record["_value"]))
57+
58+
print()
59+
print()
60+
3161
"""
3262
Query: using Stream
3363
"""
@@ -66,10 +96,13 @@
6696
"""
6797
Query: using Pandas DataFrame
6898
"""
69-
data_frame = query_api.query_data_frame('from(bucket:"my-bucket") '
70-
'|> range(start: -10m) '
71-
'|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") '
72-
'|> keep(columns: ["location", "temperature"])')
99+
data_frame = query_api.query_data_frame('''
100+
from(bucket:"my-bucket")
101+
|> range(start: -10m)
102+
|> filter(fn: (r) => r["_measurement"] == "my_measurement")
103+
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
104+
|> keep(columns: ["_time","location", "temperature"])
105+
''')
73106
print(data_frame.to_string())
74107

75108
"""

influxdb_client/client/query_api.py

+67-14
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66

77
import codecs
88
import csv
9+
from datetime import datetime, timedelta
910
from typing import List, Generator, Any
11+
from pytz import UTC
1012

11-
from influxdb_client import Dialect
13+
from influxdb_client import Dialect, IntegerLiteral, BooleanLiteral, FloatLiteral, DateTimeLiteral, StringLiteral, \
14+
VariableAssignment, Identifier, OptionStatement, File, DurationLiteral, Duration, UnaryExpression
1215
from influxdb_client import Query, QueryService
1316
from influxdb_client.client.flux_csv_parser import FluxCsvParser, FluxSerializationMode
1417
from influxdb_client.client.flux_table import FluxTable, FluxRecord
@@ -29,51 +32,54 @@ def __init__(self, influxdb_client):
2932
self._influxdb_client = influxdb_client
3033
self._query_api = QueryService(influxdb_client.api_client)
3134

32-
def query_csv(self, query: str, org=None, dialect: Dialect = default_dialect):
35+
def query_csv(self, query: str, org=None, dialect: Dialect = default_dialect, params: dict = None):
3336
"""
3437
Execute the Flux query and return results as a CSV iterator. Each iteration returns a row of the CSV file.
3538
3639
:param query: a Flux query
3740
:param org: organization name (optional if already specified in InfluxDBClient)
3841
:param dialect: csv dialect format
42+
:param params: bind parameters
3943
:return: The returned object is an iterator. Each iteration returns a row of the CSV file
4044
(which can span multiple input lines).
4145
"""
4246
if org is None:
4347
org = self._influxdb_client.org
44-
response = self._query_api.post_query(org=org, query=self._create_query(query, dialect), async_req=False,
48+
response = self._query_api.post_query(org=org, query=self._create_query(query, dialect, params), async_req=False,
4549
_preload_content=False)
4650

4751
return csv.reader(codecs.iterdecode(response, 'utf-8'))
4852

49-
def query_raw(self, query: str, org=None, dialect=default_dialect):
53+
def query_raw(self, query: str, org=None, dialect=default_dialect, params: dict = None):
5054
"""
5155
Execute synchronous Flux query and return result as raw unprocessed result as a str.
5256
5357
:param query: a Flux query
5458
:param org: organization name (optional if already specified in InfluxDBClient)
5559
:param dialect: csv dialect format
60+
:param params: bind parameters
5661
:return: str
5762
"""
5863
if org is None:
5964
org = self._influxdb_client.org
60-
result = self._query_api.post_query(org=org, query=self._create_query(query, dialect), async_req=False,
65+
result = self._query_api.post_query(org=org, query=self._create_query(query, dialect, params), async_req=False,
6166
_preload_content=False)
6267

6368
return result
6469

65-
def query(self, query: str, org=None) -> List['FluxTable']:
70+
def query(self, query: str, org=None, params: dict = None) -> List['FluxTable']:
6671
"""
6772
Execute synchronous Flux query and return result as a List['FluxTable'].
6873
6974
:param query: the Flux query
7075
:param org: organization name (optional if already specified in InfluxDBClient)
76+
:param params: bind parameters
7177
:return:
7278
"""
7379
if org is None:
7480
org = self._influxdb_client.org
7581

76-
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect),
82+
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect, params),
7783
async_req=False, _preload_content=False, _return_http_data_only=False)
7884

7985
_parser = FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.tables)
@@ -82,12 +88,14 @@ def query(self, query: str, org=None) -> List['FluxTable']:
8288

8389
return _parser.tables
8490

85-
def query_stream(self, query: str, org=None) -> Generator['FluxRecord', Any, None]:
91+
def query_stream(self, query: str, org=None, params: dict = None) -> Generator['FluxRecord', Any, None]:
8692
"""
8793
Execute synchronous Flux query and return stream of FluxRecord as a Generator['FluxRecord'].
8894
8995
:param query: the Flux query
96+
:param params: the Flux query parameters
9097
:param org: organization name (optional if already specified in InfluxDBClient)
98+
:param params: bind parameters
9199
:return:
92100
"""
93101
if org is None:
@@ -100,7 +108,7 @@ def query_stream(self, query: str, org=None) -> Generator['FluxRecord', Any, Non
100108

101109
return _parser.generator()
102110

103-
def query_data_frame(self, query: str, org=None, data_frame_index: List[str] = None):
111+
def query_data_frame(self, query: str, org=None, data_frame_index: List[str] = None, params: dict = None):
104112
"""
105113
Execute synchronous Flux query and return Pandas DataFrame.
106114
@@ -109,11 +117,12 @@ def query_data_frame(self, query: str, org=None, data_frame_index: List[str] = N
109117
:param query: the Flux query
110118
:param org: organization name (optional if already specified in InfluxDBClient)
111119
:param data_frame_index: the list of columns that are used as DataFrame index
120+
:param params: bind parameters
112121
:return:
113122
"""
114123
from ..extras import pd
115124

116-
_generator = self.query_data_frame_stream(query, org=org, data_frame_index=data_frame_index)
125+
_generator = self.query_data_frame_stream(query, org=org, data_frame_index=data_frame_index, params=params)
117126
_dataFrames = list(_generator)
118127

119128
if len(_dataFrames) == 0:
@@ -123,7 +132,7 @@ def query_data_frame(self, query: str, org=None, data_frame_index: List[str] = N
123132
else:
124133
return _dataFrames
125134

126-
def query_data_frame_stream(self, query: str, org=None, data_frame_index: List[str] = None):
135+
def query_data_frame_stream(self, query: str, org=None, data_frame_index: List[str] = None, params: dict = None):
127136
"""
128137
Execute synchronous Flux query and return stream of Pandas DataFrame as a Generator['pd.DataFrame'].
129138
@@ -132,12 +141,13 @@ def query_data_frame_stream(self, query: str, org=None, data_frame_index: List[s
132141
:param query: the Flux query
133142
:param org: organization name (optional if already specified in InfluxDBClient)
134143
:param data_frame_index: the list of columns that are used as DataFrame index
144+
:param params: bind parameters
135145
:return:
136146
"""
137147
if org is None:
138148
org = self._influxdb_client.org
139149

140-
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect),
150+
response = self._query_api.post_query(org=org, query=self._create_query(query, self.default_dialect, params),
141151
async_req=False, _preload_content=False, _return_http_data_only=False)
142152

143153
_parser = FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.dataFrame,
@@ -146,10 +156,53 @@ def query_data_frame_stream(self, query: str, org=None, data_frame_index: List[s
146156

147157
# private helper for c
148158
@staticmethod
149-
def _create_query(query, dialect=default_dialect):
150-
created = Query(query=query, dialect=dialect)
159+
def _create_query(query, dialect=default_dialect, params: dict = None):
160+
created = Query(query=query, dialect=dialect, extern=QueryApi._build_flux_ast(params))
151161
return created
152162

163+
@staticmethod
164+
def _params_to_extern_ast(params: dict) -> List['OptionStatement']:
165+
166+
statements = []
167+
for key, value in params.items():
168+
169+
if isinstance(value, bool):
170+
literal = BooleanLiteral("BooleanLiteral", value)
171+
elif isinstance(value, int):
172+
literal = IntegerLiteral("IntegerLiteral", str(value))
173+
elif isinstance(value, float):
174+
literal = FloatLiteral("FloatLiteral", value)
175+
elif isinstance(value, datetime):
176+
if not value.tzinfo:
177+
value = UTC.localize(value)
178+
else:
179+
value = value.astimezone(UTC)
180+
literal = DateTimeLiteral("DateTimeLiteral", value.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
181+
elif isinstance(value, timedelta):
182+
# convert to microsecodns
183+
_micro_delta = int(value / timedelta(microseconds=1))
184+
if _micro_delta < 0:
185+
literal = UnaryExpression("UnaryExpression", argument=DurationLiteral("DurationLiteral", [
186+
Duration(magnitude=-_micro_delta, unit="us")]), operator="-")
187+
else:
188+
literal = DurationLiteral("DurationLiteral", [Duration(magnitude=_micro_delta, unit="us")])
189+
elif isinstance(value, str):
190+
literal = StringLiteral("StringLiteral", str(value))
191+
else:
192+
literal = value
193+
194+
statements.append(OptionStatement("OptionStatement",
195+
VariableAssignment("VariableAssignment", Identifier("Identifier", key),
196+
literal)))
197+
return statements
198+
199+
@staticmethod
200+
def _build_flux_ast(params: dict = None):
201+
if params is None:
202+
return None
203+
204+
return File(package=None, name=None, type=None, imports=[], body=QueryApi._params_to_extern_ast(params))
205+
153206
def __del__(self):
154207
"""Close QueryAPI."""
155208
pass

0 commit comments

Comments
 (0)