From e4002dcf42938d32e9dc07cc9c384a61accb101a Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 13:34:55 +0200 Subject: [PATCH 1/9] set up ruff and mypy --- .github/workflows/test.yml | 29 +++++++++++++++++++++++- .pre-commit-config.yaml | 17 +++++++++++--- default.nix | 18 +++++++++++++++ docs/dev/testing.md | 28 +++++++++++++++++++++++ pyproject.toml | 46 +++++++++++++++++++++++++++++++++----- shell.nix | 16 +++++++++++++ 6 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 default.nix create mode 100644 shell.nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a513cbf..c981722 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,37 @@ -name: Tests +name: CI on: + push: + branches: [main] pull_request: branches: [main] jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff mypy + pip install -e ".[dev]" + + - name: Ruff check + run: ruff check . + + - name: Ruff format check + run: ruff format --check . + + - name: mypy + run: mypy metricsqlite + test: runs-on: ubuntu-latest strategy: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fb023f..dd5d4ad 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,18 @@ repos: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 26.3.1 + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.12 hooks: - - id: black + - id: ruff # Linter + args: [--fix] + - id: ruff-format # Formatter + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.20.2 + hooks: + - id: mypy + additional_dependencies: [pydantic>=2.0] + args: [--ignore-missing-imports] + files: ^metricsqlite/ diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..5726cff --- /dev/null +++ b/default.nix @@ -0,0 +1,18 @@ +{ lib, buildPythonPackage, fetchPypi, setuptools, setuptools-scm, fastapi +, pydantic, pymodbus }: + +buildPythonPackage { + pname = "metricsqlite"; + version = "0.0.0"; + format = "pyproject"; + + src = ./.; + + nativeBuildInputs = [ setuptools setuptools-scm ]; + propagatedBuildInputs = [ fastapi pydantic ]; + + meta = with lib; { + description = "MetricSQLite - MetricsQL api with SQLite as backend"; + license = licenses.mit; + }; +} diff --git a/docs/dev/testing.md b/docs/dev/testing.md index 456c7a4..4cb1ca2 100644 --- a/docs/dev/testing.md +++ b/docs/dev/testing.md @@ -1,5 +1,19 @@ +To set up a dev environment run; + ```bash +python3 -m venv .venv +source .venv/bin/activate +pip install --upgrade pip +pip install -e . pip install -e ".[dev]" + +pre-commit install +pre-commit autoupdate +``` + +### pytest + +```bash pytest # Or run a specific test file: @@ -8,3 +22,17 @@ pytest tests/test_client.py # Or a specific test: pytest tests/test_client.py::TestMetricsQLite::test_init_in_memory ``` + +### ruff + +```bash +ruff check . # Lint +ruff check . --fix # Lint + auto-fix +ruff format . # Format +``` + +### mypy + +```bash +mypy metricsqlite +``` diff --git a/pyproject.toml b/pyproject.toml index 91efc0e..7a581bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,12 +8,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["metricsqlite*"] -[tool.black] -line-length = 120 - [project] name = "metricsqlite" -description = "MetricsQLite - MetricsQL api with SQLite as backend" +description = "MetricSQLite - MetricsQL api with SQLite as backend" version = "0.0.0" authors = [{ name = "David van 't Wout", email = "david@vtwout.com" }] requires-python = ">=3.10" @@ -25,4 +22,43 @@ dependencies = [ fastapi = [ "fastapi>=0.100.0", ] -dev = ["black", "pre-commit", "pytest>=8.0", "mypy>=1.10", "httpx"] +dev = ["pre-commit", "pytest>=8.0", "mypy>=1.10", "ruff>=0.4", "httpx"] + + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes (unused imports, undefined names) + "I", # isort (import sorting) + "UP", # pyupgrade (modern Python syntax) + "B", # flake8-bugbear (common bugs) + "SIM", # flake8-simplify + "RUF", # Ruff-specific rules +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-first-party = ["metricsqlite"] + +[tool.ruff.format] +quote-style = "double" + +# ============================================================================= +# mypy - Static type checker +# ============================================================================= +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_ignores = true +disallow_untyped_defs = true +ignore_missing_imports = true # Start permissive, tighten later + +[[tool.mypy.overrides]] +module = "tests.*" +disallow_untyped_defs = false # Tests don't need full type annotations diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..2159a32 --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import { } }: + +let metricsqlite = pkgs.python3.pkgs.callPackage ./default.nix { }; +in pkgs.mkShell { + packages = with pkgs; + [ + (python3.withPackages (pp: + + metricsqlite.propagatedBuildInputs + ++ (with pp; [ pre-commit-hooks pytest mypy ruff ]))) + ]; + + shellHook = '' + export PYTHONPATH="$PWD:$PYTHONPATH" + ''; +} From 7316e93b77b0914da5b7fd2fc642858dcd00e8fc Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 14:03:18 +0200 Subject: [PATCH 2/9] ruff check . --fix --- .pre-commit-config.yaml | 8 -------- metricsqlite/__init__.py | 2 +- metricsqlite/client.py | 4 ++-- metricsqlite/engine/__init__.py | 24 ++++++++++++------------ metricsqlite/engine/executor.py | 24 +++++++++++++----------- metricsqlite/engine/parser/__init__.py | 14 +++++++------- metricsqlite/engine/parser/parser.py | 10 +++++----- metricsqlite/engine/query.py | 4 ++-- metricsqlite/engine/sqlite.py | 2 +- metricsqlite/fastapi/models.py | 6 ++---- metricsqlite/fastapi/routes.py | 8 ++++---- shell.nix | 24 +++++++++++++++++------- tests/engine/test_ast.py | 10 +++++----- tests/engine/test_executor.py | 6 +++--- tests/engine/test_lexer.py | 2 +- tests/engine/test_parser.py | 10 +++++----- tests/fastapi/test_routes.py | 1 - tests/queries/test_selectors.py | 4 +++- tests/test_client.py | 2 +- tests/test_lineprotocol.py | 1 - 20 files changed, 84 insertions(+), 82 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd5d4ad..c2d6f80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,11 +12,3 @@ repos: - id: ruff # Linter args: [--fix] - id: ruff-format # Formatter - - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.20.2 - hooks: - - id: mypy - additional_dependencies: [pydantic>=2.0] - args: [--ignore-missing-imports] - files: ^metricsqlite/ diff --git a/metricsqlite/__init__.py b/metricsqlite/__init__.py index 8fd6a9b..3e65ac0 100644 --- a/metricsqlite/__init__.py +++ b/metricsqlite/__init__.py @@ -2,4 +2,4 @@ from metricsqlite.exceptions import CompactedRangeError from metricsqlite.lineprotocol import LineProtocolError -__all__ = ["MetricsQLiteClient", "CompactedRangeError", "LineProtocolError", "fastapi"] +__all__ = ["CompactedRangeError", "LineProtocolError", "MetricsQLiteClient", "fastapi"] diff --git a/metricsqlite/client.py b/metricsqlite/client.py index 75b00b6..2e9756c 100644 --- a/metricsqlite/client.py +++ b/metricsqlite/client.py @@ -4,7 +4,7 @@ from datetime import datetime from pathlib import Path -from metricsqlite.engine import QueryEngine, QueryResult, MatrixResult +from metricsqlite.engine import MatrixResult, QueryEngine, QueryResult from metricsqlite.exceptions import CompactedRangeError from metricsqlite.util import parse_interval, parse_timestamp @@ -601,7 +601,7 @@ def _validate_compaction_interval( existing_size = row["bucket_size"] if interval_ms % existing_size != 0: raise ValueError( - f"New interval {interval_ms}ms is not a multiple of " f"existing bucket size {existing_size}ms." + f"New interval {interval_ms}ms is not a multiple of existing bucket size {existing_size}ms." ) def _compact_gauges_unlocked( diff --git a/metricsqlite/engine/__init__.py b/metricsqlite/engine/__init__.py index 65ef45d..0118d9f 100644 --- a/metricsqlite/engine/__init__.py +++ b/metricsqlite/engine/__init__.py @@ -1,34 +1,34 @@ """MetricsQL query parsing and execution.""" from metricsqlite.engine.executor import ( - Executor, ExecutionError, + Executor, InstantVector, - RangeVectorResult, MatrixResult, - ScalarResult, QueryResult, + RangeVectorResult, Sample, + ScalarResult, ) from metricsqlite.engine.parser import ( + BinaryExpr, # AST nodes Expr, - MetricSelector, - RangeVector, FunctionCall, - NumberLiteral, - StringLiteral, - BinaryExpr, - UnaryExpr, LabelMatcher, LabelMatchType, - # Lexer - tokenize, + MetricSelector, + NumberLiteral, + ParseError, + RangeVector, + StringLiteral, Token, TokenType, + UnaryExpr, # Parser parse, - ParseError, + # Lexer + tokenize, ) from metricsqlite.engine.query import QueryEngine diff --git a/metricsqlite/engine/executor.py b/metricsqlite/engine/executor.py index 2c7ae34..7f14c95 100644 --- a/metricsqlite/engine/executor.py +++ b/metricsqlite/engine/executor.py @@ -10,33 +10,35 @@ from dataclasses import dataclass from metricsqlite.engine.parser import ( + BinaryExpr, Expr, - MetricSelector, - RangeVector, FunctionCall, + MetricSelector, NumberLiteral, + RangeVector, StringLiteral, - BinaryExpr, UnaryExpr, ) from metricsqlite.engine.sqlite import ( - SQLiteAdapter, - RawSeriesSet, RawSeries, + RawSeriesSet, + SQLiteAdapter, +) +from metricsqlite.engine.sqlite import ( Sample as RawSample, ) __all__ = [ - "Executor", "ExecutionError", - "Sample", - "Series", - "Labels", + "Executor", "InstantVector", - "RangeVectorResult", - "ScalarResult", + "Labels", "MatrixResult", "QueryResult", + "RangeVectorResult", + "Sample", + "ScalarResult", + "Series", "raw_to_matrix", ] diff --git a/metricsqlite/engine/parser/__init__.py b/metricsqlite/engine/parser/__init__.py index dc6093b..d5ea889 100644 --- a/metricsqlite/engine/parser/__init__.py +++ b/metricsqlite/engine/parser/__init__.py @@ -1,19 +1,19 @@ """MetricsQL parser - lexer, AST, and parser.""" from .ast import ( + BinaryExpr, Expr, - MetricSelector, - RangeVector, FunctionCall, + LabelMatcher, + LabelMatchType, + MetricSelector, NumberLiteral, + RangeVector, StringLiteral, - BinaryExpr, UnaryExpr, - LabelMatcher, - LabelMatchType, ) -from .lexer import tokenize, Token, TokenType, LexerError -from .parser import parse, ParseError, parse_duration_string +from .lexer import LexerError, Token, TokenType, tokenize +from .parser import ParseError, parse, parse_duration_string __all__ = [ # AST nodes diff --git a/metricsqlite/engine/parser/parser.py b/metricsqlite/engine/parser/parser.py index bdacb67..98b568a 100644 --- a/metricsqlite/engine/parser/parser.py +++ b/metricsqlite/engine/parser/parser.py @@ -1,16 +1,16 @@ """Parser for MetricsQL queries.""" from .ast import ( + BinaryExpr, Expr, - MetricSelector, - RangeVector, FunctionCall, + LabelMatcher, + LabelMatchType, + MetricSelector, NumberLiteral, + RangeVector, StringLiteral, - BinaryExpr, UnaryExpr, - LabelMatcher, - LabelMatchType, ) from .lexer import Token, TokenType, tokenize, unescape_string diff --git a/metricsqlite/engine/query.py b/metricsqlite/engine/query.py index 792cba9..26216ed 100644 --- a/metricsqlite/engine/query.py +++ b/metricsqlite/engine/query.py @@ -10,8 +10,8 @@ QueryResult, raw_to_matrix, ) -from metricsqlite.engine.parser import MetricSelector, LabelMatchType, parse -from metricsqlite.util import parse_timestamp, parse_interval +from metricsqlite.engine.parser import LabelMatchType, MetricSelector, parse +from metricsqlite.util import parse_interval, parse_timestamp class QueryEngine: diff --git a/metricsqlite/engine/sqlite.py b/metricsqlite/engine/sqlite.py index 3f2c97d..5efa312 100644 --- a/metricsqlite/engine/sqlite.py +++ b/metricsqlite/engine/sqlite.py @@ -4,7 +4,7 @@ import sqlite3 from dataclasses import dataclass, field -from .parser import MetricSelector, LabelMatchType +from .parser import LabelMatchType, MetricSelector Labels = dict[str, str] diff --git a/metricsqlite/fastapi/models.py b/metricsqlite/fastapi/models.py index fdac978..977ea25 100644 --- a/metricsqlite/fastapi/models.py +++ b/metricsqlite/fastapi/models.py @@ -1,4 +1,4 @@ -from typing import Annotated, Literal, Generic, TypeVar +from typing import Annotated, Generic, Literal, TypeVar from pydantic import BaseModel, Field @@ -72,6 +72,4 @@ class ErrorResponse(BaseModel): class Response(Generic[T]): def __class_getitem__(cls, item: type) -> type: - return Annotated[ - SuccessResponse[item] | ErrorResponse, Field(discriminator="status") - ] # type: ignore[valid-type] + return Annotated[SuccessResponse[item] | ErrorResponse, Field(discriminator="status")] # type: ignore[valid-type] diff --git a/metricsqlite/fastapi/routes.py b/metricsqlite/fastapi/routes.py index 8833474..62a3970 100644 --- a/metricsqlite/fastapi/routes.py +++ b/metricsqlite/fastapi/routes.py @@ -21,12 +21,12 @@ from typing import TYPE_CHECKING +from metricsqlite.engine.executor import ExecutionError, InstantVector, MatrixResult, ScalarResult +from metricsqlite.engine.parser import LexerError, ParseError from metricsqlite.exceptions import QueryError, StorageError from metricsqlite.lineprotocol import LineProtocolError -from metricsqlite.engine.executor import ExecutionError -from metricsqlite.engine.parser import LexerError, ParseError -from metricsqlite.engine.executor import InstantVector, ScalarResult, MatrixResult -from .models import Response, QueryData, RangeQueryData, LabelsData, SeriesData + +from .models import LabelsData, QueryData, RangeQueryData, Response, SeriesData if TYPE_CHECKING: from metricsqlite.client import MetricsQLiteClient diff --git a/shell.nix b/shell.nix index 2159a32..711e564 100644 --- a/shell.nix +++ b/shell.nix @@ -1,14 +1,24 @@ { pkgs ? import { } }: +# Note: ruff is dynamically linked and the version installed by pip won't work on NixOS. +# This can be fixed by adding `programs.nix-ld.enable = true;` to your NixOS config. + let metricsqlite = pkgs.python3.pkgs.callPackage ./default.nix { }; in pkgs.mkShell { - packages = with pkgs; - [ - (python3.withPackages (pp: - - metricsqlite.propagatedBuildInputs - ++ (with pp; [ pre-commit-hooks pytest mypy ruff ]))) - ]; + packages = with pkgs; [ + (python3.withPackages (pp: + metricsqlite.propagatedBuildInputs ++ (with pp; [ + # Dev tools; + pre-commit-hooks + mypy + ruff + # pytest and dependencies; + pytest + fastapi + httpx + ]))) + pre-commit + ]; shellHook = '' export PYTHONPATH="$PWD:$PYTHONPATH" diff --git a/tests/engine/test_ast.py b/tests/engine/test_ast.py index d39247e..d3e0f0a 100644 --- a/tests/engine/test_ast.py +++ b/tests/engine/test_ast.py @@ -1,15 +1,15 @@ """Tests for the MetricsQL AST nodes.""" from metricsqlite.engine.parser import ( - MetricSelector, - RangeVector, + BinaryExpr, FunctionCall, + LabelMatcher, + LabelMatchType, + MetricSelector, NumberLiteral, + RangeVector, StringLiteral, - BinaryExpr, UnaryExpr, - LabelMatcher, - LabelMatchType, ) from metricsqlite.engine.parser.ast import _format_duration diff --git a/tests/engine/test_executor.py b/tests/engine/test_executor.py index 6109f0b..c7725c2 100644 --- a/tests/engine/test_executor.py +++ b/tests/engine/test_executor.py @@ -4,13 +4,13 @@ from metricsqlite import MetricsQLiteClient from metricsqlite.engine import ( - parse, + ExecutionError, Executor, InstantVector, + QueryEngine, RangeVectorResult, ScalarResult, - ExecutionError, - QueryEngine, + parse, ) EVAL_TIME = 946_681_200_000 # 2000-01-01 00:00:00 UTC diff --git a/tests/engine/test_lexer.py b/tests/engine/test_lexer.py index e22c652..e6abe27 100644 --- a/tests/engine/test_lexer.py +++ b/tests/engine/test_lexer.py @@ -2,7 +2,7 @@ import pytest -from metricsqlite.engine.parser import tokenize, TokenType, LexerError +from metricsqlite.engine.parser import LexerError, TokenType, tokenize class TestTokenize: diff --git a/tests/engine/test_parser.py b/tests/engine/test_parser.py index e75af9f..41be179 100644 --- a/tests/engine/test_parser.py +++ b/tests/engine/test_parser.py @@ -3,16 +3,16 @@ import pytest from metricsqlite.engine.parser import ( - MetricSelector, - RangeVector, + BinaryExpr, FunctionCall, + LabelMatchType, + MetricSelector, NumberLiteral, + ParseError, + RangeVector, StringLiteral, - BinaryExpr, UnaryExpr, - LabelMatchType, parse, - ParseError, parse_duration_string, ) diff --git a/tests/fastapi/test_routes.py b/tests/fastapi/test_routes.py index 83aefd1..d3837dd 100644 --- a/tests/fastapi/test_routes.py +++ b/tests/fastapi/test_routes.py @@ -1,5 +1,4 @@ import pytest - from fastapi import FastAPI from fastapi.testclient import TestClient diff --git a/tests/queries/test_selectors.py b/tests/queries/test_selectors.py index 3d8cd37..b7daec3 100644 --- a/tests/queries/test_selectors.py +++ b/tests/queries/test_selectors.py @@ -1,5 +1,7 @@ -import pytest import time + +import pytest + from metricsqlite import MetricsQLiteClient from metricsqlite.engine import InstantVector, RangeVectorResult, Sample diff --git a/tests/test_client.py b/tests/test_client.py index d9710bb..1b2b1bf 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,7 +2,7 @@ import pytest -from metricsqlite import MetricsQLiteClient, CompactedRangeError +from metricsqlite import CompactedRangeError, MetricsQLiteClient EVAL_TIME = 946_681_200_000 # 2000-01-01 00:00:00 UTC diff --git a/tests/test_lineprotocol.py b/tests/test_lineprotocol.py index 30582a6..add60ab 100644 --- a/tests/test_lineprotocol.py +++ b/tests/test_lineprotocol.py @@ -2,7 +2,6 @@ from metricsqlite.lineprotocol import ( LineProtocolError, - Point, parse_line, parse_lines, timestamp_to_ms, From 888b6c3dae0c7fa92c9fdf5196ff4dbf6dbd9cea Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 14:07:24 +0200 Subject: [PATCH 3/9] github actions: separate ruff and mypy jobs --- .github/workflows/test.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c981722..978d948 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ on: branches: [main] jobs: - lint: + ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -15,12 +15,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.14" - name: Install dependencies run: | python -m pip install --upgrade pip - pip install ruff mypy pip install -e ".[dev]" - name: Ruff check @@ -29,6 +28,21 @@ jobs: - name: Ruff format check run: ruff format --check . + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.14" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: mypy run: mypy metricsqlite From 5ed7c37321cb8c163af6fe946f268bd022d75008 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 14:13:36 +0200 Subject: [PATCH 4/9] github actions: add step summaries --- .github/workflows/test.yml | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 978d948..da9948b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,16 +17,30 @@ jobs: with: python-version: "3.14" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" + - name: Install ruff + run: pip install ruff - name: Ruff check - run: ruff check . + run: | + set +e + output=$(ruff check . --statistics 2>&1) + exit_code=$? + echo "## 📋 Ruff Lint Report" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$output" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + exit $exit_code - name: Ruff format check - run: ruff format --check . + run: | + set +e + output=$(ruff format --check . 2>&1) + exit_code=$? + echo "## 🎨 Ruff Format Report" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$output" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + exit $exit_code mypy: runs-on: ubuntu-latest @@ -44,7 +58,15 @@ jobs: pip install -e ".[dev]" - name: mypy - run: mypy metricsqlite + run: | + set +e + output=$(mypy metricsqlite 2>&1) + exit_code=$? + echo "## 🔍 mypy Report" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + echo "$output" >> $GITHUB_STEP_SUMMARY + echo "\`\`\`" >> $GITHUB_STEP_SUMMARY + exit $exit_code test: runs-on: ubuntu-latest From c53a9a209acc3edf8c41b2dfe33b4d2550cde116 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 14:44:54 +0200 Subject: [PATCH 5/9] fix mypy errors --- metricsqlite/client.py | 114 +++++++++++++++++---------- metricsqlite/engine/executor.py | 20 ++++- metricsqlite/engine/parser/parser.py | 8 +- metricsqlite/engine/query.py | 4 + metricsqlite/engine/sqlite.py | 3 +- metricsqlite/fastapi/models.py | 2 +- metricsqlite/fastapi/routes.py | 94 ++++++++++++---------- 7 files changed, 149 insertions(+), 96 deletions(-) diff --git a/metricsqlite/client.py b/metricsqlite/client.py index 2e9756c..1d13fff 100644 --- a/metricsqlite/client.py +++ b/metricsqlite/client.py @@ -46,6 +46,18 @@ def __init__( self._connection: sqlite3.Connection | None = None self._engine: QueryEngine | None = None + def _get_connection(self) -> sqlite3.Connection: + """Get the database connection, raising if not connected.""" + if self._connection is None: + raise RuntimeError("Not connected. Call connect() first.") + return self._connection + + def _get_engine(self) -> QueryEngine: + """Get the query engine, raising if not connected.""" + if self._engine is None: + raise RuntimeError("Not connected. Call connect() first.") + return self._engine + @property def series_table_name(self) -> str: return f"{self._tables_prefix}_series" @@ -72,32 +84,32 @@ def _get_parameters(self) -> dict[str, str]: "counter_insert": f"{self.counter_view_name}_insert", } - def connect(self): + def connect(self) -> None: if self._connection is not None: return db_path = self._db_path if self._db_path is not None else ":memory:" self._connection = sqlite3.connect(db_path, check_same_thread=False) self._connection.row_factory = sqlite3.Row if self._enable_wal: - self._connection.execute("PRAGMA journal_mode=WAL") + self._get_connection().execute("PRAGMA journal_mode=WAL") self._engine = QueryEngine( self._connection, self.series_table_name, self.data_table_name, ) - def create_tables(self): + def create_tables(self) -> None: with self._lock: self._create_tables_unlocked() - def _create_tables_unlocked(self): - self._connection.execute("PRAGMA foreign_keys = ON") + def _create_tables_unlocked(self) -> None: + self._get_connection().execute("PRAGMA foreign_keys = ON") gauge_insert = f"{self.gauge_view_name}_insert" counter_insert = f"{self.counter_view_name}_insert" # Series table - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE TABLE IF NOT EXISTS {self.series_table_name} ( series_id INTEGER PRIMARY KEY, type TEXT NOT NULL, @@ -107,7 +119,7 @@ def _create_tables_unlocked(self): """) # Data table - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE TABLE IF NOT EXISTS {self.data_table_name} ( series_id INTEGER NOT NULL, start INTEGER NOT NULL, @@ -123,7 +135,7 @@ def _create_tables_unlocked(self): """) # Gauge view on data table - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE VIEW IF NOT EXISTS {self.gauge_view_name} AS SELECT s.name, @@ -138,7 +150,7 @@ def _create_tables_unlocked(self): """) # INSERT trigger on gauge view - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE TRIGGER IF NOT EXISTS {gauge_insert} INSTEAD OF INSERT ON {self.gauge_view_name} BEGIN @@ -157,7 +169,7 @@ def _create_tables_unlocked(self): """) # Counter view on data table - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE VIEW IF NOT EXISTS {self.counter_view_name} AS SELECT s.name, @@ -173,7 +185,7 @@ def _create_tables_unlocked(self): # INSERT trigger on counter view # If the new value matches the last value, extend that row instead of inserting - self._connection.execute(f""" + self._get_connection().execute(f""" CREATE TRIGGER IF NOT EXISTS {counter_insert} INSTEAD OF INSERT ON {self.counter_view_name} BEGIN @@ -203,9 +215,9 @@ def _create_tables_unlocked(self): END """) - self._connection.commit() + self._get_connection().commit() - def close(self): + def close(self) -> None: with self._lock: if self._connection is not None: self._connection.close() @@ -215,7 +227,7 @@ def __enter__(self) -> "MetricsQLiteClient": self.connect() return self - def __exit__(self, exc_type: object, exc_val: object, exc_tb: object): + def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None: self.close() def query( @@ -241,7 +253,7 @@ def query( Query result (InstantVector, RangeVectorResult, or ScalarResult). """ with self._lock: - return self._engine.query(query, eval_time=time, step=step, timeout=timeout) + return self._get_engine().query(query, eval_time=time, step=step, timeout=timeout) def query_range( self, @@ -266,7 +278,7 @@ def query_range( MatrixResult containing series with multiple samples over time. """ with self._lock: - return self._engine.query_range(query, start=start, end=end, step=step, timeout=timeout) + return self._get_engine().query_range(query, start=start, end=end, step=step, timeout=timeout) @staticmethod def _build_time_filter( @@ -315,9 +327,9 @@ def get_labels( JOIN {self.data_table_name} d USING (series_id) WHERE {time_filter} """ - cursor = self._connection.execute(query, time_params) + cursor = self._get_connection().execute(query, time_params) else: - cursor = self._connection.execute(f"SELECT DISTINCT name, labels FROM {self.series_table_name}") + cursor = self._get_connection().execute(f"SELECT DISTINCT name, labels FROM {self.series_table_name}") rows = cursor.fetchall() if not rows: @@ -359,9 +371,9 @@ def get_label_values( JOIN {self.data_table_name} d USING (series_id) WHERE {time_filter} """ - cursor = self._connection.execute(query, time_params) + cursor = self._get_connection().execute(query, time_params) else: - cursor = self._connection.execute(f"SELECT DISTINCT name FROM {self.series_table_name}") + cursor = self._get_connection().execute(f"SELECT DISTINCT name FROM {self.series_table_name}") return sorted(row["name"] for row in cursor.fetchall()) if time_filter: @@ -371,9 +383,9 @@ def get_label_values( JOIN {self.data_table_name} d USING (series_id) WHERE json_extract(s.labels, '$.{label_name}') IS NOT NULL AND {time_filter} """ - cursor = self._connection.execute(query, time_params) + cursor = self._get_connection().execute(query, time_params) else: - cursor = self._connection.execute( + cursor = self._get_connection().execute( f"SELECT DISTINCT json_extract(labels, '$.{label_name}') AS value " f"FROM {self.series_table_name} " f"WHERE json_extract(labels, '$.{label_name}') IS NOT NULL" @@ -407,7 +419,7 @@ def get_series( seen = set() for selector in match: - series_list = self._engine.find_series(selector, start_ms, end_ms) + series_list = self._get_engine().find_series(selector, start_ms, end_ms) for name, labels_json in series_list: key = (name, labels_json) if key not in seen: @@ -418,7 +430,9 @@ def get_series( return results - def insert_gauge(self, name: str, value: float, timestamp: float | str | datetime, labels: dict | None = None): + def insert_gauge( + self, name: str, value: float, timestamp: float | str | datetime, labels: dict | None = None + ) -> None: """Insert a gauge metric sample. Args: @@ -430,11 +444,11 @@ def insert_gauge(self, name: str, value: float, timestamp: float | str | datetim Raises: CompactedRangeError: If the timestamp falls within a compacted time range. """ - timestamp = parse_timestamp(timestamp) + ts = parse_timestamp(timestamp) labels_json = json.dumps(labels or {}, sort_keys=True) with self._lock: # Check if timestamp falls within a compacted bucket - cursor = self._connection.execute( + cursor = self._get_connection().execute( f""" SELECT d.start, d.end FROM {self.data_table_name} d @@ -444,19 +458,21 @@ def insert_gauge(self, name: str, value: float, timestamp: float | str | datetim AND d.start <= ? AND d.end > ? LIMIT 1 """, - (name, labels_json, timestamp, timestamp), + (name, labels_json, ts, ts), ) row = cursor.fetchone() if row: raise CompactedRangeError(f"Cannot insert into compacted range [{row['start']}, {row['end']})") - self._connection.execute( + self._get_connection().execute( f"INSERT INTO {self.gauge_view_name}(name, labels, start, value) VALUES (?, ?, ?, ?)", - (name, labels_json, timestamp, value), + (name, labels_json, ts, value), ) - self._connection.commit() + self._get_connection().commit() - def insert_counter(self, name: str, value: float, timestamp: float | str | datetime, labels: dict | None = None): + def insert_counter( + self, name: str, value: float, timestamp: float | str | datetime, labels: dict | None = None + ) -> None: """Insert a counter metric sample. Args: @@ -465,14 +481,14 @@ def insert_counter(self, name: str, value: float, timestamp: float | str | datet timestamp: Unix timestamp in milliseconds or datetime. labels: Optional dict of labels. """ - timestamp = parse_timestamp(timestamp) + ts = parse_timestamp(timestamp) labels_json = json.dumps(labels or {}, sort_keys=True) with self._lock: - self._connection.execute( + self._get_connection().execute( f"INSERT INTO {self.counter_view_name}(name, labels, start, value) VALUES (?, ?, ?, ?)", - (name, labels_json, timestamp, value), + (name, labels_json, ts, value), ) - self._connection.commit() + self._get_connection().commit() def write_line_protocol(self, data: str, precision: str = "ns") -> int: """Write data using InfluxDB line protocol. @@ -545,8 +561,20 @@ def compact_gauges( ValueError: If the new interval is not compatible with existing bucket sizes. """ - older_than_ms = int(parse_timestamp(older_than)) - interval_seconds = round(parse_interval(interval)) + older_than_ms = parse_timestamp(older_than) + if older_than_ms is None: + raise ValueError( + f"Invalid timestamp for 'older_than': {older_than!r}. " + "Expected Unix timestamp (ms), ISO 8601 string, or datetime object." + ) + older_than_ms = int(older_than_ms) + interval_seconds = parse_interval(interval) + if interval_seconds is None: + raise ValueError( + f"Invalid interval: {interval!r}. " + "Expected number of seconds or duration string (e.g., '1h', '30m', '1d')." + ) + interval_seconds = round(interval_seconds) interval_ms = interval_seconds * 1000 # Align cutoff to interval boundary (in ms) @@ -586,7 +614,7 @@ def _validate_compaction_interval( if filter_clause: where += f" AND {filter_clause}" - cursor = self._connection.execute( + cursor = self._get_connection().execute( f""" SELECT DISTINCT (d.end - d.start) AS bucket_size FROM {self.data_table_name} AS d @@ -626,7 +654,7 @@ def _compact_gauges_unlocked( HAVING row_count > 1 ORDER BY bucket """ - cursor = self._connection.execute(bucket_query, [interval_ms, interval_ms, cutoff] + filter_params) + cursor = self._get_connection().execute(bucket_query, [interval_ms, interval_ms, cutoff] + filter_params) buckets = cursor.fetchall() total_sample_count = 0 @@ -639,7 +667,7 @@ def _compact_gauges_unlocked( total_samples = row["total_samples"] # Fetch all samples in this bucket - cursor = self._connection.execute( + cursor = self._get_connection().execute( f""" SELECT start, end, sample_count, positive, negative, min, max FROM {self.data_table_name} @@ -672,11 +700,11 @@ def _compact_gauges_unlocked( avg_negative = total_negative / sample_count # Replace multiple rows with single averaged row - self._connection.execute( + self._get_connection().execute( f"DELETE FROM {self.data_table_name} WHERE series_id = ? AND start >= ? AND start < ?", (series_id, bucket_start, bucket_end), ) - self._connection.execute( + self._get_connection().execute( f"INSERT INTO {self.data_table_name} (series_id, start, end, sample_count, positive, negative, min, max) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ( series_id, @@ -693,5 +721,5 @@ def _compact_gauges_unlocked( total_sample_count += sample_count total_bucket_count += 1 - self._connection.commit() + self._get_connection().commit() return total_sample_count, total_bucket_count diff --git a/metricsqlite/engine/executor.py b/metricsqlite/engine/executor.py index 7f14c95..01c8ec6 100644 --- a/metricsqlite/engine/executor.py +++ b/metricsqlite/engine/executor.py @@ -7,6 +7,7 @@ """ import sqlite3 +from collections.abc import Callable from dataclasses import dataclass from metricsqlite.engine.parser import ( @@ -502,6 +503,7 @@ def _apply_transformation( inner = self._evaluate(args[0], ctx) # Get the transformation function + fn: Callable[[float], float] if name == "abs": fn = abs elif name == "clamp_min": @@ -510,14 +512,24 @@ def _apply_transformation( min_val = self._evaluate(args[1], ctx) if not isinstance(min_val, (int, float)): raise ExecutionError("clamp_min second argument must be a scalar") - fn = lambda v, m=min_val: max(v, m) + threshold = float(min_val) + + def _clamp_min(v: float, m: float = threshold) -> float: + return max(v, m) + + fn = _clamp_min elif name == "clamp_max": if len(args) != 2: raise ExecutionError("clamp_max requires 2 arguments") max_val = self._evaluate(args[1], ctx) if not isinstance(max_val, (int, float)): raise ExecutionError("clamp_max second argument must be a scalar") - fn = lambda v, m=max_val: min(v, m) + threshold = float(max_val) + + def _clamp_max(v: float, m: float = threshold) -> float: + return min(v, m) + + fn = _clamp_max else: raise ExecutionError(f"Unknown transformation function: {name}") @@ -590,7 +602,7 @@ def _apply_aggregation( def _map_windowed( self, ws: _WindowedSeriesSet, - fn, + fn: Callable[[float], float], ) -> _WindowedSeriesSet: """Apply a function to all values in a _WindowedSeriesSet.""" result = [] @@ -832,7 +844,7 @@ def _apply_binary_op(op: str, left: float, right: float) -> float: elif op == "%": return left % right if right != 0 else float("nan") elif op == "^": - return left**right + return float(left**right) elif op == ">": return float(left > right) elif op == "<": diff --git a/metricsqlite/engine/parser/parser.py b/metricsqlite/engine/parser/parser.py index 98b568a..0035178 100644 --- a/metricsqlite/engine/parser/parser.py +++ b/metricsqlite/engine/parser/parser.py @@ -152,13 +152,13 @@ def parse_primary(self) -> Expr: # Number literal if self.match(TokenType.NUMBER): - value = float(self.advance().value) - return NumberLiteral(value) + num_value = float(self.advance().value) + return NumberLiteral(num_value) # String literal if self.match(TokenType.STRING): - value = unescape_string(self.advance().value) - return StringLiteral(value) + str_value = unescape_string(self.advance().value) + return StringLiteral(str_value) # Identifier: could be metric selector or function call if self.match(TokenType.IDENT): diff --git a/metricsqlite/engine/query.py b/metricsqlite/engine/query.py index 26216ed..309d221 100644 --- a/metricsqlite/engine/query.py +++ b/metricsqlite/engine/query.py @@ -119,6 +119,10 @@ def query_range( result = self._executor.execute_range(ast, start_ms, end_ms, step_seconds) + if isinstance(result, float): + # Scalar result - return empty matrix (scalar range queries not supported) + return MatrixResult(series=[]) + return raw_to_matrix(result) def find_series( diff --git a/metricsqlite/engine/sqlite.py b/metricsqlite/engine/sqlite.py index 5efa312..cc5cb46 100644 --- a/metricsqlite/engine/sqlite.py +++ b/metricsqlite/engine/sqlite.py @@ -2,6 +2,7 @@ import json import sqlite3 +from collections.abc import Callable from dataclasses import dataclass, field from .parser import LabelMatchType, MetricSelector @@ -36,7 +37,7 @@ class RawSeriesSet: series: list[RawSeries] = field(default_factory=list) - def map_values(self, fn) -> "RawSeriesSet": + def map_values(self, fn: Callable[[float], float]) -> "RawSeriesSet": """Apply a function to each sample value.""" return RawSeriesSet( [ diff --git a/metricsqlite/fastapi/models.py b/metricsqlite/fastapi/models.py index 977ea25..26875f4 100644 --- a/metricsqlite/fastapi/models.py +++ b/metricsqlite/fastapi/models.py @@ -72,4 +72,4 @@ class ErrorResponse(BaseModel): class Response(Generic[T]): def __class_getitem__(cls, item: type) -> type: - return Annotated[SuccessResponse[item] | ErrorResponse, Field(discriminator="status")] # type: ignore[valid-type] + return Annotated[SuccessResponse[item] | ErrorResponse, Field(discriminator="status")] # type: ignore[return-value, valid-type] diff --git a/metricsqlite/fastapi/routes.py b/metricsqlite/fastapi/routes.py index 62a3970..1098dcd 100644 --- a/metricsqlite/fastapi/routes.py +++ b/metricsqlite/fastapi/routes.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING -from metricsqlite.engine.executor import ExecutionError, InstantVector, MatrixResult, ScalarResult +from metricsqlite.engine.executor import ExecutionError, InstantVector, MatrixResult, RangeVectorResult, ScalarResult from metricsqlite.engine.parser import LexerError, ParseError from metricsqlite.exceptions import QueryError, StorageError from metricsqlite.lineprotocol import LineProtocolError @@ -32,7 +32,7 @@ from metricsqlite.client import MetricsQLiteClient -def _error_response(error_type: str, message: str, status_code: int = 400): +def _error_response(error_type: str, message: str, status_code: int = 400) -> JSONResponse: """Create a VictoriaMetrics-compatible error response.""" return JSONResponse( status_code=status_code, @@ -40,49 +40,55 @@ def _error_response(error_type: str, message: str, status_code: int = 400): ) -def _format_query_result(result): +def _format_query_result(result: InstantVector | RangeVectorResult | ScalarResult) -> JSONResponse: """Convert executor result to VictoriaMetrics API format.""" if isinstance(result, InstantVector): - return { + return JSONResponse( + { + "status": "success", + "data": { + "resultType": "vector", + "result": [ + { + "metric": labels, + "value": [sample.timestamp / 1000, str(sample.value)], + } + for labels, sample in result.series + ], + }, + } + ) + elif isinstance(result, ScalarResult): + return JSONResponse( + { + "status": "success", + "data": { + "resultType": "scalar", + "result": [result.timestamp / 1000, str(result.value)], + }, + } + ) + else: + raise ValueError(f"Unexpected result type: {type(result)}") + + +def _format_range_result(result: MatrixResult) -> JSONResponse: + """Convert range query result to VictoriaMetrics API format.""" + return JSONResponse( + { "status": "success", "data": { - "resultType": "vector", + "resultType": "matrix", "result": [ { "metric": labels, - "value": [sample.timestamp / 1000, str(sample.value)], + "values": [[sample.timestamp / 1000, str(sample.value)] for sample in values], } - for labels, sample in result.series + for labels, values in result.series ], }, } - elif isinstance(result, ScalarResult): - return { - "status": "success", - "data": { - "resultType": "scalar", - "result": [result.timestamp / 1000, str(result.value)], - }, - } - else: - raise ValueError(f"Unexpected result type: {type(result)}") - - -def _format_range_result(result: MatrixResult): - """Convert range query result to VictoriaMetrics API format.""" - return { - "status": "success", - "data": { - "resultType": "matrix", - "result": [ - { - "metric": labels, - "values": [[sample.timestamp / 1000, str(sample.value)] for sample in values], - } - for labels, values in result.series - ], - }, - } + ) def create_router(client: "MetricsQLiteClient", enable_writes: bool = False): # type: ignore[no-untyped-def] @@ -108,7 +114,7 @@ def get_query( time: float | str | None = Query(None, description="Evaluation timestamp"), step: float | str | None = Query(None, description="Interval"), timeout: float | str | None = Query(None, description="Query timeout"), - ): + ) -> JSONResponse: try: result = client.query(query, time=time, step=step, timeout=timeout) return _format_query_result(result) @@ -128,7 +134,7 @@ def get_query_range( end: float | str | None = Query(None, description="End timestamp"), step: float | str | None = Query(None, description="Query resolution step in seconds"), timeout: float | str | None = Query(None, description="Query timeout"), - ): + ) -> JSONResponse: try: result = client.query_range(query, start, end=end, step=step, timeout=timeout) return _format_range_result(result) @@ -146,9 +152,9 @@ def get_labels( start: float | str | None = Query(None, description="Start timestamp"), end: float | str | None = Query(None, description="End timestamp"), match: list[str] | None = Query(None, alias="match[]", description="Series selector"), - ): + ) -> JSONResponse: try: - return {"status": "success", "data": client.get_labels(start=start, end=end, match=match)} + return JSONResponse({"status": "success", "data": client.get_labels(start=start, end=end, match=match)}) except StorageError as e: return _error_response("internal", str(e), 500) except ValueError as e: @@ -160,9 +166,11 @@ def get_label_values( start: float | str | None = Query(None, description="Start timestamp"), end: float | str | None = Query(None, description="End timestamp"), match: list[str] | None = Query(None, alias="match[]", description="Series selector"), - ): + ) -> JSONResponse: try: - return {"status": "success", "data": client.get_label_values(label_name, start=start, end=end, match=match)} + return JSONResponse( + {"status": "success", "data": client.get_label_values(label_name, start=start, end=end, match=match)} + ) except StorageError as e: return _error_response("internal", str(e), 500) except ValueError as e: @@ -173,9 +181,9 @@ def get_series( match: list[str] = Query(..., alias="match[]", description="Series selector (required)"), start: float | str | None = Query(None, description="Start timestamp"), end: float | str | None = Query(None, description="End timestamp"), - ): + ) -> JSONResponse: try: - return {"status": "success", "data": client.get_series(match=match, start=start, end=end)} + return JSONResponse({"status": "success", "data": client.get_series(match=match, start=start, end=end)}) except (ParseError, ExecutionError, LexerError) as e: return _error_response("bad_data", str(e), 400) except StorageError as e: @@ -190,7 +198,7 @@ async def influx_write( request: Request, db: str | None = Query(None, description="Database name (ignored)"), precision: str = Query("ns", description="Timestamp precision: ns, us, ms, s"), - ): + ) -> PlainTextResponse: """Write data using InfluxDB line protocol. Compatible with VictoriaMetrics /influx/write endpoint. From 11c3bd9bb4f8b56ba0c08be1e45dde68a36cc32a Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 14:45:37 +0200 Subject: [PATCH 6/9] pre-commit: don't run ruff --fix --- .pre-commit-config.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2d6f80..e393cb1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,4 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.12 hooks: - - id: ruff # Linter - args: [--fix] - id: ruff-format # Formatter From 9e448738de4e39500340cedbe47c7948e52feccf Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 15:09:59 +0200 Subject: [PATCH 7/9] add pytest-cov to dev dependencies --- docs/dev/testing.md | 6 ++++++ pyproject.toml | 6 +++++- shell.nix | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/dev/testing.md b/docs/dev/testing.md index 4cb1ca2..6771ed7 100644 --- a/docs/dev/testing.md +++ b/docs/dev/testing.md @@ -21,6 +21,9 @@ pytest tests/test_client.py # Or a specific test: pytest tests/test_client.py::TestMetricsQLite::test_init_in_memory + +# For a code coverage report: +pytest --cov=metricsqlite --cov-report=term-missing ``` ### ruff @@ -29,6 +32,9 @@ pytest tests/test_client.py::TestMetricsQLite::test_init_in_memory ruff check . # Lint ruff check . --fix # Lint + auto-fix ruff format . # Format + +# pyproject.toml sets `output-format = "concise"`. So show more details run; +ruff check --output-format=full . ``` ### mypy diff --git a/pyproject.toml b/pyproject.toml index 7a581bc..4309b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,12 +22,13 @@ dependencies = [ fastapi = [ "fastapi>=0.100.0", ] -dev = ["pre-commit", "pytest>=8.0", "mypy>=1.10", "ruff>=0.4", "httpx"] +dev = ["pre-commit", "pytest>=8.0", "pytest-cov", "mypy>=1.10", "ruff>=0.4", "httpx"] [tool.ruff] line-length = 120 target-version = "py310" +output-format = "concise" [tool.ruff.lint] select = [ @@ -46,6 +47,9 @@ ignore = [ [tool.ruff.lint.isort] known-first-party = ["metricsqlite"] +[tool.ruff.lint.per-file-ignores] +"metricsqlite/fastapi/*" = ["B008"] # Query() in defaults is standard FastAPI pattern + [tool.ruff.format] quote-style = "double" diff --git a/shell.nix b/shell.nix index 711e564..37bb3f1 100644 --- a/shell.nix +++ b/shell.nix @@ -14,6 +14,7 @@ in pkgs.mkShell { ruff # pytest and dependencies; pytest + pytest-cov fastapi httpx ]))) From 8dc36c649a6af2e0bd48dbbafeb7fca6f8420e3b Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 15:12:47 +0200 Subject: [PATCH 8/9] fix ruff errors --- metricsqlite/client.py | 9 ++--- metricsqlite/engine/__init__.py | 41 ++++++++----------- metricsqlite/engine/parser/__init__.py | 21 +++++----- metricsqlite/engine/sqlite.py | 5 +-- metricsqlite/fastapi/__init__.py | 2 + metricsqlite/lineprotocol.py | 16 ++++---- metricsqlite/util.py | 8 ++-- pyproject.toml | 6 +-- tests/engine/test_lexer.py | 8 ++-- tests/fastapi/test_routes.py | 24 ++++++------ tests/queries/test_query_range.py | 6 +-- tests/queries/test_selectors.py | 54 +++++++++++++------------- 12 files changed, 92 insertions(+), 108 deletions(-) diff --git a/metricsqlite/client.py b/metricsqlite/client.py index 1d13fff..f299032 100644 --- a/metricsqlite/client.py +++ b/metricsqlite/client.py @@ -522,10 +522,7 @@ def write_line_protocol(self, data: str, precision: str = "ns") -> int: continue # Construct metric name - if field_name == "value": - metric_name = point.measurement - else: - metric_name = f"{point.measurement}_{field_name}" + metric_name = point.measurement if field_name == "value" else f"{point.measurement}_{field_name}" self.insert_gauge(metric_name, float(field_value), timestamp_ms, point.tags or None) count += 1 @@ -621,7 +618,7 @@ def _validate_compaction_interval( JOIN {self.series_table_name} AS s USING (series_id) WHERE {where} """, - [cutoff] + filter_params, + [cutoff, *filter_params], ) existing_sizes = cursor.fetchall() @@ -654,7 +651,7 @@ def _compact_gauges_unlocked( HAVING row_count > 1 ORDER BY bucket """ - cursor = self._get_connection().execute(bucket_query, [interval_ms, interval_ms, cutoff] + filter_params) + cursor = self._get_connection().execute(bucket_query, [interval_ms, interval_ms, cutoff, *filter_params]) buckets = cursor.fetchall() total_sample_count = 0 diff --git a/metricsqlite/engine/__init__.py b/metricsqlite/engine/__init__.py index 0118d9f..28a54d7 100644 --- a/metricsqlite/engine/__init__.py +++ b/metricsqlite/engine/__init__.py @@ -12,7 +12,6 @@ ) from metricsqlite.engine.parser import ( BinaryExpr, - # AST nodes Expr, FunctionCall, LabelMatcher, @@ -25,40 +24,34 @@ Token, TokenType, UnaryExpr, - # Parser parse, - # Lexer tokenize, ) from metricsqlite.engine.query import QueryEngine __all__ = [ - # Lexer - "tokenize", - "Token", - "TokenType", - # Parser - "parse", - "ParseError", - # AST nodes + "BinaryExpr", + "ExecutionError", + "Executor", "Expr", - "MetricSelector", - "RangeVector", "FunctionCall", - "NumberLiteral", - "StringLiteral", - "BinaryExpr", - "UnaryExpr", + "InstantVector", "LabelMatcher", "LabelMatchType", - # Executor - "Executor", - "ExecutionError", - "InstantVector", - "RangeVectorResult", "MatrixResult", - "ScalarResult", + "MetricSelector", + "NumberLiteral", + "ParseError", + "QueryEngine", "QueryResult", + "RangeVector", + "RangeVectorResult", "Sample", - "QueryEngine", + "ScalarResult", + "StringLiteral", + "Token", + "TokenType", + "UnaryExpr", + "parse", + "tokenize", ] diff --git a/metricsqlite/engine/parser/__init__.py b/metricsqlite/engine/parser/__init__.py index d5ea889..4df02dc 100644 --- a/metricsqlite/engine/parser/__init__.py +++ b/metricsqlite/engine/parser/__init__.py @@ -16,24 +16,21 @@ from .parser import ParseError, parse, parse_duration_string __all__ = [ - # AST nodes + "BinaryExpr", "Expr", - "MetricSelector", - "RangeVector", "FunctionCall", - "NumberLiteral", - "StringLiteral", - "BinaryExpr", - "UnaryExpr", "LabelMatcher", "LabelMatchType", - # Lexer - "tokenize", + "LexerError", + "MetricSelector", + "NumberLiteral", + "ParseError", + "RangeVector", + "StringLiteral", "Token", "TokenType", - "LexerError", - # Parser + "UnaryExpr", "parse", - "ParseError", "parse_duration_string", + "tokenize", ] diff --git a/metricsqlite/engine/sqlite.py b/metricsqlite/engine/sqlite.py index cc5cb46..8ac9baa 100644 --- a/metricsqlite/engine/sqlite.py +++ b/metricsqlite/engine/sqlite.py @@ -228,10 +228,7 @@ def fetch_instant( sample_end = float(row["end"]) if row["end"] is not None else None # For compacted data, clamp the timestamp to eval_time - if sample_end is not None: - timestamp = min(sample_end, time) - else: - timestamp = sample_start + timestamp = min(sample_end, time) if sample_end is not None else sample_start series_list.append( RawSeries( diff --git a/metricsqlite/fastapi/__init__.py b/metricsqlite/fastapi/__init__.py index ed0ff12..7b6f4a1 100644 --- a/metricsqlite/fastapi/__init__.py +++ b/metricsqlite/fastapi/__init__.py @@ -1 +1,3 @@ from .routes import create_router + +__all__ = ["create_router"] diff --git a/metricsqlite/lineprotocol.py b/metricsqlite/lineprotocol.py index 506c5d8..385954f 100644 --- a/metricsqlite/lineprotocol.py +++ b/metricsqlite/lineprotocol.py @@ -55,21 +55,21 @@ def _parse_field_value(value: str) -> float | int | str | bool: if value.endswith("i"): try: return int(value[:-1]) - except ValueError: - raise LineProtocolError(f"Invalid integer: {value}") + except ValueError as e: + raise LineProtocolError(f"Invalid integer: {value}") from e # Unsigned integer (suffix u) - treat as int if value.endswith("u"): try: return int(value[:-1]) - except ValueError: - raise LineProtocolError(f"Invalid unsigned integer: {value}") + except ValueError as e: + raise LineProtocolError(f"Invalid unsigned integer: {value}") from e # Float (default) try: return float(value) - except ValueError: - raise LineProtocolError(f"Invalid field value: {value}") + except ValueError as e: + raise LineProtocolError(f"Invalid field value: {value}") from e def _find_field_separator(s: str) -> int: @@ -218,8 +218,8 @@ def parse_line(line: str) -> Point | None: if timestamp_str: try: timestamp = int(timestamp_str) - except ValueError: - raise LineProtocolError(f"Invalid timestamp: {timestamp_str}") + except ValueError as e: + raise LineProtocolError(f"Invalid timestamp: {timestamp_str}") from e return Point(measurement=measurement, tags=tags, fields=fields, timestamp=timestamp) diff --git a/metricsqlite/util.py b/metricsqlite/util.py index c545a2f..f6e1cbf 100644 --- a/metricsqlite/util.py +++ b/metricsqlite/util.py @@ -50,8 +50,8 @@ def parse_timestamp(timestamp: float | str | datetime | None) -> float | None: if value > 4_102_444_800: return value return value * 1000 - except ValueError: - raise ValueError(f"Invalid timestamp format: {timestamp}") + except ValueError as e: + raise ValueError(f"Invalid timestamp format: {timestamp}") from e _INTERVAL_PATTERN = re.compile(r"^(\d+(?:\.\d+)?)(ms|s|m|h|d|w|y)$") @@ -98,5 +98,5 @@ def parse_interval(interval: float | str | None) -> float | None: interval = interval.replace("_", "") try: return float(interval) - except ValueError: - raise ValueError(f"Invalid interval format: {interval}") + except ValueError as e: + raise ValueError(f"Invalid interval format: {interval}") from e diff --git a/pyproject.toml b/pyproject.toml index 4309b8e..17775cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ select = [ ] ignore = [ "E501", # line too long (handled by formatter) + "RUF022", # __all__ sorting - alphabetical is fine ] [tool.ruff.lint.isort] @@ -53,15 +54,12 @@ known-first-party = ["metricsqlite"] [tool.ruff.format] quote-style = "double" -# ============================================================================= -# mypy - Static type checker -# ============================================================================= + [tool.mypy] python_version = "3.10" warn_return_any = true warn_unused_ignores = true disallow_untyped_defs = true -ignore_missing_imports = true # Start permissive, tighten later [[tool.mypy.overrides]] module = "tests.*" diff --git a/tests/engine/test_lexer.py b/tests/engine/test_lexer.py index e6abe27..b40e66e 100644 --- a/tests/engine/test_lexer.py +++ b/tests/engine/test_lexer.py @@ -40,7 +40,7 @@ def test_selector_multiple_labels(self): def test_single_quoted_string(self): tokens = tokenize("metric{label='value'}") - string_token = [t for t in tokens if t.type == TokenType.STRING][0] + string_token = next(t for t in tokens if t.type == TokenType.STRING) assert string_token.value == "'value'" def test_duration_simple(self): @@ -84,7 +84,7 @@ def test_function_call(self): def test_function_with_number_arg(self): tokens = tokenize("clamp_min(metric, 0)") - number_token = [t for t in tokens if t.type == TokenType.NUMBER][0] + number_token = next(t for t in tokens if t.type == TokenType.NUMBER) assert number_token.value == "0" def test_negative_number(self): @@ -98,12 +98,12 @@ def test_negative_number(self): def test_decimal_number(self): tokens = tokenize("metric * 3.14") - number_token = [t for t in tokens if t.type == TokenType.NUMBER][0] + number_token = next(t for t in tokens if t.type == TokenType.NUMBER) assert number_token.value == "3.14" def test_scientific_notation(self): tokens = tokenize("metric * 1e6") - number_token = [t for t in tokens if t.type == TokenType.NUMBER][0] + number_token = next(t for t in tokens if t.type == TokenType.NUMBER) assert number_token.value == "1e6" def test_operators(self): diff --git a/tests/fastapi/test_routes.py b/tests/fastapi/test_routes.py index d3837dd..b9c21d8 100644 --- a/tests/fastapi/test_routes.py +++ b/tests/fastapi/test_routes.py @@ -50,7 +50,7 @@ def test_query_with_labels(self, test_client): assert data["data"]["result"][0]["metric"]["location"] == "kitchen" def test_query_no_results(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/query", params={"query": "nonexistent", "time": EVAL_TIME}) @@ -60,7 +60,7 @@ def test_query_no_results(self, test_client): assert data["data"]["result"] == [] def test_query_parse_error(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/query", params={"query": "invalid{{{", "time": EVAL_TIME}) @@ -71,7 +71,7 @@ def test_query_parse_error(self, test_client): assert "error" in data def test_query_missing_required_param(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/query") @@ -103,7 +103,7 @@ def test_query_range_success(self, test_client): assert len(data["data"]["result"][0]["values"]) >= 1 def test_query_range_parse_error(self, test_client): - http, db = test_client + http, _db = test_client response = http.get( "/api/v1/query_range", @@ -131,7 +131,7 @@ def test_labels_success(self, test_client): assert "floor" in data["data"] def test_labels_empty(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/labels") @@ -240,14 +240,14 @@ def test_series_no_match(self, test_client): assert data["data"] == [] def test_series_missing_match_param(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/series") assert response.status_code == 422 # FastAPI validation error def test_series_parse_error(self, test_client): - http, db = test_client + http, _db = test_client response = http.get("/api/v1/series", params={"match[]": "invalid{{{"}) @@ -275,7 +275,7 @@ def write_client(): class TestInfluxWriteEndpoint: def test_write_disabled_by_default(self, test_client): """Write endpoint should not exist when writes are disabled.""" - http, db = test_client + http, _db = test_client response = http.post("/api/v1/influx/write", content="temperature value=25.5") @@ -298,7 +298,7 @@ def test_write_simple(self, write_client): assert rows[0]["name"] == "temperature" def test_write_with_tags(self, write_client): - http, db = write_client + http, _db = write_client response = http.post( "/api/v1/influx/write", @@ -322,7 +322,7 @@ def test_write_multiple_lines(self, write_client): assert cursor.fetchone()["cnt"] == 2 def test_write_precision_seconds(self, write_client): - http, db = write_client + http, _db = write_client response = http.post( "/api/v1/influx/write", @@ -333,7 +333,7 @@ def test_write_precision_seconds(self, write_client): assert response.status_code == 204 def test_write_parse_error(self, write_client): - http, db = write_client + http, _db = write_client response = http.post("/api/v1/influx/write", content="invalid line protocol!!!") @@ -341,7 +341,7 @@ def test_write_parse_error(self, write_client): def test_write_db_param_ignored(self, write_client): """The db parameter should be accepted but ignored.""" - http, db = write_client + http, _db = write_client response = http.post( "/api/v1/influx/write", diff --git a/tests/queries/test_query_range.py b/tests/queries/test_query_range.py index 6358466..c534bf2 100644 --- a/tests/queries/test_query_range.py +++ b/tests/queries/test_query_range.py @@ -54,7 +54,7 @@ def test_query_range_lookback(self, client: MetricsQLiteClient): result = client.query_range(query="metric", start=START, end=START + 3_600_000, step="5m") assert isinstance(result, MatrixResult) - labels, series = result.series[0] + _labels, series = result.series[0] assert [s.value for s in series] == [2, 3, 4, 5, 5] assert series[0].timestamp == START assert series[3].timestamp == START + 900_000 @@ -70,7 +70,7 @@ def test_query_range_unaligned_samples(self, client: MetricsQLiteClient): result = client.query_range(query="metric", start=START, end=START + 3_600_000, step="5m") assert isinstance(result, MatrixResult) - labels, series = result.series[0] + _labels, series = result.series[0] assert [s.value for s in series] == [1, 2, 3, 4] assert series[0].timestamp == START assert series[3].timestamp == START + 900_000 @@ -80,7 +80,7 @@ def test_only_latest_sample_in_result(self, client: MetricsQLiteClient): client.insert_gauge("metric", minute, START + 60_000 * minute) result = client.query_range(query="metric", start=START, end=START + 3_600_000, step="10m") - labels, series = result.series[0] + _labels, series = result.series[0] assert len(series) == 7 assert series[0].timestamp == START assert series[6].timestamp == START + 3_600_000 diff --git a/tests/queries/test_selectors.py b/tests/queries/test_selectors.py index b7daec3..cdff5f4 100644 --- a/tests/queries/test_selectors.py +++ b/tests/queries/test_selectors.py @@ -28,8 +28,8 @@ def test_selector(self, client: MetricsQLiteClient): result = client.query(query="metric", time=EVAL_TIME) assert isinstance(result, InstantVector) assert len(result.series) == 1 - labels, sample = result.series[0] - assert labels == {"__name__": "metric"} + _labels, sample = result.series[0] + assert _labels == {"__name__": "metric"} assert isinstance(sample, Sample) assert sample.timestamp == EVAL_TIME assert sample.value == value @@ -53,7 +53,7 @@ def test_selector_timestamp(self, client: MetricsQLiteClient): client.insert_gauge("metric", 42, EVAL_TIME - 1_000) result = client.query(query="metric", time=EVAL_TIME) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 1_000 def test_selector_correct_sample(self, client: MetricsQLiteClient): @@ -61,17 +61,17 @@ def test_selector_correct_sample(self, client: MetricsQLiteClient): client.insert_gauge("metric", 1, EVAL_TIME - 2_000) client.insert_gauge("metric", 2, EVAL_TIME - 1_000) result = client.query(query="metric", time=EVAL_TIME) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.value == 2 client.insert_gauge("metric", 3, EVAL_TIME) result = client.query(query="metric", time=EVAL_TIME) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.value == 3 client.insert_gauge("metric", 4, EVAL_TIME + 1) result = client.query(query="metric", time=EVAL_TIME) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.value == 3 def test_query_without_time(self, client: MetricsQLiteClient): @@ -81,7 +81,7 @@ def test_query_without_time(self, client: MetricsQLiteClient): client.insert_gauge("metric", 42, insertion_time) result = client.query(query="metric") - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == insertion_time def test_selector_on_compacted_gauge(self, client: MetricsQLiteClient): @@ -101,19 +101,19 @@ def test_selector_on_compacted_gauge(self, client: MetricsQLiteClient): # Should return sample from bucket [t-10:t] result = client.query(query="metric", time=EVAL_TIME - 1_000) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 1_000 assert sample.value == 3.5 # Exactly on bucket boundary. Should still return sample from bucket [t-10:t] # result = client.query(query="metric", time=EVAL_TIME) - # labels, sample = result.series[0] + # _labels, sample = result.series[0] # assert sample.timestamp == EVAL_TIME # assert sample.value == 3.5 # Should return sample from bucket [t:t+10] result = client.query(query="metric", time=EVAL_TIME + 1_000) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME + 1_000 assert sample.value == 5.5 @@ -124,7 +124,7 @@ def test_counters(self, client: MetricsQLiteClient): client.insert_counter("counter", 42, EVAL_TIME - 15_000) result = client.query("counter", time=EVAL_TIME) assert isinstance(result, InstantVector) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 15_000 assert sample.value == 42 @@ -132,7 +132,7 @@ def test_counters(self, client: MetricsQLiteClient): client.insert_counter("counter", 42, EVAL_TIME - 5_000) result = client.query("counter", time=EVAL_TIME) assert isinstance(result, InstantVector) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 5_000 assert sample.value == 42 assert sample.sample_count == 2 @@ -141,7 +141,7 @@ def test_counters(self, client: MetricsQLiteClient): client.insert_counter("counter", 42, EVAL_TIME + 5_000) result = client.query("counter", time=EVAL_TIME) assert isinstance(result, InstantVector) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME assert sample.value == 42 assert sample.sample_count == 3 @@ -149,7 +149,7 @@ def test_counters(self, client: MetricsQLiteClient): client.insert_counter("counter", 67, EVAL_TIME + 15_000) result = client.query("counter", time=EVAL_TIME + 20_000) assert isinstance(result, InstantVector) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME + 15_000 assert sample.value == 67 @@ -165,7 +165,7 @@ def test_stale_counters(self, client: MetricsQLiteClient): result = client.query("counter", time=EVAL_TIME, step="5m") assert isinstance(result, InstantVector) assert len(result.series) == 1 - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 240_000 assert sample.value == 42 @@ -178,7 +178,7 @@ def test_sample_completely_spans_lookback_window(self, client: MetricsQLiteClien result = client.query("counter", time=EVAL_TIME, step="5m") assert isinstance(result, InstantVector) assert len(result.series) == 1 - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.value == 42 # Timestamp should be clamped to eval_time since sample_end > eval_time assert sample.timestamp == EVAL_TIME @@ -201,7 +201,7 @@ def test_selector_compacted_gauge_spans_lookback_window(self, client: MetricsQLi result = client.query("gauge", time=EVAL_TIME - 60_000, step="5m") assert isinstance(result, InstantVector) - labels, sample = result.series[0] + _labels, sample = result.series[0] assert sample.timestamp == EVAL_TIME - 60_000 # clamped to query eval time (T-1m) assert sample.value == 42 @@ -219,7 +219,7 @@ def test_range_selector(self, client: MetricsQLiteClient): result = client.query(query="metric[10s]", time=EVAL_TIME) assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 7_000 assert samples[0].value == 2 @@ -237,7 +237,7 @@ def test_range_selector_counter_fully_within_range(self, client: MetricsQLiteCli assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 240_000 # start assert samples[1].timestamp == EVAL_TIME - 120_000 # end @@ -255,7 +255,7 @@ def test_range_selector_counter_start_before_range(self, client: MetricsQLiteCli assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 300_000 # clamped to query_start (T-5m) assert samples[1].timestamp == EVAL_TIME - 120_000 # actual end (T-2m) @@ -273,7 +273,7 @@ def test_range_selector_counter_end_after_range(self, client: MetricsQLiteClient assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 240_000 # actual start (T-4m) assert samples[1].timestamp == EVAL_TIME # clamped to query_end (T) @@ -291,7 +291,7 @@ def test_range_selector_counter_spans_entire_range(self, client: MetricsQLiteCli assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 300_000 # clamped to query_start (T-5m) assert samples[1].timestamp == EVAL_TIME # clamped to query_end (T) @@ -307,7 +307,7 @@ def test_range_selector_singular_counter(self, client: MetricsQLiteClient): assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 1 assert samples[0].timestamp == EVAL_TIME - 120_000 assert samples[0].value == 42 @@ -326,7 +326,7 @@ def test_range_selector_compacted_gauge_fully_within_range(self, client: Metrics assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 240_000 # bucket start assert samples[1].timestamp == EVAL_TIME - 120_000 # bucket end (start + 2m) @@ -347,7 +347,7 @@ def test_range_selector_compacted_gauge_start_before_range(self, client: Metrics assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] # First sample should be clamped to query_start (T-5m) assert samples[0].timestamp == EVAL_TIME - 300_000 # clamped to query_start assert samples[1].timestamp == EVAL_TIME - 240_000 # actual bucket end @@ -371,7 +371,7 @@ def test_range_selector_compacted_gauge_spans_entire_range(self, client: Metrics assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 2 assert samples[0].timestamp == EVAL_TIME - 360_000 # clamped to query_start (T-6m) assert samples[0].value == 42 @@ -386,7 +386,7 @@ def test_range_selector_singular_gauge(self, client: MetricsQLiteClient): assert isinstance(result, RangeVectorResult) assert len(result.series) == 1 - labels, samples = result.series[0] + _labels, samples = result.series[0] assert len(samples) == 1 assert samples[0].timestamp == EVAL_TIME - 120_000 assert samples[0].value == 42 From 28a9b45a1a7799719c678dc498030a9c33fafd14 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 26 Apr 2026 15:23:44 +0200 Subject: [PATCH 9/9] update github runners to v6 --- .github/workflows/test.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da9948b..b231524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,10 +10,10 @@ jobs: ruff: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.14" @@ -25,7 +25,7 @@ jobs: set +e output=$(ruff check . --statistics 2>&1) exit_code=$? - echo "## 📋 Ruff Lint Report" >> $GITHUB_STEP_SUMMARY + echo "## Ruff Lint Report" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$output" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY @@ -36,7 +36,7 @@ jobs: set +e output=$(ruff format --check . 2>&1) exit_code=$? - echo "## 🎨 Ruff Format Report" >> $GITHUB_STEP_SUMMARY + echo "## Ruff Format Report" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$output" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY @@ -45,10 +45,10 @@ jobs: mypy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.14" @@ -56,13 +56,14 @@ jobs: run: | python -m pip install --upgrade pip pip install -e ".[dev]" + pip install -e ".[fastapi]" - name: mypy run: | set +e output=$(mypy metricsqlite 2>&1) exit_code=$? - echo "## 🔍 mypy Report" >> $GITHUB_STEP_SUMMARY + echo "## mypy Report" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$output" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY @@ -75,10 +76,10 @@ jobs: python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }}