This is an automated email from the ASF dual-hosted git repository.

Gerrrr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/otava.git


The following commit(s) were added to refs/heads/master by this push:
     new de46ddf  Add InfluxDB importer (#170)
de46ddf is described below

commit de46ddfc672b8dcb079e1993c10389607e006b60
Author: adambernier <[email protected]>
AuthorDate: Mon Aug 24 10:24:38 2026 -0700

    Add InfluxDB importer (#170)
    
    * Add InfluxDB importer
    
    * Fix InfluxDB query escaping and timestamps
    
    * Add reproducible InfluxDB example and E2E test
    
    * Stabilize InfluxDB missing column errors
    
    * Test InfluxDB analysis through CLI
---
 README.md                             |   2 +-
 docs/INFLUXDB.md                      |  99 ++++++++++++++++
 docs/README.md                        |   1 +
 examples/influxdb/admin-token.json    |   5 +
 examples/influxdb/data.lp             |   7 ++
 examples/influxdb/docker-compose.yaml |  59 ++++++++++
 examples/influxdb/otava.yaml          |  56 +++++++++
 examples/influxdb/seed.sh             |  47 ++++++++
 otava/config.py                       |   5 +
 otava/importer.py                     | 115 ++++++++++++++++++-
 otava/influxdb.py                     |  67 +++++++++++
 otava/test_config.py                  |  80 +++++++++++++
 pyproject.toml                        |   1 +
 tests/cli_help_test.py                |  90 ++++++++++++++-
 tests/e2e_test_utils.py               |   5 +
 tests/influxdb_e2e_test.py            | 131 +++++++++++++++++++++
 tests/influxdb_test.py                | 206 ++++++++++++++++++++++++++++++++++
 uv.lock                               |  86 +++++++++++++-
 18 files changed, 1054 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 7726d13..7f68e94 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Apache Otava – Change Detection for Continuous Performance 
Engineering
 
 
 Apache Otava (incubating) performs statistical analysis of performance test 
results stored
-in CSV files, PostgreSQL, BigQuery, or Graphite database. It finds 
change-points and notifies about
+in CSV files, PostgreSQL, BigQuery, InfluxDB 3, or Graphite database. It finds 
change-points and notifies about
 possible performance regressions.
 
 A typical use-case of otava is as follows:
diff --git a/docs/INFLUXDB.md b/docs/INFLUXDB.md
new file mode 100644
index 0000000..024131f
--- /dev/null
+++ b/docs/INFLUXDB.md
@@ -0,0 +1,99 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements.  See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership.  The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License.  You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied.  See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# Importing results from InfluxDB 3
+
+Otava imports query results from InfluxDB 3 Core or Enterprise through the
+[`influxdb3-python`](https://docs.influxdata.com/influxdb3/core/reference/client-libraries/v3/python/)
+client. SQL is the default query language; set `query_language: influxql` for
+InfluxQL queries.
+
+## Connection
+
+```yaml
+influxdb:
+  host: http://localhost:8181
+  database: performance
+  token: ${INFLUXDB_TOKEN}
+```
+
+The same settings are available through `INFLUXDB_HOST`, `INFLUXDB_DATABASE`,
+and `INFLUXDB_TOKEN`, or the `--influxdb-host`, `--influxdb-database`, and
+`--influxdb-token` command-line options. Command-line values take precedence
+over environment variables, which take precedence over YAML.
+
+## Reproducible example
+
+The bundled example starts InfluxDB 3 Core with authenticated, in-memory
+storage, seeds deterministic latency data, and runs Otava against it:
+
+```bash
+docker build -t apache/otava:latest .
+docker compose -f examples/influxdb/docker-compose.yaml run --rm otava \
+  analyze api_latency_sql --branch main --since 2025-01-01
+docker compose -f examples/influxdb/docker-compose.yaml down
+```
+
+Run `api_latency_influxql` instead to query the same data with InfluxQL.
+
+The admin token committed under `examples/influxdb/` is a fixed test
+credential, and the server discards its in-memory data when stopped. Both are
+for this local demonstration only. Use a securely generated token and durable
+object storage for production deployments.
+
+## Test configuration
+
+```yaml
+tests:
+  api_latency_sql:
+    type: influxdb
+    query_language: sql
+    query: |
+      SELECT time, branch, p95_ms, commit
+      FROM api_latency
+      WHERE branch = %{BRANCH}
+      ORDER BY time
+    time_column: time
+    attributes: [branch, commit]
+    metrics:
+      p95:
+        column: p95_ms
+        direction: -1
+        scale: 1
+
+  legacy_api_latency:
+    type: influxdb
+    query_language: influxql
+    query: SELECT time, branch, p95_ms FROM api_latency WHERE branch = 
%{BRANCH}
+    attributes: [branch]
+    metrics: [p95_ms]
+```
+
+Metric definitions use `column`, `direction`, and `scale` as with the other
+SQL-backed importers. `%{BRANCH}` is replaced with an escaped string literal
+when `--branch` is supplied.
+
+Run the analysis with:
+
+```bash
+otava analyze api_latency_sql --branch main --last 100
+```
+
+InfluxDB is import-only in this release; Otava does not write change points
+back to InfluxDB.
diff --git a/docs/README.md b/docs/README.md
index 2d8b970..37da1c9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -32,4 +32,5 @@
 - [PostgreSQL](POSTGRESQL.md)
 - [BigQuery](BIG_QUERY.md)
 - [CSV](CSV.md)
+- [InfluxDB](INFLUXDB.md)
 - [Annotating Change Points in Grafana](GRAFANA.md)
diff --git a/examples/influxdb/admin-token.json 
b/examples/influxdb/admin-token.json
new file mode 100644
index 0000000..a10164c
--- /dev/null
+++ b/examples/influxdb/admin-token.json
@@ -0,0 +1,5 @@
+{
+  "token": "apiv3_otava_example_admin_token_2026",
+  "name": "otava-example-admin",
+  "description": "Test-only admin token for the reproducible Otava example"
+}
diff --git a/examples/influxdb/data.lp b/examples/influxdb/data.lp
new file mode 100644
index 0000000..46205f1
--- /dev/null
+++ b/examples/influxdb/data.lp
@@ -0,0 +1,7 @@
+api_latency,branch=main,commit=a1b2c3d p95_ms=87.0 1735689600000000000
+api_latency,branch=release,commit=r1e2l3s p95_ms=105.0 1735689600000000000
+api_latency,branch=main,commit=b2c3d4e p95_ms=85.0 1735776000000000000
+api_latency,branch=main,commit=c3d4e5f p95_ms=89.0 1735862400000000000
+api_latency,branch=main,commit=d4e5f6a p95_ms=118.0 1735948800000000000
+api_latency,branch=main,commit=e5f6a7b p95_ms=121.0 1736035200000000000
+api_latency,branch=main,commit=f6a7b8c p95_ms=119.0 1736121600000000000
diff --git a/examples/influxdb/docker-compose.yaml 
b/examples/influxdb/docker-compose.yaml
new file mode 100644
index 0000000..68f560a
--- /dev/null
+++ b/examples/influxdb/docker-compose.yaml
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+services:
+  influxdb:
+    image: influxdb:3.11.2-core
+    command:
+      - influxdb3
+      - serve
+      - --node-id=otava-example
+      - --object-store=memory
+      - --admin-token-file=/run/secrets/admin-token
+    ports:
+      - "8181:8181"
+    secrets:
+      - admin-token
+
+  seed:
+    image: influxdb:3.11.2-core
+    entrypoint: ["/bin/sh", "/example/seed.sh"]
+    depends_on:
+      - influxdb
+    environment:
+      INFLUXDB3_AUTH_TOKEN: apiv3_otava_example_admin_token_2026
+      INFLUXDB3_DATABASE_NAME: performance
+      INFLUXDB3_HOST_URL: http://influxdb:8181
+    volumes:
+      - .:/example:ro
+
+  otava:
+    image: apache/otava:latest
+    depends_on:
+      seed:
+        condition: service_completed_successfully
+    environment:
+      INFLUXDB_HOST: http://influxdb:8181
+      INFLUXDB_DATABASE: performance
+      INFLUXDB_TOKEN: apiv3_otava_example_admin_token_2026
+      OTAVA_CONFIG: /config/otava.yaml
+    volumes:
+      - ./otava.yaml:/config/otava.yaml:ro
+
+secrets:
+  admin-token:
+    file: ./admin-token.json
diff --git a/examples/influxdb/otava.yaml b/examples/influxdb/otava.yaml
new file mode 100644
index 0000000..9c30223
--- /dev/null
+++ b/examples/influxdb/otava.yaml
@@ -0,0 +1,56 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# InfluxDB 3 connection settings can also be supplied with INFLUXDB_HOST,
+# INFLUXDB_DATABASE, and INFLUXDB_TOKEN.
+influxdb:
+  host: http://localhost:8181
+  database: performance
+  token: ${INFLUXDB_TOKEN}
+
+tests:
+  api_latency_sql:
+    type: influxdb
+    query_language: sql
+    query: |
+      SELECT time, branch, p95_ms, commit
+      FROM api_latency
+      WHERE branch = %{BRANCH}
+      ORDER BY time
+    time_column: time
+    attributes: [branch, commit]
+    metrics:
+      p95:
+        column: p95_ms
+        direction: -1
+        scale: 1
+
+  api_latency_influxql:
+    type: influxdb
+    query_language: influxql
+    query: |
+      SELECT time, branch, commit, p95_ms
+      FROM api_latency
+      WHERE branch = %{BRANCH}
+      ORDER BY time
+    time_column: time
+    attributes: [branch, commit]
+    metrics:
+      p95:
+        column: p95_ms
+        direction: -1
+        scale: 1
diff --git a/examples/influxdb/seed.sh b/examples/influxdb/seed.sh
new file mode 100755
index 0000000..3499fc7
--- /dev/null
+++ b/examples/influxdb/seed.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -eu
+
+: "${INFLUXDB3_AUTH_TOKEN:?INFLUXDB3_AUTH_TOKEN must be set}"
+
+INFLUXDB3_HOST_URL="${INFLUXDB3_HOST_URL:-http://influxdb:8181}";
+INFLUXDB3_DATABASE_NAME="${INFLUXDB3_DATABASE_NAME:-performance}"
+SEED_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
+
+export INFLUXDB3_HOST_URL INFLUXDB3_DATABASE_NAME
+
+attempt=0
+until influxdb3 show databases --format csv >/dev/null 2>&1; do
+    attempt=$((attempt + 1))
+    if [ "$attempt" -ge 60 ]; then
+        echo "InfluxDB did not become ready at $INFLUXDB3_HOST_URL" >&2
+        exit 1
+    fi
+    sleep 1
+done
+
+if ! influxdb3 show databases --format csv | grep -Fqx 
"$INFLUXDB3_DATABASE_NAME"; then
+    influxdb3 create database "$INFLUXDB3_DATABASE_NAME"
+fi
+
+influxdb3 write \
+    --database "$INFLUXDB3_DATABASE_NAME" \
+    --precision ns \
+    --file "$SEED_DIR/data.lp"
diff --git a/otava/config.py b/otava/config.py
index 11e7021..ae48ad4 100644
--- a/otava/config.py
+++ b/otava/config.py
@@ -25,6 +25,7 @@ from ruamel.yaml import YAML
 from otava.bigquery import BigQueryConfig
 from otava.grafana import GrafanaConfig
 from otava.graphite import GraphiteConfig
+from otava.influxdb import InfluxDBConfig
 from otava.postgres import PostgresConfig
 from otava.slack import SlackConfig
 from otava.test_config import TestConfig, create_test_config
@@ -40,6 +41,7 @@ class Config:
     slack: SlackConfig
     postgres: PostgresConfig
     bigquery: BigQueryConfig
+    influxdb: InfluxDBConfig
 
 
 @dataclass
@@ -115,6 +117,7 @@ def load_config_from_parser_args(args: 
configargparse.Namespace) -> Config:
         slack=SlackConfig.from_parser_args(args),
         postgres=PostgresConfig.from_parser_args(args),
         bigquery=BigQueryConfig.from_parser_args(args),
+        influxdb=InfluxDBConfig.from_parser_args(args),
         tests=tests,
         test_groups=groups,
     )
@@ -133,6 +136,7 @@ class 
NestedYAMLConfigFileParser(configargparse.ConfigFileParser):
         SlackConfig.NAME,
         PostgresConfig.NAME,
         BigQueryConfig.NAME,
+        InfluxDBConfig.NAME,
     ]
 
     def parse(self, stream):
@@ -180,6 +184,7 @@ def add_service_option_groups(parser) -> None:
     SlackConfig.add_parser_args(parser.add_argument_group('Slack Options', 
'Options for Slack configuration'))
     PostgresConfig.add_parser_args(parser.add_argument_group('PostgreSQL 
Options', 'Options for PostgreSQL configuration'))
     BigQueryConfig.add_parser_args(parser.add_argument_group('BigQuery 
Options', 'Options for BigQuery configuration'))
+    InfluxDBConfig.add_parser_args(parser.add_argument_group('InfluxDB 
Options', 'Options for InfluxDB 3 configuration'))
 
 
 def argument_group(parser, title: str):
diff --git a/otava/importer.py b/otava/importer.py
index c6cea32..0552405 100644
--- a/otava/importer.py
+++ b/otava/importer.py
@@ -20,7 +20,7 @@ import json
 from collections import OrderedDict
 from contextlib import contextmanager
 from dataclasses import dataclass
-from datetime import datetime, timedelta
+from datetime import datetime, timedelta, timezone
 from pathlib import Path
 from typing import Dict, List, Optional, Set
 
@@ -30,6 +30,7 @@ from otava.bigquery import BigQuery
 from otava.config import Config
 from otava.data_selector import DataSelector
 from otava.graphite import DataPoint, Graphite, GraphiteError
+from otava.influxdb import InfluxDB
 from otava.postgres import Postgres
 from otava.series import Metric, Series
 from otava.test_config import (
@@ -39,6 +40,8 @@ from otava.test_config import (
     CsvTestConfig,
     GraphiteTestConfig,
     HistoStatTestConfig,
+    InfluxDBMetric,
+    InfluxDBTestConfig,
     JsonTestConfig,
     PostgresMetric,
     PostgresTestConfig,
@@ -827,6 +830,107 @@ class BigQueryImporter(Importer):
         return [m for m in test_conf.metrics.keys()]
 
 
+class InfluxDBImporter(Importer):
+    def __init__(self, influxdb: InfluxDB):
+        self.__influxdb = influxdb
+
+    @staticmethod
+    def __selected_metrics(
+        defined_metrics: Dict[str, InfluxDBMetric], selected_metrics: 
Optional[List[str]]
+    ) -> Dict[str, InfluxDBMetric]:
+        if selected_metrics is not None:
+            return {name: defined_metrics[name] for name in selected_metrics}
+        return defined_metrics
+
+    def fetch_data(self, test_conf: TestConfig, selector: DataSelector = 
DataSelector()) -> Series:
+        if not isinstance(test_conf, InfluxDBTestConfig):
+            raise ValueError("Expected InfluxDBTestConfig")
+
+        since_time = selector.since_time
+        until_time = selector.until_time
+        if since_time.timestamp() > until_time.timestamp():
+            raise DataImportError(
+                f"Invalid time range: 
[{format_timestamp(int(since_time.timestamp()))}, "
+                f"{format_timestamp(int(until_time.timestamp()))}]"
+            )
+
+        metrics = self.__selected_metrics(test_conf.metrics, selector.metrics)
+        query = test_conf.query
+        if "%{BRANCH}" in query:
+            if not selector.branch:
+                raise DataImportError(
+                    f"Test {test_conf.name} uses %{{BRANCH}} in query but 
--branch was not specified"
+                )
+            if test_conf.query_language == "influxql":
+                escaped_branch = (
+                    selector.branch.replace("\\", "\\\\")
+                    .replace("'", "\\'")
+                    .replace("\r", "\\r")
+                    .replace("\n", "\\n")
+                )
+            else:
+                escaped_branch = selector.branch.replace("'", "''")
+            branch_literal = f"'{escaped_branch}'"
+            query = query.replace("%{BRANCH}", branch_literal)
+
+        try:
+            columns, rows = self.__influxdb.fetch_data(query, 
test_conf.query_language)
+        except Exception as err:
+            raise DataImportError(f"Failed to import test {test_conf.name}: 
{err}") from err
+
+        required_columns = [
+            test_conf.time_column,
+            *test_conf.attributes,
+            *(metric.column for metric in metrics.values()),
+        ]
+        for column in required_columns:
+            if column not in columns:
+                raise DataImportError(f"Column not found {column!r} is not in 
list")
+
+        time_index = columns.index(test_conf.time_column)
+        attr_indexes = [columns.index(column) for column in 
test_conf.attributes]
+        metric_names = [metric.name for metric in metrics.values()]
+        metric_indexes = [columns.index(metric.column) for metric in 
metrics.values()]
+
+        time = []
+        data = {name: [] for name in metric_names}
+        attributes = {columns[index]: [] for index in attr_indexes}
+        for row in rows:
+            timestamp = row[time_index]
+            if timestamp.tzinfo is None or timestamp.utcoffset() is None:
+                timestamp = timestamp.replace(tzinfo=timezone.utc)
+            if timestamp < since_time or timestamp >= until_time:
+                continue
+            time.append(timestamp.timestamp())
+            for name, index in zip(metric_names, metric_indexes):
+                try:
+                    data[name].append(float(row[index]))
+                except (TypeError, ValueError) as err:
+                    raise DataImportError(
+                        f"Could not convert value in column {columns[index]}: 
{err}"
+                    )
+            for index in attr_indexes:
+                attributes[columns[index]].append(row[index])
+
+        metrics = {metric.name: Metric(metric.direction, metric.scale) for 
metric in metrics.values()}
+        time = time[-selector.last_n_points :]
+        data = {name: values[-selector.last_n_points :] for name, values in 
data.items()}
+        attributes = {
+            name: values[-selector.last_n_points :] for name, values in 
attributes.items()
+        }
+        return Series(
+            test_conf.name,
+            branch=selector.branch,
+            time=time,
+            metrics=metrics,
+            data=data,
+            attributes=attributes,
+        )
+
+    def fetch_all_metric_names(self, test_conf: InfluxDBTestConfig) -> 
List[str]:
+        return list(test_conf.metrics.keys())
+
+
 class Importers:
     __config: Config
     __csv_importer: Optional[CsvImporter]
@@ -835,6 +939,7 @@ class Importers:
     __postgres_importer: Optional[PostgresImporter]
     __json_importer: Optional[JsonImporter]
     __bigquery_importer: Optional[BigQueryImporter]
+    __influxdb_importer: Optional[InfluxDBImporter]
 
     def __init__(self, config: Config):
         self.__config = config
@@ -844,6 +949,7 @@ class Importers:
         self.__postgres_importer = None
         self.__json_importer = None
         self.__bigquery_importer = None
+        self.__influxdb_importer = None
 
     def csv_importer(self) -> CsvImporter:
         if self.__csv_importer is None:
@@ -875,6 +981,11 @@ class Importers:
             self.__bigquery_importer = 
BigQueryImporter(BigQuery(self.__config.bigquery))
         return self.__bigquery_importer
 
+    def influxdb_importer(self) -> InfluxDBImporter:
+        if self.__influxdb_importer is None:
+            self.__influxdb_importer = 
InfluxDBImporter(InfluxDB(self.__config.influxdb))
+        return self.__influxdb_importer
+
     def get(self, test: TestConfig) -> Importer:
         if isinstance(test, CsvTestConfig):
             return self.csv_importer()
@@ -888,5 +999,7 @@ class Importers:
             return self.json_importer()
         elif isinstance(test, BigQueryTestConfig):
             return self.bigquery_importer()
+        elif isinstance(test, InfluxDBTestConfig):
+            return self.influxdb_importer()
         else:
             raise ValueError(f"Unsupported test type {type(test)}")
diff --git a/otava/influxdb.py b/otava/influxdb.py
new file mode 100644
index 0000000..af5a53b
--- /dev/null
+++ b/otava/influxdb.py
@@ -0,0 +1,67 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from dataclasses import dataclass
+
+from influxdb_client_3 import InfluxDBClient3
+
+
+@dataclass
+class InfluxDBConfig:
+    NAME = "influxdb"
+
+    host: str
+    database: str
+    token: str
+
+    @staticmethod
+    def add_parser_args(arg_group):
+        arg_group.add_argument("--influxdb-host", help="InfluxDB 3 server 
URL", env_var="INFLUXDB_HOST")
+        arg_group.add_argument("--influxdb-database", help="InfluxDB 3 
database name", env_var="INFLUXDB_DATABASE")
+        arg_group.add_argument("--influxdb-token", help="InfluxDB 3 database 
token", env_var="INFLUXDB_TOKEN")
+
+    @staticmethod
+    def from_parser_args(args):
+        return InfluxDBConfig(
+            host=getattr(args, "influxdb_host", None),
+            database=getattr(args, "influxdb_database", None),
+            token=getattr(args, "influxdb_token", None),
+        )
+
+
+class InfluxDB:
+    def __init__(self, config: InfluxDBConfig):
+        self.config = config
+        self._client = None
+
+    @property
+    def client(self) -> InfluxDBClient3:
+        if self._client is None:
+            self._client = InfluxDBClient3(
+                host=self.config.host,
+                database=self.config.database,
+                token=self.config.token,
+            )
+        return self._client
+
+    def fetch_data(self, query: str, language: str):
+        table = self.client.query(query=query, language=language)
+        columns = table.column_names
+        # Keep the public result contract consistent with the SQL importers:
+        # rows are positional tuples in the same order as ``columns``.
+        rows = [tuple(record[column] for column in columns) for record in 
table.to_pylist()]
+        return columns, rows
diff --git a/otava/test_config.py b/otava/test_config.py
index 5cc57c9..db24d42 100644
--- a/otava/test_config.py
+++ b/otava/test_config.py
@@ -199,6 +199,42 @@ class BigQueryTestConfig(TestConfig):
         return list(self.metrics.keys())
 
 
+@dataclass
+class InfluxDBMetric:
+    name: str
+    direction: int
+    scale: float
+    column: str
+
+
+@dataclass
+class InfluxDBTestConfig(TestConfig):
+    query: str
+    time_column: str
+    attributes: List[str]
+    metrics: Dict[str, InfluxDBMetric]
+    query_language: str
+
+    def __init__(
+        self,
+        name: str,
+        query: str,
+        time_column: str = "time",
+        metrics: List[InfluxDBMetric] = None,
+        attributes: List[str] = None,
+        query_language: str = "sql",
+    ):
+        self.name = name
+        self.query = query
+        self.time_column = time_column
+        self.metrics = {m.name: m for m in metrics} if metrics else {}
+        self.attributes = attributes if attributes is not None else []
+        self.query_language = query_language
+
+    def fully_qualified_metric_names(self) -> List[str]:
+        return list(self.metrics.keys())
+
+
 def create_test_config(name: str, config: Dict) -> TestConfig:
     """
     Loads properties of a test from a dictionary read from otava's config file
@@ -217,6 +253,8 @@ def create_test_config(name: str, config: Dict) -> 
TestConfig:
         return create_postgres_test_config(name, config)
     elif test_type == "bigquery":
         return create_bigquery_test_config(name, config)
+    elif test_type == "influxdb":
+        return create_influxdb_test_config(name, config)
     elif test_type == "json":
         return create_json_test_config(name, config)
     elif test_type is None:
@@ -371,6 +409,48 @@ def create_bigquery_test_config(test_name: str, test_info: 
Dict) -> BigQueryTest
         raise TestConfigError(f"Configuration key not found in test 
{test_name}: {e.args[0]}")
 
 
+def create_influxdb_test_config(test_name: str, test_info: Dict) -> 
InfluxDBTestConfig:
+    try:
+        query = test_info["query"]
+        metrics_info = test_info["metrics"]
+    except KeyError as e:
+        raise TestConfigError(f"Configuration key not found in test 
{test_name}: {e.args[0]}")
+
+    if not isinstance(metrics_info, (List, Dict)):
+        raise TestConfigError(f"Metrics of the test {test_name} must be a list 
or dictionary")
+
+    metrics = []
+    if isinstance(metrics_info, List):
+        metrics = [InfluxDBMetric(metric_name, 1, 1.0, metric_name) for 
metric_name in metrics_info]
+    else:
+        for metric_name, metric_conf in metrics_info.items():
+            metrics.append(
+                InfluxDBMetric(
+                    name=metric_name,
+                    column=metric_conf.get("column", metric_name),
+                    direction=int(metric_conf.get("direction", "1")),
+                    scale=float(metric_conf.get("scale", "1")),
+                )
+            )
+
+    attributes = test_info.get("attributes", [])
+    if not isinstance(attributes, List):
+        raise TestConfigError(f"Attributes of the test {test_name} must be a 
list")
+    query_language = test_info.get("query_language", "sql")
+    if query_language not in ("sql", "influxql"):
+        raise TestConfigError(
+            f"Query language of the test {test_name} must be `sql` or 
`influxql`"
+        )
+    return InfluxDBTestConfig(
+        test_name,
+        query=query,
+        time_column=test_info.get("time_column", "time"),
+        metrics=metrics,
+        attributes=attributes,
+        query_language=query_language,
+    )
+
+
 @dataclass
 class JsonTestConfig(TestConfig):
     name: str
diff --git a/pyproject.toml b/pyproject.toml
index cf9ad1b..53e3762 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,6 +53,7 @@ dependencies = [
     "slack-sdk>=3.39.0",
     "google-cloud-bigquery>=3.38.0",
     "pg8000>=1.31.5",
+    "influxdb3-python>=0.20.0",
     "configargparse>=1.7.1",
     "pydantic>=2,<3",
 
diff --git a/tests/cli_help_test.py b/tests/cli_help_test.py
index 1060578..f2ce443 100644
--- a/tests/cli_help_test.py
+++ b/tests/cli_help_test.py
@@ -56,6 +56,8 @@ usage: otava [-h] [--config-file CONFIG_FILE] [--graphite-url 
GRAPHITE_URL]
              [--postgres-username POSTGRES_USERNAME] [--postgres-password 
POSTGRES_PASSWORD]
              [--postgres-database POSTGRES_DATABASE] [--bigquery-project-id 
BIGQUERY_PROJECT_ID]
              [--bigquery-dataset BIGQUERY_DATASET] [--bigquery-credentials 
BIGQUERY_CREDENTIALS]
+             [--influxdb-host INFLUXDB_HOST] [--influxdb-database 
INFLUXDB_DATABASE]
+             [--influxdb-token INFLUXDB_TOKEN]
              
{list-tests,list-metrics,list-groups,analyze,remove-annotations,validate} ...
 
 Change Detection for Continuous Performance Engineering
@@ -120,6 +122,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
 Args that start with '--' can also be set in a config file (specified via 
--config-file).  In
 general, command-line values override environment variables which override 
config file values
 which override defaults.
@@ -156,8 +168,9 @@ usage: otava analyze [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPHITE_U
                      [--postgres-database POSTGRES_DATABASE]
                      [--bigquery-project-id BIGQUERY_PROJECT_ID]
                      [--bigquery-dataset BIGQUERY_DATASET]
-                     [--bigquery-credentials BIGQUERY_CREDENTIALS] 
[--update-grafana]
-                     [--update-postgres] [--update-bigquery]
+                     [--bigquery-credentials BIGQUERY_CREDENTIALS] 
[--influxdb-host INFLUXDB_HOST]
+                     [--influxdb-database INFLUXDB_DATABASE] [--influxdb-token 
INFLUXDB_TOKEN]
+                     [--update-grafana] [--update-postgres] [--update-bigquery]
                      [--notify-slack NOTIFY_SLACK [NOTIFY_SLACK ...]] 
[--cph-report-since DATE]
                      [--output {{log,json,regressions_only}}] [--branch 
[STRING]] [--metrics LIST]
 {usage_filter_lines}
@@ -265,6 +278,16 @@ BigQuery Options:
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
   --update-bigquery     Update BigQuery database results with change points
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
@@ -288,6 +311,8 @@ usage: otava list-tests [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPHIT
                         [--bigquery-project-id BIGQUERY_PROJECT_ID]
                         [--bigquery-dataset BIGQUERY_DATASET]
                         [--bigquery-credentials BIGQUERY_CREDENTIALS]
+                        [--influxdb-host INFLUXDB_HOST] [--influxdb-database 
INFLUXDB_DATABASE]
+                        [--influxdb-token INFLUXDB_TOKEN]
                         [group ...]
 
 positional arguments:
@@ -345,6 +370,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
@@ -368,6 +403,8 @@ usage: otava list-metrics [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPH
                           [--bigquery-project-id BIGQUERY_PROJECT_ID]
                           [--bigquery-dataset BIGQUERY_DATASET]
                           [--bigquery-credentials BIGQUERY_CREDENTIALS]
+                          [--influxdb-host INFLUXDB_HOST] [--influxdb-database 
INFLUXDB_DATABASE]
+                          [--influxdb-token INFLUXDB_TOKEN]
                           test
 
 positional arguments:
@@ -425,6 +462,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
@@ -449,6 +496,8 @@ usage: otava list-groups [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPHI
                          [--bigquery-project-id BIGQUERY_PROJECT_ID]
                          [--bigquery-dataset BIGQUERY_DATASET]
                          [--bigquery-credentials BIGQUERY_CREDENTIALS]
+                         [--influxdb-host INFLUXDB_HOST] [--influxdb-database 
INFLUXDB_DATABASE]
+                         [--influxdb-token INFLUXDB_TOKEN]
 
 options:
   -h, --help            show this help message and exit
@@ -502,6 +551,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
@@ -525,7 +584,10 @@ usage: otava remove-annotations [-h] [--config-file 
CONFIG_FILE] [--graphite-url
                                 [--postgres-database POSTGRES_DATABASE]
                                 [--bigquery-project-id BIGQUERY_PROJECT_ID]
                                 [--bigquery-dataset BIGQUERY_DATASET]
-                                [--bigquery-credentials BIGQUERY_CREDENTIALS] 
[--force]
+                                [--bigquery-credentials BIGQUERY_CREDENTIALS]
+                                [--influxdb-host INFLUXDB_HOST]
+                                [--influxdb-database INFLUXDB_DATABASE]
+                                [--influxdb-token INFLUXDB_TOKEN] [--force]
                                 [tests ...]
 
 positional arguments:
@@ -584,6 +646,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
@@ -607,6 +679,8 @@ usage: otava validate [-h] [--config-file CONFIG_FILE] 
[--graphite-url GRAPHITE_
                       [--bigquery-project-id BIGQUERY_PROJECT_ID]
                       [--bigquery-dataset BIGQUERY_DATASET]
                       [--bigquery-credentials BIGQUERY_CREDENTIALS]
+                      [--influxdb-host INFLUXDB_HOST] [--influxdb-database 
INFLUXDB_DATABASE]
+                      [--influxdb-token INFLUXDB_TOKEN]
 
 options:
   -h, --help            show this help message and exit
@@ -660,6 +734,16 @@ BigQuery Options:
   --bigquery-credentials BIGQUERY_CREDENTIALS
                         BigQuery credentials file [env var: 
BIGQUERY_VAULT_SECRET]
 
+InfluxDB Options:
+  Options for InfluxDB 3 configuration
+
+  --influxdb-host INFLUXDB_HOST
+                        InfluxDB 3 server URL [env var: INFLUXDB_HOST]
+  --influxdb-database INFLUXDB_DATABASE
+                        InfluxDB 3 database name [env var: INFLUXDB_DATABASE]
+  --influxdb-token INFLUXDB_TOKEN
+                        InfluxDB 3 database token [env var: INFLUXDB_TOKEN]
+
  In general, command-line values override environment variables which override 
defaults.
 """
     )
diff --git a/tests/e2e_test_utils.py b/tests/e2e_test_utils.py
index 439dcac..bb99440 100644
--- a/tests/e2e_test_utils.py
+++ b/tests/e2e_test_utils.py
@@ -29,6 +29,7 @@ import pytest
 def container(
     image: str,
     *,
+    command: list[str] | None = None,
     env: dict[str, str] | None = None,
     ports: list[int] | None = None,
     volumes: dict[str, str] | None = None,
@@ -39,6 +40,7 @@ def container(
 
     Args:
         image: Docker image to run (e.g., "postgres:latest").
+        command: Optional command and arguments to run instead of the image 
default.
         env: Optional dict of environment variables to set in the container.
         ports: Optional list of container ports to publish (will be mapped to 
random host ports).
         volumes: Optional dict mapping host paths to container paths for 
volume mounts.
@@ -76,6 +78,9 @@ def container(
 
         cmd.append(image)
 
+        if command:
+            cmd.extend(command)
+
         # Start the container
         proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
         if proc.returncode != 0:
diff --git a/tests/influxdb_e2e_test.py b/tests/influxdb_e2e_test.py
new file mode 100644
index 0000000..32b1603
--- /dev/null
+++ b/tests/influxdb_e2e_test.py
@@ -0,0 +1,131 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+import subprocess
+import textwrap
+from pathlib import Path
+
+import pytest
+from e2e_test_utils import _remove_trailing_whitespaces, container
+
+INFLUXDB_IMAGE = "influxdb:3.11.2-core"
+INFLUXDB_PORT = 8181
+INFLUXDB_TOKEN = "apiv3_otava_example_admin_token_2026"
+EXAMPLE_DIR = Path("examples/influxdb").resolve()
+
+
+def _analyze(test_name: str, host: str) -> str:
+    command = [
+        "uv",
+        "run",
+        "otava",
+        "analyze",
+        test_name,
+        "--influxdb-host",
+        host,
+        "--influxdb-database",
+        "performance",
+        "--influxdb-token",
+        INFLUXDB_TOKEN,
+        "--branch",
+        "main",
+        "--since",
+        "2025-01-01T00:00:00Z",
+        "--until",
+        "2025-01-07T00:00:00Z",
+    ]
+    proc = subprocess.run(
+        command,
+        capture_output=True,
+        text=True,
+        timeout=600,
+        env=dict(os.environ, OTAVA_CONFIG=str(EXAMPLE_DIR / "otava.yaml")),
+    )
+    if proc.returncode != 0:
+        pytest.fail(
+            "InfluxDB analysis command returned non-zero exit code.\n\n"
+            f"Command: {proc.args!r}\n"
+            f"Exit code: {proc.returncode}\n\n"
+            f"Stdout:\n{proc.stdout}\n\n"
+            f"Stderr:\n{proc.stderr}\n"
+        )
+    return _remove_trailing_whitespaces(proc.stdout)
+
+
+def test_influxdb_sql_and_influxql_return_identical_seeded_data():
+    with container(
+        INFLUXDB_IMAGE,
+        command=[
+            "influxdb3",
+            "serve",
+            "--node-id=otava-e2e",
+            "--object-store=memory",
+            "--admin-token-file=/example/admin-token.json",
+        ],
+        ports=[INFLUXDB_PORT],
+        volumes={str(EXAMPLE_DIR): "/example:ro"},
+    ) as (container_id, port_map):
+        seed = subprocess.run(
+            [
+                "docker",
+                "exec",
+                "--env",
+                f"INFLUXDB3_HOST_URL=http://127.0.0.1:{INFLUXDB_PORT}";,
+                "--env",
+                f"INFLUXDB3_AUTH_TOKEN={INFLUXDB_TOKEN}",
+                "--env",
+                "INFLUXDB3_DATABASE_NAME=performance",
+                container_id,
+                "/bin/sh",
+                "/example/seed.sh",
+            ],
+            capture_output=True,
+            text=True,
+            timeout=120,
+        )
+        if seed.returncode != 0:
+            pytest.fail(
+                "InfluxDB seed command returned non-zero exit code.\n\n"
+                f"Command: {seed.args!r}\n"
+                f"Exit code: {seed.returncode}\n\n"
+                f"Stdout:\n{seed.stdout}\n\n"
+                f"Stderr:\n{seed.stderr}\n"
+            )
+
+        expected_output = textwrap.dedent(
+            """\
+            time                       branch    commit      p95
+            -------------------------  --------  --------  -----
+            2025-01-01 00:00:00 +0000  main      a1b2c3d      87
+            2025-01-02 00:00:00 +0000  main      b2c3d4e      85
+            2025-01-03 00:00:00 +0000  main      c3d4e5f      89
+                                                           ·····
+                                                           +37.2%
+                                                           ·····
+            2025-01-04 00:00:00 +0000  main      d4e5f6a     118
+            2025-01-05 00:00:00 +0000  main      e5f6a7b     121
+            2025-01-06 00:00:00 +0000  main      f6a7b8c     119
+            """
+        ).rstrip("\n")
+
+        host = f"http://localhost:{port_map[INFLUXDB_PORT]}";
+        outputs = [
+            _analyze(test_name, host)
+            for test_name in ("api_latency_sql", "api_latency_influxql")
+        ]
+        assert outputs == [expected_output, expected_output]
diff --git a/tests/influxdb_test.py b/tests/influxdb_test.py
new file mode 100644
index 0000000..aea65c6
--- /dev/null
+++ b/tests/influxdb_test.py
@@ -0,0 +1,206 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import os
+from datetime import datetime, timezone
+from unittest.mock import Mock
+
+import pyarrow as pa
+import pytest
+
+from otava.config import load_config_from_file
+from otava.data_selector import DataSelector
+from otava.importer import DataImportError, InfluxDBImporter
+from otava.influxdb import InfluxDB, InfluxDBConfig
+from otava.main import create_otava_cli_parser
+from otava.test_config import (
+    InfluxDBMetric,
+    InfluxDBTestConfig,
+    TestConfigError,
+    create_test_config,
+)
+
+
+def selector():
+    result = DataSelector()
+    result.since_time = datetime(2024, 1, 1, tzinfo=timezone.utc)
+    result.until_time = datetime(2024, 1, 5, tzinfo=timezone.utc)
+    return result
+
+
+def test_influxdb_connection_config_precedence(tmp_path, monkeypatch):
+    config_file = tmp_path / "otava.yaml"
+    config_file.write_text(
+        "influxdb:\n  host: yaml-host\n  database: yaml-db\n  token: 
yaml-token\n"
+    )
+    monkeypatch.setenv("INFLUXDB_HOST", "env-host")
+    monkeypatch.setenv("INFLUXDB_DATABASE", "env-db")
+    monkeypatch.setenv("INFLUXDB_TOKEN", "env-token")
+
+    config = load_config_from_file(
+        str(config_file),
+        arg_overrides=["--influxdb-host", "cli-host", "--influxdb-token", 
"cli-token"],
+    )
+    assert config.influxdb.host == "cli-host"
+    assert config.influxdb.database == "env-db"
+    assert config.influxdb.token == "cli-token"
+    assert os.environ["INFLUXDB_HOST"] == "env-host"
+
+
+def test_cli_help_includes_influxdb_options():
+    help_text = create_otava_cli_parser().format_help()
+    assert "InfluxDB Options:" in help_text
+    assert "--influxdb-host" in help_text
+    assert "--influxdb-database" in help_text
+    assert "--influxdb-token" in help_text
+
+
+def test_influxdb_test_config_defaults_to_sql_and_parses_metrics():
+    test = create_test_config(
+        "latency",
+        {
+            "type": "influxdb",
+            "query": "SELECT * FROM latency",
+            "attributes": ["branch"],
+            "metrics": {"p95": {"column": "p95_ms", "direction": -1, "scale": 
0.001}},
+        },
+    )
+    assert isinstance(test, InfluxDBTestConfig)
+    assert test.query_language == "sql"
+    assert test.metrics["p95"] == InfluxDBMetric("p95", -1, 0.001, "p95_ms")
+
+
+def test_influxdb_test_config_supports_influxql_and_rejects_unknown_language():
+    test = create_test_config(
+        "latency",
+        {"type": "influxdb", "query": "SELECT * FROM latency", "metrics": 
["p95_ms"], "query_language": "influxql"},
+    )
+    assert test.query_language == "influxql"
+    with pytest.raises(TestConfigError):
+        create_test_config(
+            "latency",
+            {"type": "influxdb", "query": "SELECT * FROM latency", "metrics": 
["p95_ms"], "query_language": "flux"},
+        )
+
+
+def test_influxdb_importer_reads_arrow_table_and_applies_selection():
+    client = Mock()
+    client.query.return_value = pa.table(
+        {
+            "time": pa.array(
+                [
+                    datetime(2023, 12, 31),
+                    datetime(2024, 1, 2),
+                    datetime(2024, 1, 3),
+                    datetime(2024, 1, 5),
+                ],
+                type=pa.timestamp("ns"),
+            ),
+            "branch": ["main", "main", "main", "main"],
+            "commit": ["before", "b", "c", "after"],
+            "p95_ms": [10, 20, 30, 40],
+        }
+    )
+    backend = InfluxDB(InfluxDBConfig("host", "database", "token"))
+    backend._client = client
+    test = InfluxDBTestConfig(
+        "latency",
+        "SELECT * FROM latency",
+        metrics=[InfluxDBMetric("p95", -1, 0.001, "p95_ms")],
+        attributes=["branch", "commit"],
+    )
+    chosen = selector()
+    chosen.metrics = ["p95"]
+    chosen.last_n_points = 2
+    series = InfluxDBImporter(backend).fetch_data(test, chosen)
+
+    assert series.branch is None
+    assert series.time == [1704153600.0, 1704240000.0]
+    assert series.data == {"p95": [20.0, 30.0]}
+    assert series.attributes == {"branch": ["main", "main"], "commit": ["b", 
"c"]}
+    assert client.query.call_args.kwargs == {"query": "SELECT * FROM latency", 
"language": "sql"}
+
+
+def test_influxdb_importer_escapes_branch_for_sql():
+    backend = Mock()
+    backend.fetch_data.return_value = (
+        ["time", "p95_ms"],
+        [(datetime(2024, 1, 2, tzinfo=timezone.utc), 4)],
+    )
+    test = InfluxDBTestConfig(
+        "latency",
+        "SELECT * FROM latency WHERE branch = %{BRANCH}",
+        metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")],
+    )
+    chosen = selector()
+    chosen.branch = "release'candidate"
+
+    series = InfluxDBImporter(backend).fetch_data(test, chosen)
+
+    assert series.data["p95"] == [4.0]
+    assert backend.fetch_data.call_args.args == (
+        "SELECT * FROM latency WHERE branch = 'release''candidate'",
+        "sql",
+    )
+
+
+def test_influxdb_importer_escapes_branch_for_influxql():
+    backend = Mock()
+    backend.fetch_data.return_value = (
+        ["time", "p95_ms"],
+        [(datetime(2024, 1, 2, tzinfo=timezone.utc), 4)],
+    )
+    test = InfluxDBTestConfig(
+        "latency",
+        "SELECT * FROM latency WHERE branch = %{BRANCH}",
+        query_language="influxql",
+        metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")],
+    )
+    chosen = selector()
+    chosen.branch = "release'candidate\\path\r\nnext"
+
+    series = InfluxDBImporter(backend).fetch_data(test, chosen)
+
+    assert series.data["p95"] == [4.0]
+    assert backend.fetch_data.call_args.args == (
+        "SELECT * FROM latency WHERE branch = 
'release\\'candidate\\\\path\\r\\nnext'",
+        "influxql",
+    )
+
+
+def test_influxdb_importer_reports_missing_columns_and_client_errors():
+    test = InfluxDBTestConfig(
+        "latency",
+        "SELECT * FROM latency",
+        metrics=[InfluxDBMetric("p95", 1, 1.0, "missing")],
+    )
+    backend = Mock()
+    backend.fetch_data.return_value = (["time"], [])
+    with pytest.raises(DataImportError) as missing_error:
+        InfluxDBImporter(backend).fetch_data(test, selector())
+    assert missing_error.value.message == "Column not found 'missing' is not 
in list"
+
+    backend.fetch_data.side_effect = RuntimeError("server unavailable")
+    with pytest.raises(DataImportError) as client_error:
+        InfluxDBImporter(backend).fetch_data(
+            InfluxDBTestConfig(
+                "latency", "SELECT * FROM latency", 
metrics=[InfluxDBMetric("p95", 1, 1.0, "p95_ms")]
+            ),
+            selector(),
+        )
+    assert "latency" in client_error.value.message
+    assert "server unavailable" in client_error.value.message
diff --git a/uv.lock b/uv.lock
index 5802fbb..9b08a4e 100644
--- a/uv.lock
+++ b/uv.lock
@@ -25,6 +25,7 @@ dependencies = [
     { name = "configargparse" },
     { name = "dateparser" },
     { name = "google-cloud-bigquery" },
+    { name = "influxdb3-python" },
     { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
     { name = "numpy", version = "2.3.5", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
     { name = "pg8000" },
@@ -67,6 +68,7 @@ requires-dist = [
     { name = "flake8", marker = "extra == 'dev'", specifier = ">=7.3.0" },
     { name = "google-cloud-bigquery", specifier = ">=3.38.0" },
     { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.0" },
+    { name = "influxdb3-python", specifier = ">=0.20.0" },
     { name = "isort", marker = "extra == 'dev'", specifier = ">=7.0.0" },
     { name = "numpy", marker = "python_full_version < '3.14'", specifier = 
"==2.2.*" },
     { name = "numpy", marker = "python_full_version >= '3.14'", specifier = 
">=2.3.2,<2.4" },
@@ -405,7 +407,7 @@ name = "exceptiongroup"
 version = "1.3.0"
 source = { registry = "https://pypi.org/simple"; }
 dependencies = [
-    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+    { name = "typing-extensions" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz";,
 hash = 
"sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size 
= 29749, upload-time = "2025-05-10T17:42:51.123Z" }
 wheels = [
@@ -746,6 +748,22 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl";,
 hash = 
"sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size 
= 72340, upload-time = "2026-05-12T22:45:55.733Z" },
 ]
 
+[[package]]
+name = "influxdb3-python"
+version = "0.20.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "certifi" },
+    { name = "pyarrow" },
+    { name = "python-dateutil" },
+    { name = "reactivex" },
+    { name = "urllib3" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/3b/f0/3fb234e83316e439a91d4d7ee5a2f569b9b81e5a709618c4168dca3a087b/influxdb3_python-0.20.0.tar.gz";,
 hash = 
"sha256:f1e28c2f4f244d48006beeb82be7aea82ebdd3b3e1807250d124b6f1d53c5943", size 
= 102084, upload-time = "2026-06-11T06:49:53.012Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/cd/2f/06961cdb1d38d0cdb3c6876970db227e69824a293896af6d1eb983fa1534/influxdb3_python-0.20.0-py3-none-any.whl";,
 hash = 
"sha256:0914f05c2ed9b96f2962fb3d410068dda9c411b562dab22fae1d6c6599a37927", size 
= 86330, upload-time = "2026-06-11T06:49:52.016Z" },
+]
+
 [[package]]
 name = "iniconfig"
 version = "2.3.0"
@@ -1025,6 +1043,56 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl";,
 hash = 
"sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size 
= 22335, upload-time = "2022-10-25T20:38:27.636Z" },
 ]
 
+[[package]]
+name = "pyarrow"
+version = "25.0.1"
+source = { registry = "https://pypi.org/simple"; }
+sdist = { url = 
"https://files.pythonhosted.org/packages/3d/e3/27f57f80141379d60defe6703eb50a707325706f07fedfd1312c7a751995/pyarrow-25.0.1.tar.gz";,
 hash = 
"sha256:9150a83248bfed9813ea3c3af74c3856c1984d444aa28e58bf7733b9750ddf6a", size 
= 1201653, upload-time = "2026-08-10T12:40:53.904Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/0a/3e/5cd70becb51e1d044c54ba5e627424a6e87df5b98008cbd22cc6abd409ca/pyarrow-25.0.1-cp310-cp310-macosx_12_0_arm64.whl";,
 hash = 
"sha256:0b1edbb2f385a6a65e9711b62ba86ac54a7816a3f8d17bb3e8a5929d65fb2485", size 
= 35954271, upload-time = "2026-08-10T12:36:33.857Z" },
+    { url = 
"https://files.pythonhosted.org/packages/64/be/17599e086df264ea7dc221d1101e3131e181e00da428a2f9bd0358f0d06b/pyarrow-25.0.1-cp310-cp310-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:a4dd8bf99a8fac133efc0ed6a92f5fddbe2adba0d0f6dd720e39ba9855cea85c", size 
= 37647543, upload-time = "2026-08-10T12:36:39.486Z" },
+    { url = 
"https://files.pythonhosted.org/packages/42/34/e138b451fd3970a6eda4599f68ae3b2b32b661bc958de3239d54a0bf6575/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:bddd0c4f7630c2a3ddf6347c1bdaa79d97bcf6bd445f9e60c816b7d77c85a5ae", size 
= 46837120, upload-time = "2026-08-10T12:36:46.58Z" },
+    { url = 
"https://files.pythonhosted.org/packages/57/5c/f8fc0eb2de03464a557d5a4d0c15e972d73362414696618833b771f7eddd/pyarrow-25.0.1-cp310-cp310-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:a4d6d5e9a3d1879a97c08ded0c797579b7965eafd0f0c26c30b45ccc06db939b", size 
= 50066460, upload-time = "2026-08-10T12:36:53.702Z" },
+    { url = 
"https://files.pythonhosted.org/packages/3f/d1/0dd64fd06de0333b808a02f60981635f067b71aad3a30698a9a104fae778/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:514ddb60285631af068875550c90eddc181db3e8e63a032b1559be189e82f056", size 
= 49937892, upload-time = "2026-08-10T12:37:00.349Z" },
+    { url = 
"https://files.pythonhosted.org/packages/cb/3c/f89d1bd76d5f3284c2a44d7d7ebbd8204535e5ae2b41f4077069b4ff2ec6/pyarrow-25.0.1-cp310-cp310-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:cab40b1edfef0262e0e5251aa2c58d75630f24d06dd7794480243acc001a1d7d", size 
= 53107240, upload-time = "2026-08-10T12:37:07.205Z" },
+    { url = 
"https://files.pythonhosted.org/packages/67/67/b554a8e09f3f3decccf405eb8fbe86696321cbcb5b62d18b4a5057a4c113/pyarrow-25.0.1-cp310-cp310-win_amd64.whl";,
 hash = 
"sha256:60e89d8f13861a1f7f8d950fa54aebb8023b30734d0ac51ffa80beabe2df4bba", size 
= 27848683, upload-time = "2026-08-10T12:37:12.058Z" },
+    { url = 
"https://files.pythonhosted.org/packages/ee/8b/0d23b47702fcfe8b3618d5292035099675c5a1c48258932350c08020f7b5/pyarrow-25.0.1-cp311-cp311-macosx_12_0_arm64.whl";,
 hash = 
"sha256:51093dd9e10325fbdb3c10a2ae7c4806e5c822d94e74ae4938b26524a3323fee", size 
= 35946180, upload-time = "2026-08-10T12:37:18.934Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d8/17/707d17a5476c55a9541fde0db8213ac30979a792864d72415f176ba50c45/pyarrow-25.0.1-cp311-cp311-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:eb6203482ff3746a5632303a7279ae0b5a304c46985b49ed1378cb350ea6728d", size 
= 37644787, upload-time = "2026-08-10T12:37:25.795Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c1/b2/cdc98ecf1a6408280bc3a6a07054cdd99a3f4670acc0545d383ce113e87d/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:880523be3d29efcf83d3998835d206118ccf35e3871dbd2fb60408cf6b007a80", size 
= 46834633, upload-time = "2026-08-10T12:37:33.604Z" },
+    { url = 
"https://files.pythonhosted.org/packages/c8/6e/d3fafc41f378b2c65be43b827798c0fae42049a641c8526633ed3eb573e2/pyarrow-25.0.1-cp311-cp311-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:25f8720bf6387d5dc2ebd2622112de630760419e4b66134405dd24110d15f37e", size 
= 50065507, upload-time = "2026-08-10T12:37:40.565Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d5/12/8d0698954b8c3001844a898e0a6900bebe83d7ee40c11195174c5122f324/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:4facd65742a024a4a366328a1d2292062d72d6e023c1b7dda8d4c37544933a25", size 
= 49955690, upload-time = "2026-08-10T12:37:46.644Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d3/0b/1ecb936ac6409e90a34d58eea1c7cec09a9ae6d2141b9e49ad01a2b1ea47/pyarrow-25.0.1-cp311-cp311-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:aa0559502e1cd6254d6814614085dd9c5a3dd0419362978a936a3f68a9e5c3df", size 
= 53128198, upload-time = "2026-08-10T12:37:52.531Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8e/1c/5236033550633c9b7377b2a53660b2bbb06cb06dc09c4356332d67643ca1/pyarrow-25.0.1-cp311-cp311-win_amd64.whl";,
 hash = 
"sha256:62cd0d785b8aa6675ee355f9fc02252a340f4441257c42674937826fd7594325", size 
= 27857263, upload-time = "2026-08-10T12:37:56.943Z" },
+    { url = 
"https://files.pythonhosted.org/packages/a6/e2/9ab15b88cbfac28e16419ce5439ec29234c5172cb8259301b4ba639bdec0/pyarrow-25.0.1-cp312-cp312-macosx_12_0_arm64.whl";,
 hash = 
"sha256:df961f2e7ae9cf496459259d798652c70625f6c080650d6952f8c04053c58ee9", size 
= 35861559, upload-time = "2026-08-10T12:38:02.567Z" },
+    { url = 
"https://files.pythonhosted.org/packages/58/79/a0036dbe1eabe1f73127427342f1d99982584c4a2cde2651d6c93499c6f6/pyarrow-25.0.1-cp312-cp312-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:cc4aa407fde9fc660be3939e49ea31f50f3e9fec17c0ec63159f7711edd3efc9", size 
= 37628383, upload-time = "2026-08-10T12:38:09.083Z" },
+    { url = 
"https://files.pythonhosted.org/packages/13/49/d93a57d375f4bf0cf82913dd6bb54acafde83dd993be2282c81ac5616cad/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:4340f0ba6c1d2e13f21658de1d7c662ca2545018568d0030a1e9afca159d87e3", size 
= 46820190, upload-time = "2026-08-10T12:38:15.458Z" },
+    { url = 
"https://files.pythonhosted.org/packages/60/c9/711ca85d79f1ec98f29a5eae2b051e25b4ecec5de3e3c0e2d5c5dcb15664/pyarrow-25.0.1-cp312-cp312-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:5389cdf79447ed1515c9e31620e6e1e2302249564d603f2ad727d4f6d313e4c3", size 
= 50102437, upload-time = "2026-08-10T12:38:22.487Z" },
+    { url = 
"https://files.pythonhosted.org/packages/80/53/8fb8359ff17cfb6263a1cf3ebf7caec9fe197de118719e84fcb1d0618026/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:d51592cb7561e87877c506113e7adbf1342ab579e6c21f0ef44b8ba41cb74c80", size 
= 49942424, upload-time = "2026-08-10T12:38:28.755Z" },
+    { url = 
"https://files.pythonhosted.org/packages/e8/83/4e5ae02a9341571b18a6fca380ac7a58ce6ddae7ab3c060208c0a1e79f02/pyarrow-25.0.1-cp312-cp312-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:6109c94d8b9f3b17a041daca16cacb2f651ad8f1ef70a4232c2c0f37a23da2a8", size 
= 53144206, upload-time = "2026-08-10T12:38:34.862Z" },
+    { url = 
"https://files.pythonhosted.org/packages/65/ee/197cbf47e49f83e6ebeb946a5259a48a638dea27ac774db42fe78022179d/pyarrow-25.0.1-cp312-cp312-win_amd64.whl";,
 hash = 
"sha256:8858d7bfc22e3f51529aeaa4077225029724623e4595dc9eff8c793935c34140", size 
= 27953934, upload-time = "2026-08-10T12:38:39.808Z" },
+    { url = 
"https://files.pythonhosted.org/packages/cc/8d/8f271a7a034c834910ec925d56fa4b29733b1380f5289419f5aaa3b02777/pyarrow-25.0.1-cp313-cp313-macosx_12_0_arm64.whl";,
 hash = 
"sha256:c7c534ec03c358a76ea3e505e74c1b6aef290af90c444dfd092dbfe23e755b85", size 
= 35855328, upload-time = "2026-08-10T12:38:45.489Z" },
+    { url = 
"https://files.pythonhosted.org/packages/d2/cd/5bac242f4e841b9971d5eb94fdfe2577e2b70be983e27401e72055786037/pyarrow-25.0.1-cp313-cp313-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:dda9470024204d7bbf2042b47c6e8a0e47a3eeb8e34405882dfaea6577e0c153", size 
= 37622415, upload-time = "2026-08-10T12:38:51.107Z" },
+    { url = 
"https://files.pythonhosted.org/packages/63/1f/96d03b4e1506524f7087adb0fd6b2f69f0c9c7aaff1ec36d8030082e15a5/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:44a9120ce5bd81936b8ab9a88076e3fd47c2c6838e0e43630fed83626aca81d9", size 
= 46813813, upload-time = "2026-08-10T12:38:57.773Z" },
+    { url = 
"https://files.pythonhosted.org/packages/98/d6/33a411115b61dbfc16ad6ad73e71730f6fea654ee3667673bc53ab0e2fe7/pyarrow-25.0.1-cp313-cp313-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:0befcf816e45a1af33ac775a9970b749e4868a230c7372f0ae5e932bee27039f", size 
= 50104452, upload-time = "2026-08-10T12:39:04.579Z" },
+    { url = 
"https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size 
= 49951343, upload-time = "2026-08-10T12:39:11.8Z" },
+    { url = 
"https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size 
= 53144784, upload-time = "2026-08-10T12:39:20.503Z" },
+    { url = 
"https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl";,
 hash = 
"sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size 
= 27870159, upload-time = "2026-08-10T12:39:26.161Z" },
+    { url = 
"https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl";,
 hash = 
"sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size 
= 35885255, upload-time = "2026-08-10T12:39:32.366Z" },
+    { url = 
"https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size 
= 37644461, upload-time = "2026-08-10T12:39:38.142Z" },
+    { url = 
"https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size 
= 46877146, upload-time = "2026-08-10T12:39:43.722Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size 
= 50131616, upload-time = "2026-08-10T12:39:49.304Z" },
+    { url = 
"https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size 
= 50008879, upload-time = "2026-08-10T12:39:56.891Z" },
+    { url = 
"https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size 
= 53170864, upload-time = "2026-08-10T12:40:04.918Z" },
+    { url = 
"https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl";,
 hash = 
"sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size 
= 28620729, upload-time = "2026-08-10T12:40:51.41Z" },
+    { url = 
"https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl";,
 hash = 
"sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size 
= 36130288, upload-time = "2026-08-10T12:40:11.014Z" },
+    { url = 
"https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl";,
 hash = 
"sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size 
= 37762187, upload-time = "2026-08-10T12:40:16.592Z" },
+    { url = 
"https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl";,
 hash = 
"sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size 
= 46888003, upload-time = "2026-08-10T12:40:23.242Z" },
+    { url = 
"https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl";,
 hash = 
"sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size 
= 50079036, upload-time = "2026-08-10T12:40:29.169Z" },
+    { url = 
"https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl";,
 hash = 
"sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size 
= 50040226, upload-time = "2026-08-10T12:40:35.186Z" },
+    { url = 
"https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl";,
 hash = 
"sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size 
= 53149035, upload-time = "2026-08-10T12:40:41.454Z" },
+    { url = 
"https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl";,
 hash = 
"sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size 
= 28753071, upload-time = "2026-08-10T12:40:46.623Z" },
+]
+
 [[package]]
 name = "pyasn1"
 version = "0.6.4"
@@ -1356,6 +1424,18 @@ wheels = [
     { url = 
"https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl";,
 hash = 
"sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size 
= 149341, upload-time = "2025-09-25T21:32:56.828Z" },
 ]
 
+[[package]]
+name = "reactivex"
+version = "5.1.0"
+source = { registry = "https://pypi.org/simple"; }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = 
"https://files.pythonhosted.org/packages/f2/c3/eeb429d774c135a8bebe2b8ac51f9639fde1953506f062b42b9ba6e44176/reactivex-5.1.0.tar.gz";,
 hash = 
"sha256:b6b40269ebcbf24c53455c1b6790d682122cc8c01c907b8c8da47e2babb3b77e", size 
= 137788, upload-time = "2026-07-27T19:07:49.114Z" }
+wheels = [
+    { url = 
"https://files.pythonhosted.org/packages/e3/37/5b71117e68e5571c8c10942600eb02fb8c454c7347d08f8cddbdcde6ba6e/reactivex-5.1.0-py3-none-any.whl";,
 hash = 
"sha256:8668c0a3c8ae8694f1180421b367489d35a8affa281a3605014bf54364eed3a0", size 
= 257317, upload-time = "2026-07-27T19:07:47.631Z" },
+]
+
 [[package]]
 name = "regex"
 version = "2025.11.3"
@@ -1590,7 +1670,7 @@ resolution-markers = [
     "python_full_version < '3.11'",
 ]
 dependencies = [
-    { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.11'" },
+    { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; } },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz";,
 hash = 
"sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size 
= 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
 wheels = [
@@ -1651,7 +1731,7 @@ resolution-markers = [
     "python_full_version >= '3.11' and python_full_version < '3.13'",
 ]
 dependencies = [
-    { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.11' and 
python_full_version < '3.14'" },
+    { name = "numpy", version = "2.2.6", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
     { name = "numpy", version = "2.3.5", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
 ]
 sdist = { url = 
"https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz";,
 hash = 
"sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size 
= 30597883, upload-time = "2025-10-28T17:38:54.068Z" }

Reply via email to