prombed 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ .pytest_cache/
3
+ __pycache__/
4
+ *.py[cod]
5
+ dist/
6
+ build/
7
+ *.egg-info/
prombed-0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ Copyright 2026 Prombed Authors
8
+
9
+ Licensed under the Apache License, Version 2.0 (the "License");
10
+ you may not use this file except in compliance with the License.
11
+ You may obtain a copy of the License at
12
+
13
+ http://www.apache.org/licenses/LICENSE-2.0
14
+
15
+ Unless required by applicable law or agreed to in writing, software
16
+ distributed under the License is distributed on an "AS IS" BASIS,
17
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18
+ See the License for the specific language governing permissions and
19
+ limitations under the License.
prombed-0.1.0/Makefile ADDED
@@ -0,0 +1,15 @@
1
+ .PHONY: fix lint test build
2
+
3
+ fix:
4
+ uv run ruff format .
5
+ uv run ruff check --fix .
6
+
7
+ lint:
8
+ uv run ruff format --check .
9
+ uv run ruff check .
10
+
11
+ test:
12
+ uv run pytest -q
13
+
14
+ build:
15
+ uv build
prombed-0.1.0/NOTICE ADDED
@@ -0,0 +1,8 @@
1
+ Prombed
2
+ Copyright 2026 Prombed Authors
3
+
4
+ Prombed includes algorithms adapted from Prometheus, which is licensed under
5
+ the Apache License 2.0. Adapted behavior includes rate extrapolation, classic
6
+ histogram quantiles, scrape label mutation and staleness, and evaluator sample
7
+ accounting. Corresponding source files retain Prometheus Authors copyright
8
+ notices.
prombed-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.5
2
+ Name: prombed
3
+ Version: 0.1.0
4
+ Summary: A small, embeddable Prometheus subset for short-lived metric analysis.
5
+ License: Apache-2.0
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Requires-Python: >=3.10
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest-asyncio<2.0,>=0.23; extra == 'dev'
11
+ Requires-Dist: pytest<10.0,>=8.0; extra == 'dev'
12
+ Requires-Dist: ruff<1.0,>=0.12; extra == 'dev'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Prombed Python
16
+
17
+ The Python implementation provides zero-runtime-dependency Prometheus text exposition parsing,
18
+ bounded in-memory storage, static-target scraping, and PromQL queries. It requires Python 3.10+.
19
+
20
+ ## Development
21
+
22
+ ```bash
23
+ cd <repo_root>/python
24
+ uv sync --extra dev
25
+ uv run pytest -q
26
+ uv run ruff format .
27
+ uv run ruff check .
28
+ ```
29
+
30
+ ## API
31
+
32
+ ```python
33
+ from prombed import Prombed, ScrapeTarget
34
+
35
+ metrics = Prombed(
36
+ targets=[
37
+ ScrapeTarget(
38
+ "http://example:8080/metrics",
39
+ labels={"service": "example"},
40
+ metric_names=("example_requests_total", "example_duration_seconds"),
41
+ )
42
+ ],
43
+ retention_ms=10 * 60_000,
44
+ scrape_interval_ms=5_000,
45
+ )
46
+
47
+ await metrics.start()
48
+ success_rate = metrics.query(
49
+ 'sum(rate(example_requests_total{status="success"}[1m])) '
50
+ "/ sum(rate(example_requests_total[1m]))"
51
+ )
52
+ latency = metrics.query(
53
+ "histogram_quantile(0.95, sum by (le) (rate(example_duration_seconds_bucket[1m])))"
54
+ )
55
+ metrics.stop()
56
+ ```
57
+
58
+ - `await scrape_once()` scrapes configured targets immediately.
59
+ - `await start()` performs an initial scrape and starts periodic collection; `stop()` stops it.
60
+ - `ingest()` accepts a snapshot when the caller owns the sampling lifecycle.
61
+ - `targets()` returns target health and the latest scrape error.
62
+ - `query()` and `query_range()` return Prometheus HTTP API-style dictionaries.
63
+
64
+ Storage, queries, and external requests have explicit capacity and timeout limits. See the
65
+ [PromQL compatibility contract](../docs/promql-compatibility.md) for supported behavior and
66
+ deliberate omissions.
@@ -0,0 +1,52 @@
1
+ # Prombed Python
2
+
3
+ The Python implementation provides zero-runtime-dependency Prometheus text exposition parsing,
4
+ bounded in-memory storage, static-target scraping, and PromQL queries. It requires Python 3.10+.
5
+
6
+ ## Development
7
+
8
+ ```bash
9
+ cd <repo_root>/python
10
+ uv sync --extra dev
11
+ uv run pytest -q
12
+ uv run ruff format .
13
+ uv run ruff check .
14
+ ```
15
+
16
+ ## API
17
+
18
+ ```python
19
+ from prombed import Prombed, ScrapeTarget
20
+
21
+ metrics = Prombed(
22
+ targets=[
23
+ ScrapeTarget(
24
+ "http://example:8080/metrics",
25
+ labels={"service": "example"},
26
+ metric_names=("example_requests_total", "example_duration_seconds"),
27
+ )
28
+ ],
29
+ retention_ms=10 * 60_000,
30
+ scrape_interval_ms=5_000,
31
+ )
32
+
33
+ await metrics.start()
34
+ success_rate = metrics.query(
35
+ 'sum(rate(example_requests_total{status="success"}[1m])) '
36
+ "/ sum(rate(example_requests_total[1m]))"
37
+ )
38
+ latency = metrics.query(
39
+ "histogram_quantile(0.95, sum by (le) (rate(example_duration_seconds_bucket[1m])))"
40
+ )
41
+ metrics.stop()
42
+ ```
43
+
44
+ - `await scrape_once()` scrapes configured targets immediately.
45
+ - `await start()` performs an initial scrape and starts periodic collection; `stop()` stops it.
46
+ - `ingest()` accepts a snapshot when the caller owns the sampling lifecycle.
47
+ - `targets()` returns target health and the latest scrape error.
48
+ - `query()` and `query_range()` return Prometheus HTTP API-style dictionaries.
49
+
50
+ Storage, queries, and external requests have explicit capacity and timeout limits. See the
51
+ [PromQL compatibility contract](../docs/promql-compatibility.md) for supported behavior and
52
+ deliberate omissions.
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "prombed"
3
+ version = "0.1.0"
4
+ description = "A small, embeddable Prometheus subset for short-lived metric analysis."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "Apache-2.0" }
8
+ dependencies = []
9
+
10
+ [project.optional-dependencies]
11
+ dev = [
12
+ "pytest>=8.0,<10.0",
13
+ "pytest-asyncio>=0.23,<2.0",
14
+ "ruff>=0.12,<1.0",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/prombed"]
23
+
24
+ [tool.pytest.ini_options]
25
+ asyncio_mode = "auto"
26
+ testpaths = ["tests"]
27
+
28
+ [tool.ruff]
29
+ line-length = 100
30
+ target-version = "py310"
31
+
32
+ [tool.ruff.lint]
33
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
@@ -0,0 +1,50 @@
1
+ from .errors import PrombedError as PrombedError
2
+ from .errors import PrombedErrorCode as PrombedErrorCode
3
+ from .exposition import parse_exposition as parse_exposition
4
+ from .prombed import Fetch as Fetch
5
+ from .prombed import Prombed as Prombed
6
+ from .promql import PromQLEngine as PromQLEngine
7
+ from .promql import parse_duration as parse_duration
8
+ from .promql import parse_promql as parse_promql
9
+ from .storage import AppendResult as AppendResult
10
+ from .storage import InMemoryStorage as InMemoryStorage
11
+ from .storage import StorageAppender as StorageAppender
12
+ from .storage import StoredSeries as StoredSeries
13
+ from .storage import label_key as label_key
14
+ from .types import (
15
+ ExpositionSample as ExpositionSample,
16
+ )
17
+ from .types import (
18
+ LabelMatcher as LabelMatcher,
19
+ )
20
+ from .types import (
21
+ LabelMatcherOperator as LabelMatcherOperator,
22
+ )
23
+ from .types import (
24
+ LabelSet as LabelSet,
25
+ )
26
+ from .types import (
27
+ MetricMetadata as MetricMetadata,
28
+ )
29
+ from .types import (
30
+ MetricType as MetricType,
31
+ )
32
+ from .types import (
33
+ ParsedExposition as ParsedExposition,
34
+ )
35
+ from .types import (
36
+ Sample as Sample,
37
+ )
38
+ from .types import (
39
+ ScrapeResult as ScrapeResult,
40
+ )
41
+ from .types import (
42
+ ScrapeTarget as ScrapeTarget,
43
+ )
44
+ from .types import (
45
+ ScrapeTargetHealth as ScrapeTargetHealth,
46
+ )
47
+ from .types import (
48
+ ScrapeTargetStatus as ScrapeTargetStatus,
49
+ )
50
+ from .value import STALE_NAN_BITS as STALE_NAN_BITS
@@ -0,0 +1,20 @@
1
+ from typing import Literal
2
+
3
+ PrombedErrorCode = Literal[
4
+ "invalid_exposition",
5
+ "invalid_query",
6
+ "unsupported_promql_feature",
7
+ "query_execution_error",
8
+ "out_of_order_sample",
9
+ "duplicate_sample",
10
+ "storage_capacity_exceeded",
11
+ "scrape_failed",
12
+ ]
13
+
14
+
15
+ class PrombedError(Exception):
16
+ """A stable, machine-readable failure from Prombed."""
17
+
18
+ def __init__(self, code: PrombedErrorCode, message: str) -> None:
19
+ super().__init__(message)
20
+ self.code = code
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ import re
5
+
6
+ from .errors import PrombedError
7
+ from .types import ExpositionSample, MetricMetadata, ParsedExposition
8
+
9
+ _METRIC_NAME = re.compile(r"^[A-Za-z_:][A-Za-z0-9_:]*$")
10
+ _LABEL_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
11
+ _FLOAT = re.compile(r"^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$")
12
+ _METRIC_TYPES: set[str] = {
13
+ "counter",
14
+ "gauge",
15
+ "histogram",
16
+ "summary",
17
+ "untyped",
18
+ "info",
19
+ "stateset",
20
+ "gaugehistogram",
21
+ "unknown",
22
+ }
23
+
24
+
25
+ def _invalid(line_number: int, message: str) -> None:
26
+ raise PrombedError(
27
+ "invalid_exposition",
28
+ f"invalid Prometheus exposition at line {line_number}: {message}",
29
+ )
30
+
31
+
32
+ def _skip_whitespace(value: str, start: int) -> int:
33
+ index = start
34
+ while index < len(value) and value[index].isspace():
35
+ index += 1
36
+ return index
37
+
38
+
39
+ def _parse_escaped_string(value: str, start: int, line_number: int) -> tuple[str, int]:
40
+ if start >= len(value) or value[start] != '"':
41
+ _invalid(line_number, "expected quoted label value")
42
+ output: list[str] = []
43
+ index = start + 1
44
+ while index < len(value):
45
+ char = value[index]
46
+ if char == '"':
47
+ return "".join(output), index + 1
48
+ if char != "\\":
49
+ output.append(char)
50
+ index += 1
51
+ continue
52
+ index += 1
53
+ if index >= len(value):
54
+ _invalid(line_number, "unterminated quoted label value")
55
+ escaped = value[index]
56
+ replacement = {"\\": "\\", '"': '"', "n": "\n"}.get(escaped)
57
+ if replacement is None:
58
+ _invalid(line_number, f"unsupported escape sequence \\{escaped}")
59
+ output.append(replacement)
60
+ index += 1
61
+ _invalid(line_number, "unterminated quoted label value")
62
+ raise AssertionError("unreachable")
63
+
64
+
65
+ def _parse_labels(value: str, start: int, line_number: int) -> tuple[dict[str, str], int]:
66
+ labels: dict[str, str] = {}
67
+ index = start + 1
68
+ while True:
69
+ index = _skip_whitespace(value, index)
70
+ if index < len(value) and value[index] == "}":
71
+ return labels, index + 1
72
+ name_start = index
73
+ while index < len(value) and (value[index].isalnum() or value[index] == "_"):
74
+ index += 1
75
+ name = value[name_start:index]
76
+ if not _LABEL_NAME.fullmatch(name):
77
+ _invalid(line_number, f"invalid label name {name!r}")
78
+ if name in labels:
79
+ _invalid(line_number, f"duplicate label {name!r}")
80
+ index = _skip_whitespace(value, index)
81
+ if index >= len(value) or value[index] != "=":
82
+ _invalid(line_number, f"expected '=' after label {name!r}")
83
+ index = _skip_whitespace(value, index + 1)
84
+ parsed, index = _parse_escaped_string(value, index, line_number)
85
+ labels[name] = parsed
86
+ index = _skip_whitespace(value, index)
87
+ if index < len(value) and value[index] == ",":
88
+ index += 1
89
+ continue
90
+ if index < len(value) and value[index] == "}":
91
+ return labels, index + 1
92
+ _invalid(line_number, "expected ',' or '}' after label value")
93
+
94
+
95
+ def _parse_float(token: str, line_number: int) -> float:
96
+ if token == "NaN":
97
+ return math.nan
98
+ if token in {"+Inf", "Inf"}:
99
+ return math.inf
100
+ if token == "-Inf":
101
+ return -math.inf
102
+ if not _FLOAT.fullmatch(token):
103
+ _invalid(line_number, f"invalid sample value {token!r}")
104
+ return float(token)
105
+
106
+
107
+ def _parse_sample_line(line: str, line_number: int) -> ExpositionSample:
108
+ index = 0
109
+ while index < len(line) and not line[index].isspace() and line[index] != "{":
110
+ index += 1
111
+ name = line[:index]
112
+ if not _METRIC_NAME.fullmatch(name):
113
+ _invalid(line_number, f"invalid metric name {name!r}")
114
+ labels: dict[str, str] = {}
115
+ if index < len(line) and line[index] == "{":
116
+ labels, index = _parse_labels(line, index, line_number)
117
+ index = _skip_whitespace(line, index)
118
+ value_start = index
119
+ while index < len(line) and not line[index].isspace():
120
+ index += 1
121
+ sample_value = _parse_float(line[value_start:index], line_number)
122
+ index = _skip_whitespace(line, index)
123
+ timestamp: int | None = None
124
+ if index < len(line) and line[index] != "#":
125
+ timestamp_start = index
126
+ while index < len(line) and not line[index].isspace():
127
+ index += 1
128
+ token = line[timestamp_start:index]
129
+ if not re.fullmatch(r"-?\d+", token):
130
+ _invalid(line_number, f"invalid sample timestamp {token!r}")
131
+ timestamp = int(token)
132
+ if abs(timestamp) > 2**53 - 1:
133
+ _invalid(line_number, f"sample timestamp is outside the safe integer range: {token}")
134
+ index = _skip_whitespace(line, index)
135
+ if index < len(line) and line[index] != "#":
136
+ _invalid(line_number, f"unexpected trailing content {line[index:]!r}")
137
+ return ExpositionSample(name, labels, sample_value, timestamp)
138
+
139
+
140
+ def _unescape_help(value: str, line_number: int) -> str:
141
+ output: list[str] = []
142
+ index = 0
143
+ while index < len(value):
144
+ char = value[index]
145
+ if char != "\\":
146
+ output.append(char)
147
+ index += 1
148
+ continue
149
+ index += 1
150
+ if index >= len(value):
151
+ _invalid(line_number, "unsupported HELP escape sequence \\")
152
+ escaped = value[index]
153
+ if escaped == "\\":
154
+ output.append("\\")
155
+ elif escaped == "n":
156
+ output.append("\n")
157
+ else:
158
+ _invalid(line_number, f"unsupported HELP escape sequence \\{escaped}")
159
+ index += 1
160
+ return "".join(output)
161
+
162
+
163
+ def parse_exposition(value: str) -> ParsedExposition:
164
+ """Parse Prometheus text format 0.0.4 and its classic OpenMetrics subset."""
165
+
166
+ samples: list[ExpositionSample] = []
167
+ metadata: dict[str, MetricMetadata] = {}
168
+ descriptor_pattern = re.compile(r"^#\s*(HELP|TYPE|UNIT)\s+([^\s]+)\s+(.+)$")
169
+ for line_number, raw in enumerate(value.splitlines(), 1):
170
+ line = raw.strip()
171
+ if not line or line == "# EOF":
172
+ continue
173
+ if not line.startswith("#"):
174
+ samples.append(_parse_sample_line(line, line_number))
175
+ continue
176
+ match = descriptor_pattern.fullmatch(line)
177
+ if match is None:
178
+ continue
179
+ kind, name, raw_value = match.groups()
180
+ if not _METRIC_NAME.fullmatch(name):
181
+ _invalid(line_number, f"invalid metadata metric name {name!r}")
182
+ entry = metadata.setdefault(name, MetricMetadata())
183
+ if kind == "TYPE":
184
+ metric_type = raw_value.lower()
185
+ if metric_type not in _METRIC_TYPES:
186
+ _invalid(line_number, f"unsupported metric type {raw_value!r}")
187
+ entry.type = metric_type # type: ignore[assignment]
188
+ elif kind == "HELP":
189
+ entry.help = _unescape_help(raw_value, line_number)
190
+ else:
191
+ entry.unit = raw_value
192
+ return ParsedExposition(samples, metadata)