No project description provided
Project description
promcraft
A Python library for building Prometheus QL queries programmatically. Instead of constructing raw query strings, use composable Python objects that serialize to valid PromQL syntax.
Installation
pip install promcraft
# or with Poetry
poetry add promcraft
Requires Python 3.10+.
Quick Start
from promcraft import InstantVector, RangeVector, Label, Duration
# Simple metric selector
query = InstantVector("up", [])
str(query) # "up{}"
# With label filters
query = InstantVector("http_requests_total", [
Label.eq("job", "api-server"),
Label.eq("env", "production"),
])
str(query) # 'http_requests_total{ job = "api-server", env = "production" }'
# Range vector with rate()
from promcraft import rate
query = rate(RangeVector("http_requests_total", [
Label.eq("job", "api-server"),
], Duration(m=5)))
str(query) # 'rate(http_requests_total{ job = "api-server" }[5m])'
# Binary operation
left = InstantVector("http_requests_total", [Label.eq("status", "500")])
right = InstantVector("http_requests_total", [])
ratio = left / right
str(ratio) # 'http_requests_total{ status = "500" } / http_requests_total{}'
Raw values instead of wrapper types
Anywhere a String is expected (label values, count_values, label-manipulation function
arguments), you can pass a plain str instead — it's converted with String.from_value()
under the hood. Anywhere a Scalar is expected (Float-typed function/aggregation
parameters, offset, at), you can pass a plain float/int instead, converted with
Float.from_value(). Passing an existing String/Scalar instance through is a no-op, so
both styles compose freely:
Label.eq("job", "api-server") # same as Label.eq("job", String("api-server"))
topk(5, v) # same as topk(Float(5), v)
API Reference
Scalars
Float
Wraps a Python float for use in queries.
from promcraft import Float
Float(3.14) # "3.14"
Float(0.0) # "0.0"
Float(-2.5) # "-2.5"
Float.from_value(3) # Float(3.0) -- "3.0"
Float.from_value(Float(3.14)) # passthrough, still "3.14"
Hex
Represents a hexadecimal integer literal.
from promcraft import Hex
Hex(255) # "0xff"
Hex(0) # "0x0"
Hex(16) # "0x10"
Duration
Represents a Prometheus duration literal. All parameters are keyword-only.
from promcraft import Duration
Duration() # "0s" (default when no units given)
Duration(s=5) # "5s"
Duration(m=1, s=30) # "1m30s"
Duration(h=1, m=30) # "1h30m"
Duration(ms=500) # "500ms"
Duration(y=1, w=2, d=3, h=4, m=5, s=6, ms=7) # "1y2w3d4h5m6s7ms"
Duration(m=5, neg=True) # "-5m"
| Parameter | Unit |
|---|---|
y |
years |
w |
weeks |
d |
days |
h |
hours |
m |
minutes |
s |
seconds |
ms |
milliseconds |
neg |
negate the duration |
Every Scalar subclass (Float, Hex, Duration) also implements a from_value()
classmethod used throughout the library to accept raw numbers wherever a scalar is expected.
Passing an existing Scalar through from_value() returns it unchanged.
String
Represents a PromQL string literal with configurable quoting.
from promcraft import String
String("hello") # '"hello"' (double-quote default)
String("hello", '"') # '"hello"'
String("hello", "'") # "'hello'"
String("hello", "`") # "`hello`"
String.from_value("hello") # same as String("hello")
String.from_value(String("hello", "'")) # passthrough, still "'hello'"
Special characters are escaped automatically for " and ' quotes. Backtick strings are not escaped.
Variable
Represents a Grafana dashboard template variable ($name, or ${name} when braces are needed). Grafana interpolates the variable's selected value before the query text reaches Prometheus, so a Variable is accepted anywhere a query fragment, scalar, or string is expected — metric names, label names/values, offset/at/range/resolution, binary-operator operands, on/ignoring/group_left/group_right/by/without label lists, and aggregation/function arguments.
from promcraft import Variable
Variable("job") # "$job"
Variable("job", braces=True) # "${job}"
from promcraft import InstantVector, Label, sum_, topk
# dynamic metric name and label value
InstantVector(Variable("metric_name"), [Label.eq("env", Variable("environment"))])
# '$metric_name{ env = $environment }'
# dynamic offset and range
InstantVector("up", [], offset=Variable("offset_var"))[Variable("range_var")]
# 'up{}[$range_var] offset $offset_var'
# dynamic grouping labels
sum_(InstantVector("requests", [])).by([Variable("groupby")])
# 'sum(requests{}) by ($groupby)'
# dynamic aggregation parameter
topk(Variable("k"), InstantVector("requests", []))
# 'topk($k, requests{})'
A Variable is never coerced into a literal — Float.from_value(), Hex.from_value(), Duration.from_value(), and String.from_value() all pass an existing Variable through unchanged, the same way they already do for Scalar/String instances.
Label
Represents a label matcher used inside vector selectors. Use the factory methods for convenience. The value accepts either a raw str or a String instance.
from promcraft import Label, String
Label.eq("job", "prometheus") # 'job = "prometheus"'
Label.neq("env", "prod") # 'env != "prod"'
Label.re("name", "prom.*") # 'name =~ "prom.*"'
Label.nre("name", String("test.*")) # 'name !~ "test.*"'
| Method | Operator | Meaning |
|---|---|---|
Label.eq |
= |
Exact match |
Label.neq |
!= |
Not equal |
Label.re |
=~ |
Regex match |
Label.nre |
!~ |
Regex does not match |
InstantVector
Selects a set of time series at a single point in time.
InstantVector(metric, labels, *, offset=None, at=None)
from promcraft import InstantVector, Label, String, Duration, Float
# Basic
InstantVector("up", [])
# "up{}"
# With labels
InstantVector("http_requests_total", [
Label.eq("job", "api"),
Label.re("status", "5.."),
])
# 'http_requests_total{ job = "api", status =~ "5.." }'
# With offset
InstantVector("up", [], offset=Duration(m=5))
# "up{} offset 5m"
# With @ modifier (Unix timestamp)
InstantVector("up", [], at=1609746000.0)
# "up{} @ 1609746000.0"
# With @ modifier (range function references)
InstantVector("up", [], at="start()")
# "up{} @ start()"
InstantVector("up", [], at="end()")
# "up{} @ end()"
RangeVector
Selects a range of samples over a time window. Required by functions like rate(), increase(), avg_over_time().
RangeVector(metric, labels, range, resolution=None, *, offset=None, at=None)
from promcraft import RangeVector, Label, String, Duration
# Basic range
RangeVector("http_requests_total", [], Duration(m=5))
# "http_requests_total{}[5m]"
# With resolution (subquery step)
RangeVector("up", [], Duration(m=5), resolution=Duration(s=30))
# "up{}[5m :30s]"
# With offset
RangeVector("up", [], Duration(m=5), offset=Duration(m=1))
# "up{}[5m] offset 1m"
# Combined
RangeVector(
"http_requests_total",
[Label.eq("job", "api")],
Duration(m=5),
resolution=Duration(s=30),
offset=Duration(m=1),
at="end()",
)
# 'http_requests_total{ job = "api" }[5m :30s] offset 1m @ end()'
InstantVector also supports subscript syntax as a shortcut for building a RangeVector that reuses its metric, labels, offset and @ modifier:
InstantVector("up", [])[Duration(m=5)] # equivalent to RangeVector("up", [], Duration(m=5))
InstantVector("up", [])[Duration(m=5), Duration(s=30)] # range + resolution
Binary operators
Combine two query expressions with an arithmetic, comparison, logical/set, or trigonometric operator. Each operator has a module-level helper function, and this is the primary way to build binary expressions:
from promcraft import (
add, sub, mul, div, mod, pow,
eq, neq, lt, lte, gt, gte,
and_, or_, unless, atan2,
Float, InstantVector,
)
v1 = InstantVector("requests", [])
v2 = InstantVector("errors", [])
add(Float(1.0), Float(2.0)) # "1.0 + 2.0"
div(v2, v1) # "errors{} / requests{}"
gt(v1, Float(0.0)) # "requests{} > 0.0"
and_(v1, v2) # "requests{} and errors{}"
| Helper | Symbol | Category |
|---|---|---|
add |
+ |
Arithmetic |
sub |
- |
Arithmetic |
mul |
* |
Arithmetic |
div |
/ |
Arithmetic |
mod |
% |
Arithmetic |
pow |
^ |
Arithmetic |
eq |
== |
Comparison |
neq |
!= |
Comparison |
lt |
< |
Comparison |
lte |
<= |
Comparison |
gt |
> |
Comparison |
gte |
>= |
Comparison |
and_ |
and |
Logical/Set |
or_ |
or |
Logical/Set |
unless |
unless |
Logical/Set |
atan2 |
atan2 |
Trigonometric |
Python's own operators also work directly on any Query (+ - * / % **):
requests + errors # same as add(requests, errors)
Nested expressions
Nested binary expressions are automatically parenthesized to preserve evaluation order:
inner = add(Float(1.0), Float(2.0))
outer = mul(inner, Float(3.0))
str(outer) # "(1.0 + 2.0) * 3.0"
Label matching
Use .on() or .ignoring() to control which labels are used for matching when combining two vector selectors:
requests = InstantVector("http_requests_total", [])
errors = InstantVector("http_errors_total", [])
# Match on specific labels
expr = div(errors, requests).on(["job", "env"])
# 'http_errors_total{} / on(job, env) http_requests_total{}'
# Ignore specific labels
expr = div(errors, requests).ignoring(["instance"])
# 'http_errors_total{} / ignoring(instance) http_requests_total{}'
Grouping
Use .group_left() or .group_right() for many-to-one or one-to-many matching:
# group_left: result has labels from the left side
expr = mul(requests, errors).on(["job"]).group_left(["env"])
# '... on(job) group_left(env) ...'
# group_right: result has labels from the right side
expr = mul(requests, errors).group_right([])
.on(), .ignoring(), .group_left(), and .group_right() are all chainable and each returns a new immutable instance, so they can be combined freely:
mul(requests, errors).ignoring(["env"]).group_right(["env"])
Aggregation operators
Apply a PromQL aggregation operator to a vector via a module-level helper function. Helpers for operators that require a parameter (topk, bottomk, quantile, limitk, limit_ratio, count_values) take it as their first argument.
No-parameter aggregations — sum_, avg, min_, max_, count, group, stddev, stdvar:
sum,min, andmaxare namedsum_,min_,max_in this library so they don't shadow the Python builtins.
from promcraft import sum_, avg, min_, max_, count, group, stddev, stdvar, InstantVector
v = InstantVector("http_requests_total", [])
sum_(v) # "sum(http_requests_total{})"
avg(v).by(["job"]) # "avg(http_requests_total{}) by (job)"
stddev(v).without(["env"])
# "stddev(http_requests_total{}) without (env)"
Use .by(labels) to keep only the listed labels in the grouped result, or .without(labels) to drop the listed labels and keep everything else. Both are chainable and return a new instance.
labels must be non-empty — .by([])/.without([]) raise ValueError. To aggregate away
all labels, omit .by()/.without() entirely rather than passing an empty list.
Scalar-parameter aggregations — topk, bottomk, quantile, limitk, limit_ratio. The parameter accepts a raw float/int or a Float:
from promcraft import topk, bottomk, quantile, limitk, limit_ratio
topk(5, v) # "topk(5.0, http_requests_total{})"
bottomk(3, v) # "bottomk(3.0, http_requests_total{})"
quantile(0.95, v) # "quantile(0.95, http_requests_total{})"
limitk(10, v) # "limitk(10.0, http_requests_total{})"
limit_ratio(0.1, v) # "limit_ratio(0.1, http_requests_total{})"
topk(5, v).by(["job"])
# "topk(5.0, http_requests_total{}) by (job)"
String-parameter aggregation — count_values (label name, accepts a raw str or a String):
from promcraft import count_values
count_values("version", v)
# 'count_values("version", http_requests_total{})'
Functions
Every Prometheus query function has a typed module-level helper. Scalar and string arguments accept raw float/int/str values interchangeably with Float/String.
Rate / counter functions
from promcraft import RangeVector, Duration, rate, irate, increase, delta, deriv, resets
rv = RangeVector("http_requests_total", [], Duration(m=5))
rate(rv) # "rate(http_requests_total{}[5m])"
irate(rv) # "irate(http_requests_total{}[5m])"
increase(rv) # "increase(http_requests_total{}[5m])"
delta(rv) # "delta(http_requests_total{}[5m])"
deriv(rv) # "deriv(http_requests_total{}[5m])"
resets(rv) # "resets(http_requests_total{}[5m])"
*_over_time aggregations
from promcraft import (
avg_over_time, min_over_time, max_over_time, sum_over_time,
count_over_time, stddev_over_time, last_over_time,
present_over_time, absent_over_time, quantile_over_time,
)
avg_over_time(rv) # "avg_over_time(http_requests_total{}[5m])"
quantile_over_time(0.95, rv) # "quantile_over_time(0.95, http_requests_total{}[5m])"
Math & rounding
from promcraft import InstantVector, abs, ceil, floor, sqrt, round, clamp, clamp_min, clamp_max
v = InstantVector("cpu_usage", [])
abs(v) # "abs(cpu_usage{})"
ceil(v) # "ceil(cpu_usage{})"
sqrt(v) # "sqrt(cpu_usage{})"
round(v) # "round(cpu_usage{})"
round(v, 0.5) # "round(cpu_usage{}, 0.5)"
clamp(v, 0.0, 1.0) # "clamp(cpu_usage{}, 0.0, 1.0)"
clamp_min(v, 0.0) # "clamp_min(cpu_usage{}, 0.0)"
clamp_max(v, 1.0) # "clamp_max(cpu_usage{}, 1.0)"
Exponential & logarithm
from promcraft import exp, ln, log2, log10
exp(v) # "exp(cpu_usage{})"
ln(v) # "ln(cpu_usage{})"
log2(v) # "log2(cpu_usage{})"
log10(v) # "log10(cpu_usage{})"
Trigonometry
from promcraft import sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, deg, rad, pi, sgn
sin(v) # "sin(cpu_usage{})"
deg(v) # "deg(cpu_usage{})"
rad(v) # "rad(cpu_usage{})"
pi() # "pi()"
sgn(v) # "sgn(cpu_usage{})"
Date / time
All accept an optional instant vector; when omitted they default to vector(time()) in PromQL.
from promcraft import hour, minute, day_of_week, day_of_month, month, year, time
hour() # "hour()"
hour(v) # "hour(cpu_usage{})"
time() # "time()"
Sorting
from promcraft import sort, sort_desc, sort_by_label, sort_by_label_desc
sort(v) # "sort(cpu_usage{})"
sort_desc(v) # "sort_desc(cpu_usage{})"
sort_by_label(v, "job") # 'sort_by_label(cpu_usage{}, "job")'
sort_by_label_desc(v, "job", "env")
# 'sort_by_label_desc(cpu_usage{}, "job", "env")'
Label manipulation
from promcraft import label_join, label_replace
label_join(v, "addr", ":", "host", "port")
# 'label_join(cpu_usage{}, "addr", ":", "host", "port")'
label_replace(v, "job", "${1}", "job", "(.+)")
# 'label_replace(cpu_usage{}, "job", "${1}", "job", "(.+)")'
Type conversion
from promcraft import scalar, vector
scalar(v) # "scalar(cpu_usage{})"
vector(1.0) # "vector(1.0)"
Histogram functions
from promcraft import (
histogram_quantile, histogram_fraction,
histogram_count, histogram_sum, histogram_avg,
histogram_stddev, histogram_stdvar,
)
histogram_quantile(0.95, v) # "histogram_quantile(0.95, cpu_usage{})"
histogram_fraction(0.0, 1.0, v) # "histogram_fraction(0.0, 1.0, cpu_usage{})"
histogram_count(v) # "histogram_count(cpu_usage{})"
Prediction & smoothing
from promcraft import predict_linear, double_exponential_smoothing
predict_linear(rv, 3600.0)
# "predict_linear(http_requests_total{}[5m], 3600.0)"
double_exponential_smoothing(rv, 0.1, 0.5)
# "double_exponential_smoothing(http_requests_total{}[5m], 0.1, 0.5)"
Absence detection
from promcraft import absent, absent_over_time
absent(v) # "absent(cpu_usage{})"
absent_over_time(rv) # "absent_over_time(http_requests_total{}[5m])"
Low-level: Function
All of the helpers above build a Function instance internally. It isn't part of the top-level promcraft public API — import it from its submodule if you need to call a custom or newly-added Prometheus function that doesn't have a helper yet:
from promcraft.functions import Function
from promcraft import InstantVector, Float
Function("my_custom_func", [InstantVector("up", []), Float(42.0)])
# "my_custom_func(up{}, 42.0)"
Composing Complex Queries
Every Query implements to_string() (which __str__ delegates to), so you can compose arbitrarily deep expressions and print them with str():
from promcraft import (
InstantVector, RangeVector,
Label, Duration, rate, div,
)
job_label = Label.eq("job", "api")
# rate of 5xx errors divided by rate of total requests
error_rate = rate(RangeVector("http_requests_total", [
job_label,
Label.re("status", "5.."),
], Duration(m=5)))
total_rate = rate(RangeVector("http_requests_total", [job_label], Duration(m=5)))
ratio = div(error_rate, total_rate).on(["job"])
str(ratio)
# '(rate(http_requests_total{ job = "api", status =~ "5.." }[5m]))
# / on(job) (rate(http_requests_total{ job = "api" }[5m]))'
Pretty printing
Every to_string() accepts an optional indent argument to render multi-line, indented
output instead of the single-line compact form. Pass an int (spaces per level) or a
whitespace string (e.g. "\t"):
from promcraft import InstantVector, Duration, sum_
vec = InstantVector("metric", [])[Duration(m=1)]
expr = sum_(vec).by(["job", "env"])
print(expr.to_string(indent=4))
# sum(
# metric{}[1m]
# ) by (job, env)
Omitting indent (or passing None) keeps the existing single-line output — str(query)
is unaffected either way.
Development
# Install dependencies
poetry install
# Run tests (includes type checking and linting)
poetry run pytest
# Run only unit tests
poetry run pytest -o addopts=""
The test suite uses pytest with pytest-mypy for static type checking and pytest-ruff for linting. All checks run together by default.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file promcraft-0.3.0.tar.gz.
File metadata
- Download URL: promcraft-0.3.0.tar.gz
- Upload date:
- Size: 24.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2b3a699f8f62c3e530d656b77bd1667186143af03629f5cf8f6d0a2900319b99
|
|
| MD5 |
4f5ccafc74319e204105a62313df261d
|
|
| BLAKE2b-256 |
4c0af867cc1c436455630098757f2d02dfa8f52bb8ecfdc0e565659fea0a51ab
|
Provenance
The following attestation bundles were made for promcraft-0.3.0.tar.gz:
Publisher:
release.yaml on xbabka01/promcraft
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promcraft-0.3.0.tar.gz -
Subject digest:
2b3a699f8f62c3e530d656b77bd1667186143af03629f5cf8f6d0a2900319b99 - Sigstore transparency entry: 2174293675
- Sigstore integration time:
-
Permalink:
xbabka01/promcraft@b994d77bc4c9ecc4c75046ebf1d38b899656d3db -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/xbabka01
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@b994d77bc4c9ecc4c75046ebf1d38b899656d3db -
Trigger Event:
push
-
Statement type:
File details
Details for the file promcraft-0.3.0-py3-none-any.whl.
File metadata
- Download URL: promcraft-0.3.0-py3-none-any.whl
- Upload date:
- Size: 23.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec68135f52c3bb106b161b1885a17ba303ed4d18f89ee520111506922cc0fc7b
|
|
| MD5 |
104e75a65a90ecb270c1ddbbb8de24d2
|
|
| BLAKE2b-256 |
b4806e87a34976eb2e8617f3d1798f088f551c11485a80f208529420a8427c40
|
Provenance
The following attestation bundles were made for promcraft-0.3.0-py3-none-any.whl:
Publisher:
release.yaml on xbabka01/promcraft
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
promcraft-0.3.0-py3-none-any.whl -
Subject digest:
ec68135f52c3bb106b161b1885a17ba303ed4d18f89ee520111506922cc0fc7b - Sigstore transparency entry: 2174293679
- Sigstore integration time:
-
Permalink:
xbabka01/promcraft@b994d77bc4c9ecc4c75046ebf1d38b899656d3db -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/xbabka01
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@b994d77bc4c9ecc4c75046ebf1d38b899656d3db -
Trigger Event:
push
-
Statement type: