influxdata-plugin-utils 0.1.0__tar.gz → 0.3.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.
@@ -0,0 +1,63 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.3.0] - 2026-07-31
11
+
12
+ ### Security
13
+
14
+ - `config.load_plugin_config` — disable dynaconf's `@` token substitution
15
+ (`@read_file`, `@format`, `@jinja`, `@get`, and ~30 others) by constructing
16
+ the settings object with `AUTO_CAST_FOR_DYNACONF=False`. Previously any
17
+ string value beginning with `@` was evaluated, so an untrusted value from an
18
+ HTTP request body could read the server's files or environment variables
19
+ (for example `@read_file /etc/passwd` or `@format {env[SECRET]}`). Values are
20
+ now always treated as literal data. See
21
+ [#134](https://github.com/influxdata/influxdb3_plugins/issues/134).
22
+
23
+ ### Changed
24
+
25
+ - Pin `dynaconf>=3.2,<4` so a future major release cannot silently re-enable
26
+ token substitution.
27
+
28
+ ## [0.2.0] - 2026-07-12
29
+
30
+ ### Added
31
+
32
+ - `write.write_data` — optional `database` parameter for writing to another
33
+ database.
34
+ - `introspection` — optional `database` parameter for schema helpers and
35
+ `query_window`.
36
+ - `parsing.parse_timedelta` — `ms` (milliseconds) and `us` (microseconds)
37
+ duration units.
38
+
39
+ ### Changed
40
+
41
+ - `write.write_data` — `no_sync` now defaults to `None`: writes go through
42
+ `write` / `write_to_db` (available on all InfluxDB 3 versions); passing a
43
+ boolean switches to `write_sync` / `write_sync_to_db` (InfluxDB 3.8+).
44
+
45
+ ## [0.1.0] - 2026-07-08
46
+
47
+ ### Added
48
+
49
+ - `config` — dynaconf-backed config loading (`load_plugin_config`), plugin
50
+ directory resolution (`resolve_plugin_dir`, `resolve_path`), re-exported
51
+ `Validator`.
52
+ - `introspection` — schema helpers (`get_table_names`, `get_tag_names`,
53
+ `get_field_names`) and `query_window`, with optional TTL caching.
54
+ - `parsing` — `parse_timedelta`, `parse_timestamp_ns`, `parse_int`,
55
+ `parse_bool`, `parse_delimited_list`, `parse_key_value`.
56
+ - `cache` — `cached` TTL wrapper over `influxdb3_local.cache`.
57
+ - `write` — `build_line`, `build_line_typed`, `add_field_with_type`,
58
+ `write_data` (batching + retry), `BatchLines`.
59
+
60
+ [Unreleased]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.3.0...HEAD
61
+ [0.3.0]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.2.0...utils-v0.3.0
62
+ [0.2.0]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.1.0...utils-v0.2.0
63
+ [0.1.0]: https://github.com/influxdata/influxdb3_plugins/releases/tag/utils-v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: influxdata-plugin-utils
3
- Version: 0.1.0
3
+ Version: 0.3.0
4
4
  Summary: Shared helpers for InfluxDB 3 plugins.
5
5
  Project-URL: Homepage, https://github.com/influxdata/influxdb3_plugins
6
6
  Project-URL: Repository, https://github.com/influxdata/influxdb3_plugins
@@ -12,7 +12,7 @@ Keywords: influxdb,influxdb3,plugins
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Requires-Python: >=3.11
15
- Requires-Dist: dynaconf>=3.2
15
+ Requires-Dist: dynaconf<4,>=3.2
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # influxdata-plugin-utils
@@ -36,7 +36,7 @@ pip install -e influxdata-plugin-utils
36
36
  | Module | What it provides |
37
37
  |-----------------|-----------------------------------------------------------------------------------------------------------------------------|
38
38
  | `config` | `load_plugin_config(args, validators)` (dynaconf-backed), `resolve_plugin_dir()`, `resolve_path()`, re-exported `Validator` |
39
- | `introspection` | `get_table_names()`, `get_tag_names()`, `get_field_names()`, `query_window()` |
39
+ | `introspection` | `get_table_names()`, `get_tag_names()`, `get_field_names()`, `query_window()` with optional `database=` |
40
40
  | `parsing` | `parse_timedelta()`, `parse_timestamp_ns()`, `parse_int()`, `parse_bool()`, `parse_delimited_list()`, `parse_key_value()` |
41
41
  | `cache` | `cached(influxdb3_local, key, producer, ttl_seconds=3600)` |
42
42
  | `write` | `build_line()`, `build_line_typed()`, `add_field_with_type()`, `write_data()`, `BatchLines` |
@@ -82,9 +82,32 @@ lines = [
82
82
  ]
83
83
  write_data(influxdb3_local, lines) # batched + retried by default
84
84
  # write_data(influxdb3_local, lines, batch=False, retries=0) # opt out
85
+ # write_data(influxdb3_local, lines, database="other_db") # another database
86
+ # write_data(influxdb3_local, lines, no_sync=True) # write_sync API (3.8+)
87
+ ```
88
+
89
+ ## Cross-database queries
90
+
91
+ On InfluxDB versions that support processing-engine cross-database queries,
92
+ the introspection helpers accept `database=` and pass it through to
93
+ `influxdb3_local.query`.
94
+ Cached schema results are separated per database.
95
+
96
+ ```python
97
+ from influxdata_plugin_utils.introspection import get_field_names, query_window
98
+
99
+ fields = get_field_names(influxdb3_local, "cpu", database="source_db")
100
+ rows = query_window(
101
+ influxdb3_local,
102
+ "cpu",
103
+ start=start,
104
+ end=end,
105
+ columns=fields,
106
+ database="source_db",
107
+ )
85
108
  ```
86
109
 
87
110
  ## License
88
111
 
89
112
  Licensed under either of [Apache License 2.0](LICENSE-APACHE) or
90
- [MIT license](LICENSE-MIT) at your option.
113
+ [MIT license](LICENSE-MIT) at your option.
@@ -19,7 +19,7 @@ pip install -e influxdata-plugin-utils
19
19
  | Module | What it provides |
20
20
  |-----------------|-----------------------------------------------------------------------------------------------------------------------------|
21
21
  | `config` | `load_plugin_config(args, validators)` (dynaconf-backed), `resolve_plugin_dir()`, `resolve_path()`, re-exported `Validator` |
22
- | `introspection` | `get_table_names()`, `get_tag_names()`, `get_field_names()`, `query_window()` |
22
+ | `introspection` | `get_table_names()`, `get_tag_names()`, `get_field_names()`, `query_window()` with optional `database=` |
23
23
  | `parsing` | `parse_timedelta()`, `parse_timestamp_ns()`, `parse_int()`, `parse_bool()`, `parse_delimited_list()`, `parse_key_value()` |
24
24
  | `cache` | `cached(influxdb3_local, key, producer, ttl_seconds=3600)` |
25
25
  | `write` | `build_line()`, `build_line_typed()`, `add_field_with_type()`, `write_data()`, `BatchLines` |
@@ -65,9 +65,32 @@ lines = [
65
65
  ]
66
66
  write_data(influxdb3_local, lines) # batched + retried by default
67
67
  # write_data(influxdb3_local, lines, batch=False, retries=0) # opt out
68
+ # write_data(influxdb3_local, lines, database="other_db") # another database
69
+ # write_data(influxdb3_local, lines, no_sync=True) # write_sync API (3.8+)
70
+ ```
71
+
72
+ ## Cross-database queries
73
+
74
+ On InfluxDB versions that support processing-engine cross-database queries,
75
+ the introspection helpers accept `database=` and pass it through to
76
+ `influxdb3_local.query`.
77
+ Cached schema results are separated per database.
78
+
79
+ ```python
80
+ from influxdata_plugin_utils.introspection import get_field_names, query_window
81
+
82
+ fields = get_field_names(influxdb3_local, "cpu", database="source_db")
83
+ rows = query_window(
84
+ influxdb3_local,
85
+ "cpu",
86
+ start=start,
87
+ end=end,
88
+ columns=fields,
89
+ database="source_db",
90
+ )
68
91
  ```
69
92
 
70
93
  ## License
71
94
 
72
95
  Licensed under either of [Apache License 2.0](LICENSE-APACHE) or
73
- [MIT license](LICENSE-MIT) at your option.
96
+ [MIT license](LICENSE-MIT) at your option.
@@ -12,7 +12,7 @@ license = "MIT OR Apache-2.0"
12
12
  license-files = ["LICENSE-MIT", "LICENSE-APACHE"]
13
13
  authors = [{ name = "InfluxData" }]
14
14
  keywords = ["influxdb", "influxdb3", "plugins"]
15
- dependencies = ["dynaconf>=3.2"]
15
+ dependencies = ["dynaconf>=3.2,<4"]
16
16
  classifiers = [
17
17
  "Programming Language :: Python :: 3.11",
18
18
  "Operating System :: OS Independent",
@@ -8,7 +8,7 @@ Modules:
8
8
  write - LineBuilder builders and resilient write_data
9
9
  """
10
10
 
11
- __version__ = "0.1.0"
11
+ __version__ = "0.3.0"
12
12
 
13
13
  from . import cache, config, introspection, parsing, write
14
14
  from .cache import cached
@@ -4,6 +4,11 @@ Loads a plugin's TOML config (resolved via the plugin directory), merges
4
4
  environment variables and engine-supplied ``args``, and validates the result.
5
5
  dynaconf is an implementation detail and must not leak into plugin code beyond
6
6
  the re-exported ``Validator``.
7
+
8
+ All values are treated as literal data. dynaconf's ``@`` token substitution
9
+ (``@read_file``, ``@format``, ``@get``, ...) is disabled so that a value
10
+ beginning with ``@`` is never evaluated against the server's filesystem or
11
+ environment; see https://github.com/influxdata/influxdb3_plugins/issues/134.
7
12
  """
8
13
 
9
14
  import os
@@ -104,8 +109,14 @@ def load_plugin_config(
104
109
  with open(resolve_path(config_file_path), "rb") as config_file:
105
110
  layers.update(tomllib.load(config_file))
106
111
 
107
- # loaders=[] disables the DYNACONF_* env loader; env is read only via env_keys
108
- settings = Dynaconf(loaders=[])
112
+ # loaders=[] disables the DYNACONF_* env loader; env is read only via
113
+ # env_keys. AUTO_CAST_FOR_DYNACONF=False disables dynaconf's "@" token
114
+ # substitution (@read_file, @format, @jinja, @get, ... — ~30 tokens, each
115
+ # beginning with "@"). Without it, any string value that begins with "@" is
116
+ # evaluated instead of stored as data, so an untrusted value arriving in an
117
+ # HTTP request body could read the server's files or environment variables.
118
+ # See https://github.com/influxdata/influxdb3_plugins/issues/134.
119
+ settings = Dynaconf(loaders=[], AUTO_CAST_FOR_DYNACONF=False)
109
120
  settings.update(layers)
110
121
  if validators:
111
122
  settings.validators.register(*validators)
@@ -2,6 +2,8 @@
2
2
 
3
3
  Each helper takes ``influxdb3_local`` explicitly and may optionally use the
4
4
  TTL cache. Queries mirror the patterns already used across the plugins.
5
+ Helpers accept an optional ``database`` argument for engines that support
6
+ cross-database plugin queries.
5
7
  """
6
8
 
7
9
  from .cache import cached
@@ -22,13 +24,40 @@ def _quote_identifier(identifier: str) -> str:
22
24
  return '"' + identifier.replace('"', '""') + '"'
23
25
 
24
26
 
27
+ def _query(
28
+ influxdb3_local,
29
+ query: str,
30
+ args: dict | None = None,
31
+ database: str | None = None,
32
+ ):
33
+ """Run a plugin query, preserving the old call shape when no database is set."""
34
+ if database is None:
35
+ if args is None:
36
+ return influxdb3_local.query(query)
37
+ return influxdb3_local.query(query, args)
38
+ if args is None:
39
+ return influxdb3_local.query(query, database=database)
40
+ return influxdb3_local.query(query, args, database=database)
41
+
42
+
43
+ def _cache_key(base: str, database: str | None) -> str:
44
+ """Return a cache key, keeping existing default-database keys unchanged."""
45
+ if database is None:
46
+ return base
47
+ return f"{base}:database:{database}"
48
+
49
+
25
50
  def get_table_names(
26
- influxdb3_local, *, use_cache: bool = True, ttl_seconds: int = 3600
51
+ influxdb3_local,
52
+ *,
53
+ database: str | None = None,
54
+ use_cache: bool = True,
55
+ ttl_seconds: int = 3600,
27
56
  ) -> list[str]:
28
57
  """Return base table names via ``SHOW TABLES``."""
29
58
 
30
59
  def producer() -> list[str]:
31
- rows = influxdb3_local.query("SHOW TABLES")
60
+ rows = _query(influxdb3_local, "SHOW TABLES", database=database)
32
61
  return [
33
62
  row["table_name"]
34
63
  for row in rows
@@ -36,12 +65,22 @@ def get_table_names(
36
65
  ]
37
66
 
38
67
  if use_cache:
39
- return cached(influxdb3_local, "shared:tables", producer, ttl_seconds=ttl_seconds)
68
+ return cached(
69
+ influxdb3_local,
70
+ _cache_key("shared:tables", database),
71
+ producer,
72
+ ttl_seconds=ttl_seconds,
73
+ )
40
74
  return producer()
41
75
 
42
76
 
43
77
  def get_tag_names(
44
- influxdb3_local, table: str, *, use_cache: bool = True, ttl_seconds: int = 3600
78
+ influxdb3_local,
79
+ table: str,
80
+ *,
81
+ database: str | None = None,
82
+ use_cache: bool = True,
83
+ ttl_seconds: int = 3600,
45
84
  ) -> list[str]:
46
85
  """Return tag column names of a table (``Dictionary(Int32, Utf8)`` columns)."""
47
86
 
@@ -50,14 +89,20 @@ def get_tag_names(
50
89
  "SELECT column_name FROM information_schema.columns "
51
90
  "WHERE table_name = $table AND data_type = $data_type"
52
91
  )
53
- rows = influxdb3_local.query(
54
- query, {"table": table, "data_type": _TAG_DATA_TYPE}
92
+ rows = _query(
93
+ influxdb3_local,
94
+ query,
95
+ {"table": table, "data_type": _TAG_DATA_TYPE},
96
+ database=database,
55
97
  )
56
98
  return [row["column_name"] for row in rows]
57
99
 
58
100
  if use_cache:
59
101
  return cached(
60
- influxdb3_local, f"shared:tags:{table}", producer, ttl_seconds=ttl_seconds
102
+ influxdb3_local,
103
+ _cache_key(f"shared:tags:{table}", database),
104
+ producer,
105
+ ttl_seconds=ttl_seconds,
61
106
  )
62
107
  return producer()
63
108
 
@@ -67,6 +112,7 @@ def get_field_names(
67
112
  table: str,
68
113
  *,
69
114
  numeric_only: bool = False,
115
+ database: str | None = None,
70
116
  use_cache: bool = True,
71
117
  ttl_seconds: int = 3600,
72
118
  ) -> list[str]:
@@ -80,7 +126,7 @@ def get_field_names(
80
126
  "SELECT column_name, data_type FROM information_schema.columns "
81
127
  "WHERE table_name = $table"
82
128
  )
83
- rows = influxdb3_local.query(query, {"table": table})
129
+ rows = _query(influxdb3_local, query, {"table": table}, database=database)
84
130
  names: list[str] = []
85
131
  for row in rows:
86
132
  name = row["column_name"]
@@ -93,7 +139,7 @@ def get_field_names(
93
139
  return names
94
140
 
95
141
  if use_cache:
96
- key = f"shared:fields:{table}:{int(numeric_only)}"
142
+ key = _cache_key(f"shared:fields:{table}:{int(numeric_only)}", database)
97
143
  return cached(influxdb3_local, key, producer, ttl_seconds=ttl_seconds)
98
144
  return producer()
99
145
 
@@ -105,6 +151,7 @@ def query_window(
105
151
  start,
106
152
  end,
107
153
  columns: list[str] | None = None,
154
+ database: str | None = None,
108
155
  ) -> list[dict]:
109
156
  """Run a basic ``time``-window query and return rows (``[]`` if none).
110
157
 
@@ -118,5 +165,10 @@ def query_window(
118
165
  f"SELECT {selected} FROM {_quote_identifier(table)} "
119
166
  "WHERE time >= $start AND time < $end ORDER BY time"
120
167
  )
121
- rows = influxdb3_local.query(query, {"start": start, "end": end})
122
- return rows or []
168
+ rows = _query(
169
+ influxdb3_local,
170
+ query,
171
+ {"start": start, "end": end},
172
+ database=database,
173
+ )
174
+ return rows or []
@@ -19,6 +19,8 @@ __all__ = [
19
19
 
20
20
  _DURATION_RE = re.compile(r"^\s*(\d+)\s*([a-zA-Z]+)\s*$")
21
21
  _DURATION_UNITS = {
22
+ "us": "microseconds",
23
+ "ms": "milliseconds",
22
24
  "s": "seconds",
23
25
  "min": "minutes",
24
26
  "h": "hours",
@@ -33,7 +35,7 @@ _FALSE = {"false", "f", "0", "no", "off"}
33
35
 
34
36
 
35
37
  def parse_timedelta(raw) -> timedelta:
36
- """Parse a duration like ``30s``, ``5min``, ``1h``, ``2d``, ``1w``."""
38
+ """Parse a duration like ``500us``, ``100ms``, ``30s``, ``5min``, ``1h``, ``2d``, ``1w``."""
37
39
  if isinstance(raw, timedelta):
38
40
  return raw
39
41
  match = _DURATION_RE.match(str(raw))
@@ -9,6 +9,7 @@ write helpers operate on already-built line objects and need no class.
9
9
  import math
10
10
  import random
11
11
  import time
12
+ from functools import partial
12
13
 
13
14
  from .parsing import parse_bool
14
15
 
@@ -160,7 +161,8 @@ def write_data(
160
161
  batch: bool = True,
161
162
  retries: int = 3,
162
163
  base_delay: float = 1.0,
163
- no_sync: bool = True,
164
+ no_sync: bool | None = None,
165
+ database: str | None = None,
164
166
  ) -> None:
165
167
  """Write LineBuilder objects with optional batching and retry.
166
168
 
@@ -170,8 +172,17 @@ def write_data(
170
172
  batch: Combine all lines into one payload via :class:`BatchLines`.
171
173
  Set ``False`` to write each line individually.
172
174
  retries: Number of extra attempts on failure (``0`` disables retry).
175
+ Effective only when ``no_sync`` is set: the default buffered
176
+ ``write`` / ``write_to_db`` never raise at call time.
173
177
  base_delay: Base seconds for exponential backoff with jitter.
174
- no_sync: Passed through to ``write_sync``.
178
+ no_sync: When set, writes go through ``write_sync`` /
179
+ ``write_sync_to_db`` with this flag (requires InfluxDB 3.8+),
180
+ which write immediately and raise on failure. When ``None``
181
+ (default), the universally available ``write`` / ``write_to_db``
182
+ are used: they only queue lines that are flushed after plugin
183
+ execution completes, so failures are not reported to the caller.
184
+ database: Target database; when ``None``, writes to the trigger's
185
+ database.
175
186
  """
176
187
  builders = [builder for builder in line_builders if builder is not None]
177
188
  if not builders:
@@ -180,13 +191,23 @@ def write_data(
180
191
  payloads = [BatchLines(builders)] if batch else builders
181
192
  attempts = max(retries, 0) + 1
182
193
 
194
+ if no_sync is None:
195
+ if database is not None:
196
+ write_fn = partial(influxdb3_local.write_to_db, database)
197
+ else:
198
+ write_fn = influxdb3_local.write
199
+ elif database is not None:
200
+ write_fn = partial(influxdb3_local.write_sync_to_db, database, no_sync=no_sync)
201
+ else:
202
+ write_fn = partial(influxdb3_local.write_sync, no_sync=no_sync)
203
+
183
204
  for payload in payloads:
184
205
  for attempt in range(attempts):
185
206
  try:
186
- influxdb3_local.write_sync(payload, no_sync=no_sync)
207
+ write_fn(payload)
187
208
  break
188
209
  except Exception:
189
210
  if attempt == attempts - 1:
190
211
  raise
191
212
  delay = (2**attempt) * base_delay + random.uniform(0, base_delay)
192
- time.sleep(delay)
213
+ time.sleep(delay)
@@ -1,26 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.1.0] - 2026-07-08
11
-
12
- ### Added
13
-
14
- - `config` — dynaconf-backed config loading (`load_plugin_config`), plugin
15
- directory resolution (`resolve_plugin_dir`, `resolve_path`), re-exported
16
- `Validator`.
17
- - `introspection` — schema helpers (`get_table_names`, `get_tag_names`,
18
- `get_field_names`) and `query_window`, with optional TTL caching.
19
- - `parsing` — `parse_timedelta`, `parse_timestamp_ns`, `parse_int`,
20
- `parse_bool`, `parse_delimited_list`, `parse_key_value`.
21
- - `cache` — `cached` TTL wrapper over `influxdb3_local.cache`.
22
- - `write` — `build_line`, `build_line_typed`, `add_field_with_type`,
23
- `write_data` (batching + retry), `BatchLines`.
24
-
25
- [Unreleased]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.1.0...HEAD
26
- [0.1.0]: https://github.com/influxdata/influxdb3_plugins/releases/tag/utils-v0.1.0