influxdata-plugin-utils 0.3.0__tar.gz → 0.4.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 (22) hide show
  1. influxdata_plugin_utils-0.4.0/CHANGELOG.md +188 -0
  2. influxdata_plugin_utils-0.4.0/PKG-INFO +268 -0
  3. influxdata_plugin_utils-0.4.0/README.md +252 -0
  4. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/pyproject.toml +1 -1
  5. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/__init__.py +42 -6
  6. influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/_utils.py +41 -0
  7. influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/cache.py +41 -0
  8. influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/config.py +158 -0
  9. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/introspection.py +48 -1
  10. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/parsing.py +1 -1
  11. influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/sources.py +539 -0
  12. influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/validation.py +309 -0
  13. influxdata_plugin_utils-0.3.0/CHANGELOG.md +0 -63
  14. influxdata_plugin_utils-0.3.0/PKG-INFO +0 -113
  15. influxdata_plugin_utils-0.3.0/README.md +0 -96
  16. influxdata_plugin_utils-0.3.0/src/influxdata_plugin_utils/cache.py +0 -25
  17. influxdata_plugin_utils-0.3.0/src/influxdata_plugin_utils/config.py +0 -124
  18. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/.gitignore +0 -0
  19. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/LICENSE-APACHE +0 -0
  20. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/LICENSE-MIT +0 -0
  21. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/py.typed +0 -0
  22. {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/write.py +0 -0
@@ -0,0 +1,188 @@
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.4.0] - 2026-09-12
11
+
12
+ ### Added
13
+
14
+ - `sources` module: one parser per place configuration comes from —
15
+ `parse_trigger_args()`, `parse_toml()`, `parse_env()`, `parse_json_body()`,
16
+ `parse_request_headers()` and `parse_query_parameters()`. Each reads one raw
17
+ input and returns a plain dict, so a plugin composes the layers it needs.
18
+ - `sources.KeySpec` says which keys of a source become config values and under
19
+ what names: `allowlist`, `denylist`, `rename`, and an `unknown` policy that
20
+ either drops a refused key or names it back to whoever sent it. `parse_env`
21
+ requires an allowlist, since the process environment belongs to the host.
22
+ - `sources.parse_toml` refuses a path that does not name a `.toml` file before
23
+ opening it; `require_suffix=False` lifts that for a config file named some
24
+ other way, and `sources.is_toml_path()` answers the same question on its own,
25
+ for a plugin that would rather report the path itself than raise.
26
+ - `config.load_config(*layers, validators=...)` merges the layers in the order
27
+ given — lowest precedence first — and validates the result once.
28
+ - `config.merge_config_layers(*layers, pinned=...)` merges without validating
29
+ and can hold chosen keys against the layers above them, so a request cannot
30
+ move what the operator fixed.
31
+ - `config.Config`, the validated configuration: a dict that also answers to
32
+ attribute access.
33
+ - `sources.parse_request_headers` folds only the casing of a header name, which
34
+ RFC 9110 makes meaningless; a name is otherwise kept as written, and `rename`
35
+ gives a key another name.
36
+ - A header or query parameter the plugin asked for that arrives more than once
37
+ is refused rather than resolved by the order the runtime delivers them in —
38
+ `multi=True` reads every value as a list instead.
39
+ - `validation` module: `Validator` and `validate()`. A rule carries a default,
40
+ a `cast`, and checks — 25 of them, from `gte` and `is_in` to `regex` — plus
41
+ `condition` for an arbitrary predicate and `when` to apply a rule only while
42
+ another one holds. A list, dict or set `default` is copied for each use, so
43
+ one rule's default cannot be changed through the values it fills in. Checks
44
+ are named explicitly, so a misspelled one is a `TypeError` where the rule is
45
+ written.
46
+ - `introspection.get_schema(influxdb3_local, table)` returns
47
+ `{column_name: data_type}` from one `information_schema` query.
48
+ - `cache.cached()` gains two parameters: `refresh` replaces a stored entry, and
49
+ `cache_empty=False` leaves a falsy result unstored. Combined, a refresh that
50
+ produces an empty value drops the entry, so a table dropped between reads
51
+ leaves neither its old schema cached nor an empty one that would never be
52
+ retried. Every introspection lookup passes `cache_empty=False`, so an empty
53
+ answer is retried rather than remembered; `get_schema()` also forwards
54
+ `refresh`, letting a caller re-read a schema on seeing an unknown column.
55
+
56
+ ### Changed
57
+
58
+ - `config.load_plugin_config` reads the same three layers as before — the named
59
+ environment variables, the trigger arguments, the TOML file — and returns a
60
+ `Config`. A value that arrives empty is left out of its own layer, so a blank
61
+ trigger argument lets a validator default apply and a blank in the file no
62
+ longer erases the argument underneath it. Every failure is a `ValueError`,
63
+ including an unreadable file and a rejected value, and a `config_file_path`
64
+ that does not name a `.toml` file is now refused before the file is opened.
65
+ It stays supported, and `load_config` is the one to reach for in new plugins.
66
+ - Configuration keys are stored as they arrive. Nothing in a layer is
67
+ interpreted, whatever a value spells, and a key is matched exactly as
68
+ written — except header names, which become lower-case config keys
69
+ (`X-Api-Key` -> `x-api-key`) because their spelling comes from the protocol.
70
+ A validator name is read the same way: dynaconf matched `Validator("Rows")`
71
+ to a `rows` key and walked `Validator("a.b")` into a nested dict, where here
72
+ `a.b` is the name of a flat key and a rule named `Rows` finds nothing while
73
+ the layer carries `rows`.
74
+
75
+ ### Removed
76
+
77
+ - The `dynaconf` dependency. The package now has none. `Validator` keeps the
78
+ argument names for the subset the plugins use, so most rules port unchanged,
79
+ but these habits from dynaconf no longer hold:
80
+ - `required` is a plain "this key must carry a usable value", and `False` is
81
+ simply no rule. In dynaconf `required` was an alias for `must_exist`, so
82
+ both `required=False` and `must_exist=False` meant "this key must be
83
+ absent" and raised when it was present. `must_exist` is gone; a rule that
84
+ read `required=False` there says nothing here.
85
+ - a string `default` is stored as written. dynaconf read it as TOML, so
86
+ `default="5"` arrived as the number `5` and `default="5", gte=1` passed;
87
+ now that rule fails, and `cast=int` is how a string default becomes a
88
+ number.
89
+ - a callable `default` is stored as the callable itself. dynaconf called it
90
+ with `(settings, validator)` and kept what it returned.
91
+ - a `when` rule does not hold while its key is unset. dynaconf read an absent
92
+ key carrying no existence rule as passing, so
93
+ `Validator("ripple", required=True, when=Validator("prototype", eq="cheby1"))`
94
+ demanded `ripple` from a configuration that never mentioned `prototype`;
95
+ here the rule waits until `prototype` is set.
96
+ - a `when` rule judges a copy, so its own `default` and `cast` are not kept.
97
+ dynaconf applied them to the settings on the way past, so
98
+ `Validator("k", lte=10, when=Validator("k", cast=int))` compared a number
99
+ there and compares the string here. Put the `cast` on the rule itself.
100
+ - a `when` rule that cannot judge -- its own `cast` or predicate raising --
101
+ is reported against the rule that asked, naming both ends: `window: its
102
+ condition could not be checked: rows: invalid literal for int() with base
103
+ 10: 'abc'`. dynaconf let the guard's own error out, which named the guarded
104
+ key and never mentioned the rule being written.
105
+ - every failure is a `ValueError`. dynaconf raised `ValidationError`, which is
106
+ not one, and let anything but a `TypeError` out of a cast, a condition or a
107
+ check untouched: `AttributeError` from `startswith` on a number,
108
+ `re.PatternError` from a bad pattern, `KeyError` from a cast.
109
+ - `required=True` is not satisfied by `""`, whitespace or `None`, and it is
110
+ read before `cast`. dynaconf cast first, so `cast=str` turned `None` into
111
+ the string `"None"` and the rule passed.
112
+ - values keep their Python types. dynaconf handed a rule containers of its
113
+ own -- a tuple arrived as a list, a dict as a case-insensitive mapping --
114
+ so `contains="A"` passed on `{"a": 1}` there and fails here.
115
+ - a misspelled check is a `TypeError` where the rule is written. dynaconf
116
+ took any unknown name as a check and raised `AttributeError` at validation
117
+ time, or passed in silence while the key was absent.
118
+ - `env`, `messages`, `description`, `items_validators`, the `|` and `&`
119
+ combinators, `validate_all` and `only`/`exclude` are not carried over. Every
120
+ rule in the list is applied, too: dynaconf's `register` dropped a rule equal
121
+ to an earlier one, and compared everything except `default`.
122
+
123
+ ## [0.3.1] - 2026-08-03
124
+
125
+ ### Changed
126
+
127
+ - No functional changes. Released to exercise the tag-triggered release
128
+ automation added in
129
+ [#137](https://github.com/influxdata/influxdb3_plugins/pull/137), which
130
+ builds release notes from this file and publishes a GitHub release
131
+ alongside the PyPI upload.
132
+
133
+ ## [0.3.0] - 2026-07-31
134
+
135
+ ### Security
136
+
137
+ - `config.load_plugin_config` — disable dynaconf's `@` token substitution
138
+ (`@read_file`, `@format`, `@jinja`, `@get`, and ~30 others) by constructing
139
+ the settings object with `AUTO_CAST_FOR_DYNACONF=False`. Previously any
140
+ string value beginning with `@` was evaluated, so an untrusted value from an
141
+ HTTP request body could read the server's files or environment variables
142
+ (for example `@read_file /etc/passwd` or `@format {env[SECRET]}`). Values are
143
+ now always treated as literal data. See
144
+ [#134](https://github.com/influxdata/influxdb3_plugins/issues/134).
145
+
146
+ ### Changed
147
+
148
+ - Pin `dynaconf>=3.2,<4` so a future major release cannot silently re-enable
149
+ token substitution.
150
+
151
+ ## [0.2.0] - 2026-07-12
152
+
153
+ ### Added
154
+
155
+ - `write.write_data` — optional `database` parameter for writing to another
156
+ database.
157
+ - `introspection` — optional `database` parameter for schema helpers and
158
+ `query_window`.
159
+ - `parsing.parse_timedelta` — `ms` (milliseconds) and `us` (microseconds)
160
+ duration units.
161
+
162
+ ### Changed
163
+
164
+ - `write.write_data` — `no_sync` now defaults to `None`: writes go through
165
+ `write` / `write_to_db` (available on all InfluxDB 3 versions); passing a
166
+ boolean switches to `write_sync` / `write_sync_to_db` (InfluxDB 3.8+).
167
+
168
+ ## [0.1.0] - 2026-07-08
169
+
170
+ ### Added
171
+
172
+ - `config` — dynaconf-backed config loading (`load_plugin_config`), plugin
173
+ directory resolution (`resolve_plugin_dir`, `resolve_path`), re-exported
174
+ `Validator`.
175
+ - `introspection` — schema helpers (`get_table_names`, `get_tag_names`,
176
+ `get_field_names`) and `query_window`, with optional TTL caching.
177
+ - `parsing` — `parse_timedelta`, `parse_timestamp_ns`, `parse_int`,
178
+ `parse_bool`, `parse_delimited_list`, `parse_key_value`.
179
+ - `cache` — `cached` TTL wrapper over `influxdb3_local.cache`.
180
+ - `write` — `build_line`, `build_line_typed`, `add_field_with_type`,
181
+ `write_data` (batching + retry), `BatchLines`.
182
+
183
+ [Unreleased]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.4.0...HEAD
184
+ [0.4.0]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.3.1...utils-v0.4.0
185
+ [0.3.1]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.3.0...utils-v0.3.1
186
+ [0.3.0]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.2.0...utils-v0.3.0
187
+ [0.2.0]: https://github.com/influxdata/influxdb3_plugins/compare/utils-v0.1.0...utils-v0.2.0
188
+ [0.1.0]: https://github.com/influxdata/influxdb3_plugins/releases/tag/utils-v0.1.0
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.5
2
+ Name: influxdata-plugin-utils
3
+ Version: 0.4.0
4
+ Summary: Shared helpers for InfluxDB 3 plugins.
5
+ Project-URL: Homepage, https://github.com/influxdata/influxdb3_plugins
6
+ Project-URL: Repository, https://github.com/influxdata/influxdb3_plugins
7
+ Author: InfluxData
8
+ License-Expression: MIT OR Apache-2.0
9
+ License-File: LICENSE-APACHE
10
+ License-File: LICENSE-MIT
11
+ Keywords: influxdb,influxdb3,plugins
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+
17
+ # influxdata-plugin-utils
18
+
19
+ Shared helpers for InfluxDB 3 plugins.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install influxdata-plugin-utils
25
+ ```
26
+
27
+ Editable, for local development:
28
+
29
+ ```bash
30
+ pip install -e influxdata-plugin-utils
31
+ ```
32
+
33
+ ## Modules
34
+
35
+ | Module | What it provides |
36
+ |-----------------|----------------------------------------------------------------------------------------------------------------------------------------------|
37
+ | `sources` | `KeySpec`, `parse_trigger_args()`, `parse_toml()`, `parse_env()`, `parse_json_body()`, `parse_request_headers()`, `parse_query_parameters()` |
38
+ | `config` | `load_config()`, `load_plugin_config()`, `merge_config_layers()`, `Config`, `resolve_plugin_dir()`, `resolve_path()` |
39
+ | `validation` | `Validator`, `validate()` |
40
+ | `introspection` | `get_table_names()`, `get_tag_names()`, `get_field_names()`, `get_schema()`, `query_window()` with optional `database=` |
41
+ | `parsing` | `parse_timedelta()`, `parse_timestamp_ns()`, `parse_int()`, `parse_bool()`, `parse_delimited_list()`, `parse_key_value()` |
42
+ | `cache` | `cached(influxdb3_local, key, producer, ttl_seconds=3600, refresh=False, cache_empty=True)` |
43
+ | `write` | `build_line()`, `build_line_typed()`, `add_field_with_type()`, `write_data()`, `BatchLines` |
44
+
45
+ The package has no dependencies, and every module raises `ValueError` on bad
46
+ input, so a plugin answers a bad configuration from one `except` clause.
47
+
48
+ ## Configuration
49
+
50
+ Configuration reaches a plugin from several places: the trigger arguments, a
51
+ TOML file, environment variables, and — for `process_request` plugins — the
52
+ request body, its headers and its query string. Each of those is a **source**
53
+ with its own parser, and each parser returns a plain dict.
54
+
55
+ `load_config` merges the dicts you give it and validates the result. The
56
+ argument order is the precedence, lowest first.
57
+
58
+ ```python
59
+ from influxdata_plugin_utils.config import load_config
60
+ from influxdata_plugin_utils.parsing import parse_int, parse_timedelta
61
+ from influxdata_plugin_utils.sources import (
62
+ KeySpec,
63
+ parse_env,
64
+ parse_json_body,
65
+ parse_query_parameters,
66
+ parse_request_headers,
67
+ parse_toml,
68
+ parse_trigger_args,
69
+ )
70
+ from influxdata_plugin_utils.validation import Validator
71
+
72
+ BODY = KeySpec(allowlist=["measurement", "field", "window"], unknown="reject")
73
+ QUERY = KeySpec(allowlist=["window"])
74
+ HEADERS = KeySpec(allowlist=["x-api-key"], rename={"x-api-key": "api_key"})
75
+ ENV = KeySpec(allowlist=["PLUGIN_API_KEY"], rename={"PLUGIN_API_KEY": "api_key"})
76
+
77
+ VALIDATORS = [
78
+ Validator("measurement", required=True),
79
+ Validator("api_key", required=True),
80
+ Validator("window", default="1h", cast=parse_timedelta),
81
+ Validator("limit", default=1000, cast=parse_int, gte=1, lte=10_000),
82
+ ]
83
+
84
+ def process_request(
85
+ influxdb3_local, query_parameters, request_headers, request_body, args=None
86
+ ):
87
+ cfg = load_config(
88
+ parse_env(ENV),
89
+ parse_trigger_args(args),
90
+ parse_toml(args.get("config_file_path") if args else None),
91
+ parse_json_body(request_body, BODY),
92
+ parse_request_headers(request_headers, HEADERS),
93
+ parse_query_parameters(query_parameters, QUERY),
94
+ validators=VALIDATORS,
95
+ )
96
+ influxdb3_local.info(f"{cfg.measurement} window={cfg['window']}")
97
+ ```
98
+
99
+ `load_plugin_config` covers three layers in a fixed order — the named
100
+ environment variables, the trigger arguments, and the file at
101
+ `config_file_path`:
102
+
103
+ ```python
104
+ from influxdata_plugin_utils.config import load_plugin_config
105
+
106
+ def process_scheduled_call(influxdb3_local, call_time, args):
107
+ cfg = load_plugin_config(args, validators=VALIDATORS, env_keys=["PLUGIN_API_KEY"])
108
+ ```
109
+
110
+ Pass `source="args"` or `source="toml"` to use only one of the last two. Prefer
111
+ `load_config` in new plugins: there the layers are ordinary arguments, so a
112
+ plugin adds, reorders or drops any of them.
113
+
114
+ ### What a source contributes
115
+
116
+ A `KeySpec` says which keys of a source become config values and under what
117
+ names:
118
+
119
+ ```python
120
+ KeySpec(allowlist=["measurement"], rename={"measurement": "table"}, unknown="reject")
121
+ ```
122
+
123
+ - `allowlist` names the keys that pass, `denylist` the ones that do not;
124
+ - `rename` maps a source key onto the config key it becomes;
125
+ - `unknown` decides what happens to a refused key — `"ignore"` drops it,
126
+ `"reject"` names it in the error so the sender learns what was wrong.
127
+
128
+ On a layer the caller controls, prefer `allowlist`: a parameter added to the
129
+ plugin later stays unreachable until it is listed, where a `denylist` would let
130
+ it through unnoticed.
131
+
132
+ Header names are matched regardless of casing — RFC 9110 makes it meaningless —
133
+ and become config keys spelled in lower case (`X-Api-Key` → `x-api-key`); use
134
+ `rename` for a name of your own. Everywhere else names are matched and kept
135
+ exactly as written.
136
+
137
+ Within one spec, spell a `rename` key the way the `allowlist` spells it. A
138
+ `KeySpec` does not know which source will read it, so it checks the two against
139
+ each other as written, and `allowlist=["X-Api-Key"]` with
140
+ `rename={"x-api-key": "api_key"}` is refused at construction. Either spelling
141
+ works as long as both use it.
142
+
143
+ A header or query parameter the plugin asked for that arrives more than once is
144
+ refused, since which value it would otherwise get is the order the runtime
145
+ delivers them in; `multi=True` reads every value as a list instead. InfluxDB 3
146
+ hands the plugin a plain dict, which holds one value per name, so a repeat never
147
+ reaches a plugin there and neither the refusal nor `multi` fires; both are for a
148
+ runtime that delivers name/value pairs.
149
+
150
+ `parse_env` requires an allowlist: the process environment belongs to the host
151
+ and holds credentials, so nothing is read without being named. `Authorization`
152
+ never reaches a plugin — the engine authenticates with it — so a token needs a
153
+ header of your own.
154
+
155
+ `parse_toml` refuses a path that does not name a `.toml` file before opening it;
156
+ pass `require_suffix=False` for a config file named some other way, and
157
+ `is_toml_path()` answers the same question without reading anything. Otherwise
158
+ it reads whatever path it is given: a relative one resolves under the plugin
159
+ directory, an absolute one is used as is. Take that path from a layer the
160
+ operator controls — the trigger arguments, or the file itself. A path that
161
+ arrives in the request body, a header or the query string lets the caller name
162
+ any file the engine can read, and whatever parses as TOML becomes this plugin's
163
+ configuration, another plugin's credentials included.
164
+
165
+ A value that arrives empty — a blank string, a JSON `null`, an unset variable —
166
+ is left out of its layer, so a validator default applies instead and a blank in
167
+ one layer does not erase the layer below it. `0`, `False` and `[]` are real
168
+ values and are kept.
169
+
170
+ Headers, query-string parameters and environment variables arrive as text, so
171
+ surrounding whitespace is trimmed before anything else sees the value: a header
172
+ sent as ` secret ` becomes `secret`. A trigger argument, a body field and a
173
+ TOML value keep exactly what was written, whitespace included.
174
+
175
+ ### Holding a key against the request
176
+
177
+ `merge_config_layers` merges without validating, and can hold chosen keys
178
+ against the layers above them:
179
+
180
+ ```python
181
+ merged = merge_config_layers(args, body, pinned=["measurement"])
182
+ ```
183
+
184
+ A later layer that sets a pinned key raises; `on_conflict="ignore"` keeps the
185
+ value already set instead. A pinned key nobody set stays open, so the same
186
+ plugin works with or without a fixed measurement.
187
+
188
+ ### Validating
189
+
190
+ A `Validator` describes one config key: the default it falls back to, the cast
191
+ that turns it into a usable type, and the checks it must then pass.
192
+
193
+ ```python
194
+ Validator("window", default="1h", cast=parse_timedelta, gt=timedelta(0), lte=timedelta(days=30))
195
+ Validator("aggregate", default="mean", is_in=("mean", "min", "max", "count"))
196
+ Validator("ripple", required=True, when=Validator("prototype", eq="cheby1"))
197
+ ```
198
+
199
+ `required` asks for a usable value, so a key that arrives blank or `null`
200
+ counts as unset. `when` applies a rule only while another one holds — and holds
201
+ means the key is there and passes. A `when` rule that cannot judge at all, say
202
+ a `cast` of its own that fails, gives no answer, and that is reported against
203
+ the rule which asked rather than quietly leaving it out. `condition` takes any
204
+ predicate and runs after the checks. The checks are `eq`, `ne`, `gt`, `gte`,
205
+ `ge`, `lt`, `lte`, `le`, `identity`, `is_type_of`, `is_in`, `is_not_in`,
206
+ `contains`, `cont`, `not_contains`, `len_eq`, `len_ne`, `len_min`, `len_max`,
207
+ `startswith`, `endswith`, `not_startswith`, `not_endswith`, `regex` and
208
+ `not_regex`. They are named explicitly, so a misspelled one is a `TypeError`
209
+ where the rule is written.
210
+
211
+ `regex` and `not_regex` match from the start of the value, so `regex="b"`
212
+ rejects `"abc"`, and a pattern that may appear anywhere needs `.*` in front.
213
+ `is_type_of` reads a parameterized generic through to the items, so
214
+ `list[int]`, `dict[str, int]` and `tuple[int, ...]` say what they look like.
215
+
216
+ Validation runs once, over the merged values: defaults fill what no layer set,
217
+ `cast` runs next, and the checks see the cast value.
218
+
219
+ TOML becomes native — no manual string parsing:
220
+
221
+ ```toml
222
+ source_table = "cpu"
223
+ batch_size = 2000
224
+ excluded_fields = ["usage_idle", "usage_guest"]
225
+ ```
226
+
227
+ ## Write helpers
228
+
229
+ `LineBuilder` is a runtime global injected into the plugin, so builders take the
230
+ class as their first argument:
231
+
232
+ ```python
233
+ from influxdata_plugin_utils.write import build_line, write_data
234
+
235
+ lines = [
236
+ build_line(LineBuilder, "cpu", tags={"host": "a"}, fields={"usage": 12.5}, time_ns=ts)
237
+ ]
238
+ write_data(influxdb3_local, lines) # batched + retried by default
239
+ # write_data(influxdb3_local, lines, batch=False, retries=0) # opt out
240
+ # write_data(influxdb3_local, lines, database="other_db") # another database
241
+ # write_data(influxdb3_local, lines, no_sync=True) # write_sync API (3.8+)
242
+ ```
243
+
244
+ ## Cross-database queries
245
+
246
+ On InfluxDB versions that support processing-engine cross-database queries,
247
+ the introspection helpers accept `database=` and pass it through to
248
+ `influxdb3_local.query`.
249
+ Cached schema results are separated per database.
250
+
251
+ ```python
252
+ from influxdata_plugin_utils.introspection import get_field_names, query_window
253
+
254
+ fields = get_field_names(influxdb3_local, "cpu", database="source_db")
255
+ rows = query_window(
256
+ influxdb3_local,
257
+ "cpu",
258
+ start=start,
259
+ end=end,
260
+ columns=fields,
261
+ database="source_db",
262
+ )
263
+ ```
264
+
265
+ ## License
266
+
267
+ Licensed under either of [Apache License 2.0](LICENSE-APACHE) or
268
+ [MIT license](LICENSE-MIT) at your option.