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.
- influxdata_plugin_utils-0.4.0/CHANGELOG.md +188 -0
- influxdata_plugin_utils-0.4.0/PKG-INFO +268 -0
- influxdata_plugin_utils-0.4.0/README.md +252 -0
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/pyproject.toml +1 -1
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/__init__.py +42 -6
- influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/_utils.py +41 -0
- influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/cache.py +41 -0
- influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/config.py +158 -0
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/introspection.py +48 -1
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/parsing.py +1 -1
- influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/sources.py +539 -0
- influxdata_plugin_utils-0.4.0/src/influxdata_plugin_utils/validation.py +309 -0
- influxdata_plugin_utils-0.3.0/CHANGELOG.md +0 -63
- influxdata_plugin_utils-0.3.0/PKG-INFO +0 -113
- influxdata_plugin_utils-0.3.0/README.md +0 -96
- influxdata_plugin_utils-0.3.0/src/influxdata_plugin_utils/cache.py +0 -25
- influxdata_plugin_utils-0.3.0/src/influxdata_plugin_utils/config.py +0 -124
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/.gitignore +0 -0
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/LICENSE-APACHE +0 -0
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/LICENSE-MIT +0 -0
- {influxdata_plugin_utils-0.3.0 → influxdata_plugin_utils-0.4.0}/src/influxdata_plugin_utils/py.typed +0 -0
- {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.
|