promcraft 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Peter Babka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,670 @@
1
+ Metadata-Version: 2.4
2
+ Name: promcraft
3
+ Version: 0.1.0
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: Peter Babka
7
+ Author-email: 159peter951@gmail.com
8
+ Requires-Python: >=3.10,<4.0
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Description-Content-Type: text/markdown
16
+
17
+ # promcraft
18
+
19
+ [![pypi](https://img.shields.io/pypi/v/promcraft)](https://pypi.org/project/promcraft/)
20
+ [![python](https://img.shields.io/pypi/pyversions/promcraft.svg)](https://pypi.org/project/promcraft/)
21
+ [![Tests](https://github.com/xbabka01/promcraft/actions/workflows/python.yml/badge.svg)](https://github.com/xbabka01/promcraft/actions/workflows/python.yml)
22
+
23
+ A Python library for building [Prometheus QL](https://prometheus.io/docs/prometheus/latest/querying/basics/) queries programmatically. Instead of constructing raw query strings, use composable Python objects that serialize to valid PromQL syntax.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install promcraft
29
+ # or with Poetry
30
+ poetry add promcraft
31
+ ```
32
+
33
+ Requires Python 3.10+.
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from promcraft import InstantVector, RangeVector, Label, String, Duration, BinaryOprator
39
+
40
+ # Simple metric selector
41
+ query = InstantVector("up", [])
42
+ str(query) # "up{}"
43
+
44
+ # With label filters
45
+ query = InstantVector("http_requests_total", [
46
+ Label.eq("job", String("api-server")),
47
+ Label.eq("env", String("production")),
48
+ ])
49
+ str(query) # 'http_requests_total{job = "api-server", env = "production"}'
50
+
51
+ # Range vector with rate()
52
+ from promcraft import rate
53
+ query = rate(RangeVector("http_requests_total", [
54
+ Label.eq("job", String("api-server")),
55
+ ], Duration(m=5)))
56
+ str(query) # 'rate(http_requests_total{job = "api-server"}[5m])'
57
+
58
+ # Binary operation
59
+ left = InstantVector("http_requests_total", [Label.eq("status", String("500"))])
60
+ right = InstantVector("http_requests_total", [])
61
+ ratio = left / right
62
+ str(ratio) # 'http_requests_total{status = "500"} / http_requests_total{}'
63
+ ```
64
+
65
+ ## API Reference
66
+
67
+ ### Scalars
68
+
69
+ #### `Float`
70
+
71
+ Wraps a Python float for use in queries.
72
+
73
+ ```python
74
+ from promcraft import Float
75
+
76
+ Float(3.14) # "3.14"
77
+ Float(0.0) # "0.0"
78
+ Float(-2.5) # "-2.5"
79
+ ```
80
+
81
+ #### `Hex`
82
+
83
+ Represents a hexadecimal integer literal.
84
+
85
+ ```python
86
+ from promcraft import Hex
87
+
88
+ Hex(255) # "0xff"
89
+ Hex(0) # "0x0"
90
+ Hex(16) # "0x10"
91
+ ```
92
+
93
+ #### `Duration`
94
+
95
+ Represents a Prometheus duration literal. All parameters are keyword-only.
96
+
97
+ ```python
98
+ from promcraft import Duration
99
+
100
+ Duration() # "0s" (default when no units given)
101
+ Duration(s=5) # "5s"
102
+ Duration(m=1, s=30) # "1m30s"
103
+ Duration(h=1, m=30) # "1h30m"
104
+ Duration(ms=500) # "500ms"
105
+ Duration(y=1, w=2, d=3, h=4, m=5, s=6, ms=7) # "1y2w3d4h5m6s7ms"
106
+ Duration(m=5, neg=True) # "-5m"
107
+ ```
108
+
109
+ | Parameter | Unit |
110
+ |-----------|-------------|
111
+ | `y` | years |
112
+ | `w` | weeks |
113
+ | `d` | days |
114
+ | `h` | hours |
115
+ | `m` | minutes |
116
+ | `s` | seconds |
117
+ | `ms` | milliseconds|
118
+ | `neg` | negate the duration |
119
+
120
+ ---
121
+
122
+ ### `String`
123
+
124
+ Represents a PromQL string literal with configurable quoting.
125
+
126
+ ```python
127
+ from promcraft import String
128
+
129
+ String("hello") # '"hello"' (double-quote default)
130
+ String("hello", '"') # '"hello"'
131
+ String("hello", "'") # "'hello'"
132
+ String("hello", "`") # "`hello`"
133
+ ```
134
+
135
+ Special characters are escaped automatically for `"` and `'` quotes. Backtick strings are not escaped.
136
+
137
+ ---
138
+
139
+ ### `Label`
140
+
141
+ Represents a label matcher used inside vector selectors. Use the factory methods for convenience.
142
+
143
+ ```python
144
+ from promcraft import Label, String
145
+
146
+ Label.eq("job", String("prometheus")) # 'job = "prometheus"'
147
+ Label.neq("env", String("prod")) # 'env != "prod"'
148
+ Label.re("name", String("prom.*")) # 'name =~ "prom.*"'
149
+ Label.nre("name", String("test.*")) # 'name !~ "test.*"'
150
+ ```
151
+
152
+ | Method | Operator | Meaning |
153
+ |---------------|----------|----------------------|
154
+ | `Label.eq` | `=` | Exact match |
155
+ | `Label.neq` | `!=` | Not equal |
156
+ | `Label.re` | `=~` | Regex match |
157
+ | `Label.nre` | `!~` | Regex does not match |
158
+
159
+ ---
160
+
161
+ ### `InstantVector`
162
+
163
+ Selects a set of time series at a single point in time.
164
+
165
+ ```python
166
+ InstantVector(metric, labels, *, offset=None, at=None)
167
+ ```
168
+
169
+ ```python
170
+ from promcraft import InstantVector, Label, String, Duration, Float
171
+
172
+ # Basic
173
+ InstantVector("up", [])
174
+ # "up{}"
175
+
176
+ # With labels
177
+ InstantVector("http_requests_total", [
178
+ Label.eq("job", String("api")),
179
+ Label.re("status", String("5..")),
180
+ ])
181
+ # 'http_requests_total{job = "api", status =~ "5.."}'
182
+
183
+ # With offset
184
+ InstantVector("up", [], offset=Duration(m=5))
185
+ # "up{} offset 5m"
186
+
187
+ # With @ modifier (Unix timestamp)
188
+ InstantVector("up", [], at=Float(1609746000.0))
189
+ # "up{} @ 1609746000.0"
190
+
191
+ # With @ modifier (range function references)
192
+ InstantVector("up", [], at="start()")
193
+ # "up{} @ start()"
194
+
195
+ InstantVector("up", [], at="end()")
196
+ # "up{} @ end()"
197
+ ```
198
+
199
+ ---
200
+
201
+ ### `RangeVector`
202
+
203
+ Selects a range of samples over a time window. Required by functions like `rate()`, `increase()`, `avg_over_time()`.
204
+
205
+ ```python
206
+ RangeVector(metric, labels, range, *, resolution=None, offset=None, at=None)
207
+ ```
208
+
209
+ ```python
210
+ from promcraft import RangeVector, Label, String, Duration
211
+
212
+ # Basic range
213
+ RangeVector("http_requests_total", [], Duration(m=5))
214
+ # "http_requests_total{}[5m]"
215
+
216
+ # With resolution (subquery step)
217
+ RangeVector("up", [], Duration(m=5), resolution=Duration(s=30))
218
+ # "up{}[5m :30s]"
219
+
220
+ # With offset
221
+ RangeVector("up", [], Duration(m=5), offset=Duration(m=1))
222
+ # "up{}[5m] offset 1m"
223
+
224
+ # Combined
225
+ RangeVector(
226
+ "http_requests_total",
227
+ [Label.eq("job", String("api"))],
228
+ Duration(m=5),
229
+ resolution=Duration(s=30),
230
+ offset=Duration(m=1),
231
+ at="end()",
232
+ )
233
+ # 'http_requests_total{job = "api"}[5m :30s] offset 1m @ end()'
234
+ ```
235
+
236
+ ---
237
+
238
+ ### `BinaryOprator`
239
+
240
+ Combines two query expressions with a binary operator.
241
+
242
+ ```python
243
+ BinaryOprator(op, left, right, *, match=None, group=None)
244
+ ```
245
+
246
+ #### Operators
247
+
248
+ | Enum value | Symbol | Category |
249
+ |------------------------------|----------|----------------|
250
+ | `BinaryOprator.Operator.ADD` | `+` | Arithmetic |
251
+ | `BinaryOprator.Operator.SUB` | `-` | Arithmetic |
252
+ | `BinaryOprator.Operator.MUL` | `*` | Arithmetic |
253
+ | `BinaryOprator.Operator.DIV` | `/` | Arithmetic |
254
+ | `BinaryOprator.Operator.MOD` | `%` | Arithmetic |
255
+ | `BinaryOprator.Operator.POW` | `^` | Arithmetic |
256
+ | `BinaryOprator.Operator.EQ` | `==` | Comparison |
257
+ | `BinaryOprator.Operator.NEQ` | `!=` | Comparison |
258
+ | `BinaryOprator.Operator.LT` | `<` | Comparison |
259
+ | `BinaryOprator.Operator.LTE` | `<=` | Comparison |
260
+ | `BinaryOprator.Operator.GT` | `>` | Comparison |
261
+ | `BinaryOprator.Operator.GTE` | `>=` | Comparison |
262
+ | `BinaryOprator.Operator.AND` | `and` | Logical/Set |
263
+ | `BinaryOprator.Operator.OR` | `or` | Logical/Set |
264
+ | `BinaryOprator.Operator.UNLESS` | `unless` | Logical/Set |
265
+ | `BinaryOprator.Operator.ATAN2` | `atan2` | Trigonometric |
266
+
267
+ #### Helper functions
268
+
269
+ Each operator has a module-level helper so you don't need to import the enum:
270
+
271
+ ```python
272
+ from promcraft import (
273
+ add, sub, mul, div, mod, pow,
274
+ eq, neq, lt, lte, gt, gte,
275
+ and_, or_, unless, atan2,
276
+ Float, InstantVector,
277
+ )
278
+
279
+ v1 = InstantVector("requests", [])
280
+ v2 = InstantVector("errors", [])
281
+
282
+ add(Float(1.0), Float(2.0)) # "1.0 + 2.0"
283
+ div(v2, v1) # "errors{} / requests{}"
284
+ gt(v1, Float(0.0)) # "requests{} > 0.0"
285
+ and_(v1, v2) # "requests{} and errors{}"
286
+ ```
287
+
288
+ All helpers accept the same optional `match` and `group` keyword arguments as the constructor:
289
+
290
+ ```python
291
+ div(v2, v1, match=Match.on(["job"]), group=Group.left([]))
292
+ ```
293
+
294
+ #### Basic usage
295
+
296
+ ```python
297
+ from promcraft import BinaryOprator, Float
298
+
299
+ Op = BinaryOprator.Operator
300
+
301
+ BinaryOprator(Op.ADD, Float(1.0), Float(2.0)) # "1.0 + 2.0"
302
+ BinaryOprator(Op.MUL, Float(2.0), Float(3.0)) # "2.0 * 3.0"
303
+ ```
304
+
305
+ #### Label matching
306
+
307
+ Use `on()` or `ignoring()` to control which labels are used for matching when combining two vector selectors:
308
+
309
+ ```python
310
+ from promcraft import BinaryOprator, InstantVector, Label, String
311
+
312
+ Op = BinaryOprator.Operator
313
+
314
+ requests = InstantVector("http_requests_total", [])
315
+ errors = InstantVector("http_errors_total", [])
316
+
317
+ # Match on specific labels
318
+ expr = BinaryOprator(Op.DIV, errors, requests).on(["job", "env"])
319
+ # 'http_errors_total{} / on(job, env) http_requests_total{}'
320
+
321
+ # Ignore specific labels
322
+ expr = BinaryOprator(Op.DIV, errors, requests).ignoring(["instance"])
323
+ # 'http_errors_total{} / ignoring(instance) http_requests_total{}'
324
+ ```
325
+
326
+ #### Grouping
327
+
328
+ Use `group_left()` or `group_right()` for many-to-one or one-to-many matching:
329
+
330
+ ```python
331
+ # group_left: result has labels from the left side
332
+ expr = (BinaryOprator(Op.MUL, requests, errors)
333
+ .on(["job"])
334
+ .group_left(["env"]))
335
+ # '... on(job) group_left(env) ...'
336
+
337
+ # group_right: result has labels from the right side
338
+ expr = (BinaryOprator(Op.MUL, requests, errors)
339
+ .group_right([]))
340
+ ```
341
+
342
+ Matching and grouping can also be passed directly to the constructor:
343
+
344
+ ```python
345
+ BinaryOprator(Op.DIV, left, right, match=Match.on(["job"]), group=Group.left([]))
346
+ ```
347
+
348
+ #### Nested expressions
349
+
350
+ Nested `BinaryOprator` operands are automatically parenthesized:
351
+
352
+ ```python
353
+ inner = BinaryOprator(Op.ADD, Float(1.0), Float(2.0))
354
+ outer = BinaryOprator(Op.MUL, inner, Float(3.0))
355
+ str(outer) # "(1.0 + 2.0) * 3.0"
356
+ ```
357
+
358
+ ---
359
+
360
+ ### `Match`
361
+
362
+ Controls label matching for binary operations (used directly or via `BinaryOprator` chainable methods).
363
+
364
+ ```python
365
+ from promcraft import Match
366
+
367
+ Match.on(["job", "env"]) # "on(job, env)"
368
+ Match.on([]) # "on()"
369
+ Match.ignoring(["instance"]) # "ignoring(instance)"
370
+ ```
371
+
372
+ ---
373
+
374
+ ### `Group`
375
+
376
+ Controls grouping for many-to-one / one-to-many binary operations.
377
+
378
+ ```python
379
+ from promcraft import Group
380
+
381
+ Group.left(["env"]) # "group_left(env)"
382
+ Group.left([]) # "group_left()"
383
+ Group.right(["job"]) # "group_right(job)"
384
+ ```
385
+
386
+ ---
387
+
388
+ ### `AggregationOperator`
389
+
390
+ Applies a PromQL [aggregation operator](https://prometheus.io/docs/prometheus/latest/querying/operators/#aggregation-operators) to a vector.
391
+
392
+ ```python
393
+ AggregationOperator(op, vector, *, parameter=None, grouping=None)
394
+ ```
395
+
396
+ #### `Grouping`
397
+
398
+ Controls label grouping for aggregations. Use `by()` to keep specified labels, `without()` to drop them:
399
+
400
+ ```python
401
+ from promcraft import Grouping
402
+
403
+ Grouping.by(["job", "env"]) # "by(job, env)"
404
+ Grouping.by([]) # "by()"
405
+ Grouping.without(["instance"]) # "without(instance)"
406
+ ```
407
+
408
+ The `.by()` and `.without()` chainable methods on `AggregationOperator` return a new instance:
409
+
410
+ ```python
411
+ from promcraft import AggregationOperator, InstantVector
412
+
413
+ v = InstantVector("http_requests_total", [])
414
+
415
+ AggregationOperator(AggregationOperator.Operator.SUM, v).by(["job"])
416
+ # "sum(http_requests_total{}) by(job)"
417
+ ```
418
+
419
+ #### Helper functions
420
+
421
+ Each aggregation operator has a module-level helper. Helpers for operators that require a `parameter` take it as their first argument.
422
+
423
+ **No-parameter aggregations** — `sum`, `avg`, `min`, `max`, `count`, `group`, `stddev`, `stdvar`:
424
+
425
+ ```python
426
+ from promcraft import (
427
+ sum, avg, min, max, count, group, stddev, stdvar,
428
+ InstantVector, Grouping,
429
+ )
430
+
431
+ v = InstantVector("http_requests_total", [])
432
+
433
+ sum(v) # "sum(http_requests_total{})"
434
+ avg(v, grouping=Grouping.by(["job"])) # "avg(http_requests_total{}) by(job)"
435
+ stddev(v, grouping=Grouping.without(["env"]))
436
+ # "stddev(http_requests_total{}) without(env)"
437
+ ```
438
+
439
+ **Scalar-parameter aggregations** — `topk`, `bottomk`, `quantile`, `limitk`, `limit_ratio`:
440
+
441
+ ```python
442
+ from promcraft import topk, bottomk, quantile, limitk, limit_ratio, Float
443
+
444
+ topk(Float(5.0), v) # "topk(5.0, http_requests_total{})"
445
+ bottomk(Float(3.0), v) # "bottomk(3.0, http_requests_total{})"
446
+ quantile(Float(0.95), v) # "quantile(0.95, http_requests_total{})"
447
+ limitk(Float(10.0), v) # "limitk(10.0, http_requests_total{})"
448
+ limit_ratio(Float(0.1), v) # "limit_ratio(0.1, http_requests_total{})"
449
+ topk(Float(5.0), v, grouping=Grouping.by(["job"]))
450
+ # "topk(5.0, http_requests_total{}) by(job)"
451
+ ```
452
+
453
+ **String-parameter aggregation** — `count_values` (label name as a `String`):
454
+
455
+ ```python
456
+ from promcraft import count_values, String
457
+
458
+ count_values(String("version"), v)
459
+ # 'count_values("version", http_requests_total{})'
460
+ ```
461
+
462
+ ---
463
+
464
+ ### Functions
465
+
466
+ `Function` wraps any [Prometheus query function](https://prometheus.io/docs/prometheus/latest/querying/functions/). Each function is also available as a typed helper.
467
+
468
+ #### Rate / counter functions
469
+
470
+ ```python
471
+ from promcraft import RangeVector, Duration, rate, irate, increase, delta, deriv, resets
472
+
473
+ rv = RangeVector("http_requests_total", [], Duration(m=5))
474
+
475
+ rate(rv) # "rate(http_requests_total{}[5m])"
476
+ irate(rv) # "irate(http_requests_total{}[5m])"
477
+ increase(rv) # "increase(http_requests_total{}[5m])"
478
+ delta(rv) # "delta(http_requests_total{}[5m])"
479
+ deriv(rv) # "deriv(http_requests_total{}[5m])"
480
+ resets(rv) # "resets(http_requests_total{}[5m])"
481
+ ```
482
+
483
+ #### `*_over_time` aggregations
484
+
485
+ ```python
486
+ from promcraft import (
487
+ avg_over_time, min_over_time, max_over_time, sum_over_time,
488
+ count_over_time, stddev_over_time, last_over_time,
489
+ present_over_time, absent_over_time, quantile_over_time, Float,
490
+ )
491
+
492
+ avg_over_time(rv) # "avg_over_time(http_requests_total{}[5m])"
493
+ quantile_over_time(Float(0.95), rv) # "quantile_over_time(0.95, http_requests_total{}[5m])"
494
+ ```
495
+
496
+ #### Math & rounding
497
+
498
+ ```python
499
+ from promcraft import InstantVector, abs, ceil, floor, sqrt, round, clamp, clamp_min, clamp_max, Float
500
+
501
+ v = InstantVector("cpu_usage", [])
502
+
503
+ abs(v) # "abs(cpu_usage{})"
504
+ ceil(v) # "ceil(cpu_usage{})"
505
+ sqrt(v) # "sqrt(cpu_usage{})"
506
+ round(v) # "round(cpu_usage{})"
507
+ round(v, Float(0.5)) # "round(cpu_usage{}, 0.5)"
508
+ clamp(v, Float(0.0), Float(1.0)) # "clamp(cpu_usage{}, 0.0, 1.0)"
509
+ clamp_min(v, Float(0.0)) # "clamp_min(cpu_usage{}, 0.0)"
510
+ clamp_max(v, Float(1.0)) # "clamp_max(cpu_usage{}, 1.0)"
511
+ ```
512
+
513
+ #### Exponential & logarithm
514
+
515
+ ```python
516
+ from promcraft import exp, ln, log2, log10
517
+
518
+ exp(v) # "exp(cpu_usage{})"
519
+ ln(v) # "ln(cpu_usage{})"
520
+ log2(v) # "log2(cpu_usage{})"
521
+ log10(v) # "log10(cpu_usage{})"
522
+ ```
523
+
524
+ #### Trigonometry
525
+
526
+ ```python
527
+ from promcraft import sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, deg, rad, pi, sgn
528
+
529
+ sin(v) # "sin(cpu_usage{})"
530
+ deg(v) # "deg(cpu_usage{})"
531
+ rad(v) # "rad(cpu_usage{})"
532
+ pi() # "pi()"
533
+ sgn(v) # "sgn(cpu_usage{})"
534
+ ```
535
+
536
+ #### Date / time
537
+
538
+ All accept an optional instant vector; when omitted they default to `vector(time())` in PromQL.
539
+
540
+ ```python
541
+ from promcraft import hour, minute, day_of_week, day_of_month, month, year, time
542
+
543
+ hour() # "hour()"
544
+ hour(v) # "hour(cpu_usage{})"
545
+ time() # "time()"
546
+ ```
547
+
548
+ #### Sorting
549
+
550
+ ```python
551
+ from promcraft import sort, sort_desc, sort_by_label, sort_by_label_desc, String
552
+
553
+ sort(v) # "sort(cpu_usage{})"
554
+ sort_desc(v) # "sort_desc(cpu_usage{})"
555
+ sort_by_label(v, String("job")) # 'sort_by_label(cpu_usage{}, "job")'
556
+ sort_by_label_desc(v, String("job"), String("env"))
557
+ # 'sort_by_label_desc(cpu_usage{}, "job", "env")'
558
+ ```
559
+
560
+ #### Label manipulation
561
+
562
+ ```python
563
+ from promcraft import label_join, label_replace, String
564
+
565
+ label_join(v, String("addr"), String(":"), String("host"), String("port"))
566
+ # 'label_join(cpu_usage{}, "addr", ":", "host", "port")'
567
+
568
+ label_replace(v, String("job"), String("${1}"), String("job"), String("(.+)"))
569
+ # 'label_replace(cpu_usage{}, "job", "${1}", "job", "(.+)")'
570
+ ```
571
+
572
+ #### Type conversion
573
+
574
+ ```python
575
+ from promcraft import scalar, vector, Float
576
+
577
+ scalar(v) # "scalar(cpu_usage{})"
578
+ vector(Float(1.0)) # "vector(1.0)"
579
+ ```
580
+
581
+ #### Histogram functions
582
+
583
+ ```python
584
+ from promcraft import (
585
+ histogram_quantile, histogram_fraction,
586
+ histogram_count, histogram_sum, histogram_avg,
587
+ histogram_stddev, histogram_stdvar, Float,
588
+ )
589
+
590
+ histogram_quantile(Float(0.95), v) # "histogram_quantile(0.95, cpu_usage{})"
591
+ histogram_fraction(Float(0.0), Float(1.0), v) # "histogram_fraction(0.0, 1.0, cpu_usage{})"
592
+ histogram_count(v) # "histogram_count(cpu_usage{})"
593
+ ```
594
+
595
+ #### Prediction & smoothing
596
+
597
+ ```python
598
+ from promcraft import predict_linear, double_exponential_smoothing
599
+
600
+ predict_linear(rv, Float(3600.0))
601
+ # "predict_linear(http_requests_total{}[5m], 3600.0)"
602
+
603
+ double_exponential_smoothing(rv, Float(0.1), Float(0.5))
604
+ # "double_exponential_smoothing(http_requests_total{}[5m], 0.1, 0.5)"
605
+ ```
606
+
607
+ #### Absence detection
608
+
609
+ ```python
610
+ from promcraft import absent, absent_over_time
611
+
612
+ absent(v) # "absent(cpu_usage{})"
613
+ absent_over_time(rv) # "absent_over_time(http_requests_total{}[5m])"
614
+ ```
615
+
616
+ #### Low-level: `Function`
617
+
618
+ The `Function` class underlies all helpers and can be used directly for any custom function:
619
+
620
+ ```python
621
+ from promcraft import Function, InstantVector
622
+
623
+ Function("my_custom_func", [InstantVector("up", []), Float(42.0)])
624
+ # "my_custom_func(up{}, 42.0)"
625
+ ```
626
+
627
+ ---
628
+
629
+ ## Composing Complex Queries
630
+
631
+ All objects implement `__str__`, so you can compose arbitrarily deep expressions:
632
+
633
+ ```python
634
+ from promcraft import (
635
+ BinaryOprator, InstantVector, RangeVector,
636
+ Label, String, Duration, Float, rate, div,
637
+ )
638
+
639
+ Op = BinaryOprator.Operator
640
+ job_label = Label.eq("job", String("api"))
641
+
642
+ # rate of 5xx errors divided by rate of total requests
643
+ error_rate = rate(RangeVector("http_requests_total", [
644
+ job_label,
645
+ Label.re("status", String("5..")),
646
+ ], Duration(m=5)))
647
+
648
+ total_rate = rate(RangeVector("http_requests_total", [job_label], Duration(m=5)))
649
+
650
+ ratio = div(error_rate, total_rate).on(["job"])
651
+ str(ratio)
652
+ # 'rate(http_requests_total{job = "api", status =~ "5.."}[5m])
653
+ # / on(job) rate(http_requests_total{job = "api"}[5m])'
654
+ ```
655
+
656
+ ## Development
657
+
658
+ ```bash
659
+ # Install dependencies
660
+ poetry install
661
+
662
+ # Run tests (includes type checking and linting)
663
+ poetry run pytest
664
+
665
+ # Run only unit tests
666
+ poetry run pytest --no-header -p no:mypy -p no:ruff
667
+ ```
668
+
669
+ The test suite uses `pytest` with `pytest-mypy` for static type checking and `pytest-ruff` for linting. All checks run together by default.
670
+