promtext-cli 0.1.2.dev97__tar.gz → 0.1.2.dev98__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.
Files changed (21) hide show
  1. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.copier-answers.yml +1 -1
  2. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/PKG-INFO +1 -1
  3. promtext_cli-0.1.2.dev98/promtext_cli/main.py +12 -0
  4. promtext_cli-0.1.2.dev98/promtext_cli/promtext.py +182 -0
  5. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/pyproject.toml +2 -2
  6. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/tests/test_functional.py +14 -18
  7. promtext_cli-0.1.2.dev97/promtext_cli/main.py +0 -152
  8. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.commitlintrc.yaml +0 -0
  9. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.forgejo/workflows/commitlint.yaml +0 -0
  10. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.forgejo/workflows/lint.yaml +0 -0
  11. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.forgejo/workflows/publish.yaml +0 -0
  12. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.forgejo/workflows/test.yaml +0 -0
  13. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.gitignore +0 -0
  14. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/.pre-commit-config.yaml +0 -0
  15. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/README.md +0 -0
  16. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/cliff.toml +0 -0
  17. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/promtext_cli/__init__.py +0 -0
  18. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/renovate.json +0 -0
  19. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/tests/.gitkeep +0 -0
  20. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/tests/test_blackbox.py +0 -0
  21. {promtext_cli-0.1.2.dev97 → promtext_cli-0.1.2.dev98}/uv.lock +0 -0
@@ -14,4 +14,4 @@ project_name: promtext-cli
14
14
  pypi_publish: true
15
15
  pypi_publish_dev: true
16
16
  scripts:
17
- promtext: promtext_cli.main:promtext
17
+ promtext: promtext_cli:main.main
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: promtext-cli
3
- Version: 0.1.2.dev97
3
+ Version: 0.1.2.dev98
4
4
  Summary: Prometheus Textfile Tooling
5
5
  Project-URL: Documentation, https://codeberg.org/margau/promtext-cli/src/branch/main#readme
6
6
  Project-URL: Issues, https://codeberg.org/margau/promtext-cli/issues
@@ -0,0 +1,12 @@
1
+ """This module takes care of entrypoints"""
2
+
3
+ from .promtext import Promtext
4
+
5
+
6
+ def main():
7
+ """main method invoking the core class"""
8
+ Promtext().cli_entrypoint()
9
+
10
+
11
+ if __name__ == "__main__":
12
+ main()
@@ -0,0 +1,182 @@
1
+ """this is the module providing the core functionality"""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+ import logging
6
+ import sys
7
+
8
+ from prometheus_client.parser import text_string_to_metric_families
9
+ from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
10
+
11
+
12
+ class Promtext:
13
+ """Class for the core functionality"""
14
+
15
+ def __init__(self):
16
+ self.textfile = None
17
+ self.logger = None
18
+ self.args = None
19
+ self.registry = CollectorRegistry()
20
+
21
+ self.metrics = {}
22
+
23
+ def _arguments(self):
24
+ """load every input item from arguments & define cli"""
25
+ # required file first
26
+ parser = argparse.ArgumentParser(description="Prometheus textfile helper")
27
+ parser.add_argument(
28
+ "filename",
29
+ type=str,
30
+ help="Path to existing or new prometheus textfile, will be updated",
31
+ )
32
+
33
+ # metric name, required
34
+ parser.add_argument("metric", type=str, help="metric name (new or updated)")
35
+
36
+ # metric value as int/float, required
37
+ parser.add_argument("value", type=float, help="metric value")
38
+
39
+ # metric documentation as optional argument
40
+ parser.add_argument(
41
+ "--docs",
42
+ type=str,
43
+ help="metric documentation",
44
+ default="metric appended by promtext-cli",
45
+ )
46
+
47
+ # labels, key-value, minimum 0, repeatable
48
+ parser.add_argument(
49
+ "--label",
50
+ metavar="KEY=VALUE",
51
+ help="label key=value pairs",
52
+ action="append",
53
+ )
54
+
55
+ # log level from argparse
56
+ parser.add_argument(
57
+ "-v",
58
+ "--verbose",
59
+ action="store_const",
60
+ dest="loglevel",
61
+ const=logging.INFO,
62
+ )
63
+ self.args = parser.parse_args()
64
+
65
+ # processing: check if file is available, if yes, parse
66
+ self.textfile = Path(self.args.filename)
67
+
68
+ def parse_file(self):
69
+ """If possible, convert the input textfile to metrics in the registry"""
70
+ # check if self.args.filename exists with pathlib
71
+ if self.textfile.is_file():
72
+ for f in text_string_to_metric_families(
73
+ self.textfile.read_text(encoding="utf-8")
74
+ ):
75
+ # per metric: iterate over samples, create metric in registry
76
+ m = False
77
+ samples = []
78
+ for s in f.samples:
79
+ samples.append(s)
80
+ if len(samples) > 0:
81
+ # if we have samples, use the labelnames from them
82
+ labelnames = list(samples[0].labels.keys())
83
+ # metric-type specific init
84
+ if f.type == "gauge":
85
+ m = Gauge(
86
+ f.name,
87
+ f.documentation,
88
+ unit=f.unit,
89
+ labelnames=labelnames,
90
+ registry=self.registry,
91
+ )
92
+ else:
93
+ # we don't support other types yet, continue in these cases
94
+ self.logger.warning(
95
+ "unsupported metric type %s, dropping", f.type
96
+ )
97
+ continue
98
+ for s in samples:
99
+ if len(labelnames) > 0:
100
+ labelvalues = list(s.labels.values())
101
+ m.labels(*labelvalues).set(s.value)
102
+ else:
103
+ m.set(s.value)
104
+ self.metrics[f.name] = m
105
+ self.logger.info(
106
+ "copy gauge metric %s with labels %s from old file",
107
+ f.name,
108
+ ", ".join(labelnames),
109
+ )
110
+ else:
111
+ self.logger.warning(
112
+ "got empty metric %s from old file, dropping", f.name
113
+ )
114
+
115
+ def _build_metrics(self):
116
+ # add new metric from commandline
117
+ m = False
118
+
119
+ # figure out labelkey- and values
120
+ labels = {}
121
+ if self.args.label:
122
+ for lpair in self.args.label:
123
+ k, v = lpair.split("=")
124
+ labels[k] = v
125
+
126
+ labelvalues = []
127
+ # here, we use a new metric
128
+ if self.args.metric not in self.metrics:
129
+ self.logger.info("adding new metric %s", self.args.metric)
130
+ m = Gauge(
131
+ self.args.metric,
132
+ self.args.docs,
133
+ registry=self.registry,
134
+ labelnames=labels.keys(),
135
+ )
136
+ labelvalues = labels.values()
137
+ else:
138
+ m = self.metrics[self.args.metric]
139
+
140
+ # There is no way to access existing labelnames directly
141
+ # pylint: disable=W0212
142
+ old_labelnames = list(m._labelnames)
143
+ for la in old_labelnames:
144
+ self.logger.info("processing label %s", la)
145
+ if la in labels: # labelvalues are needed in order!
146
+ labelvalues.append(labels[la])
147
+ else:
148
+ self.logger.error(
149
+ "previously known label '%s' missing, cannot update!", la
150
+ )
151
+ sys.exit(1)
152
+ if len(old_labelnames) != len(labels.keys()):
153
+ self.logger.error(
154
+ "labelnames for metric %s not the same, cannot update! Old: %s, New: %s",
155
+ self.args.metric,
156
+ old_labelnames,
157
+ list(labels.keys()),
158
+ )
159
+ sys.exit(1)
160
+ self.logger.info("updating metric %s", self.args.metric)
161
+
162
+ # actually set the value
163
+ if len(labelvalues) > 0:
164
+ m.labels(*labelvalues).set(self.args.value)
165
+ else:
166
+ m.set(self.args.value)
167
+
168
+ def output_file(self):
169
+ """Output to a textfile"""
170
+ write_to_textfile(self.textfile, self.registry)
171
+ self.logger.info("wrote to %s", self.textfile)
172
+
173
+ def cli_entrypoint(self):
174
+ """Main method called from the CLI"""
175
+ self._arguments()
176
+
177
+ logging.basicConfig(level=self.args.loglevel)
178
+ self.logger = logging.getLogger(__name__)
179
+
180
+ self.parse_file()
181
+ self._build_metrics()
182
+ self.output_file()
@@ -28,7 +28,7 @@ Source = "https://codeberg.org/margau/promtext-cli.git"
28
28
 
29
29
  # Scripts
30
30
  [project.scripts]
31
- promtext = "promtext_cli.main:promtext"
31
+ promtext = "promtext_cli:main.main"
32
32
 
33
33
  [dependency-groups]
34
34
  dev = [
@@ -43,6 +43,6 @@ dev = [
43
43
  cache-keys = [{ file = "pyproject.toml" }, { git = { commit = true, tags = true } }]
44
44
 
45
45
  [tool.coverage.run]
46
- source = ["src", "tests"]
46
+ source = ["promtext_cli", "tests"]
47
47
 
48
48
  # TODO: Dev/CI scripts for lint & co.
@@ -4,7 +4,7 @@
4
4
  from cli_test_helpers import ArgvContext
5
5
  import pytest
6
6
 
7
- import promtext_cli.main
7
+ from promtext_cli.main import main as promtext_main
8
8
 
9
9
 
10
10
  def test_new_file(tmp_path):
@@ -13,7 +13,7 @@ def test_new_file(tmp_path):
13
13
  """
14
14
  promfile = tmp_path / "new_file.prom"
15
15
  with ArgvContext("promtext", str(promfile), "test_metric", "0"):
16
- promtext_cli.main.promtext()
16
+ promtext_main()
17
17
  assert promfile.exists()
18
18
  assert promfile.read_text() == (
19
19
  "# HELP test_metric metric appended by promtext-cli"
@@ -33,7 +33,7 @@ def test_labels(tmp_path):
33
33
  with ArgvContext(
34
34
  "promtext", "--label", "testlabel=testvalue", str(promfile), "test_metric", 0
35
35
  ):
36
- promtext_cli.main.promtext()
36
+ promtext_main()
37
37
  assert promfile.exists()
38
38
  assert promfile.read_text() == (
39
39
  "# HELP test_metric metric appended by promtext-cli"
@@ -62,7 +62,7 @@ def test_existing_metric_append_metric(tmp_path):
62
62
  with ArgvContext(
63
63
  "promtext", "--label", "testlabel=testvalue", str(promfile), "new_metric", 0
64
64
  ):
65
- promtext_cli.main.promtext()
65
+ promtext_main()
66
66
  assert promfile.exists()
67
67
  assert promfile.read_text() == (
68
68
  "# HELP existing_metric metric appended by promtext-cli"
@@ -97,7 +97,7 @@ def test_existing_metric_append_labelvalue(tmp_path):
97
97
  with ArgvContext(
98
98
  "promtext", "--label", "testlabel=new", str(promfile), "existing_metric", 0
99
99
  ):
100
- promtext_cli.main.promtext()
100
+ promtext_main()
101
101
  assert promfile.exists()
102
102
  assert promfile.read_text() == (
103
103
  "# HELP existing_metric metric appended by promtext-cli"
@@ -135,7 +135,7 @@ def test_existing_metric_multilabel(tmp_path):
135
135
  "existing_metric",
136
136
  "42",
137
137
  ):
138
- promtext_cli.main.promtext()
138
+ promtext_main()
139
139
  assert promfile.exists()
140
140
  assert promfile.read_text() == (
141
141
  "# HELP existing_metric metric appended by promtext-cli"
@@ -170,7 +170,7 @@ def test_existing_metric_overwrite(tmp_path):
170
170
  "existing_metric",
171
171
  "42",
172
172
  ):
173
- promtext_cli.main.promtext()
173
+ promtext_main()
174
174
  assert promfile.exists()
175
175
  assert promfile.read_text() == (
176
176
  "# HELP existing_metric metric appended by promtext-cli"
@@ -196,7 +196,7 @@ def test_existing_metric_plain(tmp_path):
196
196
  "\n"
197
197
  )
198
198
  with ArgvContext("promtext", str(promfile), "existing_metric", "42"):
199
- promtext_cli.main.promtext()
199
+ promtext_main()
200
200
  assert promfile.exists()
201
201
  assert promfile.read_text() == (
202
202
  "# HELP existing_metric metric appended by promtext-cli"
@@ -226,7 +226,7 @@ def test_existing_metric_labeldrop(tmp_path, capsys):
226
226
  ArgvContext("promtext", str(promfile), "existing_metric", "42"),
227
227
  pytest.raises(SystemExit) as pytest_wrapped_e,
228
228
  ):
229
- promtext_cli.main.promtext()
229
+ promtext_main()
230
230
  assert promfile.exists()
231
231
  assert promfile.read_text() == (
232
232
  "# HELP existing_metric metric appended by promtext-cli"
@@ -240,10 +240,7 @@ def test_existing_metric_labeldrop(tmp_path, capsys):
240
240
  assert pytest_wrapped_e.type is SystemExit
241
241
  assert pytest_wrapped_e.value.code == 1
242
242
  captured = capsys.readouterr()
243
- assert (
244
- "ERROR:promtext_cli.main:previously known label 'testlabel' missing, cannot update!"
245
- in captured.err
246
- )
243
+ assert "previously known label 'testlabel' missing, cannot update!" in captured.err
247
244
 
248
245
 
249
246
  def test_existing_metric_labelchange(tmp_path, capsys):
@@ -271,7 +268,7 @@ def test_existing_metric_labelchange(tmp_path, capsys):
271
268
  ),
272
269
  pytest.raises(SystemExit) as pytest_wrapped_e,
273
270
  ):
274
- promtext_cli.main.promtext()
271
+ promtext_main()
275
272
  assert promfile.exists()
276
273
  assert promfile.read_text() == (
277
274
  "# HELP existing_metric metric appended by promtext-cli"
@@ -286,8 +283,7 @@ def test_existing_metric_labelchange(tmp_path, capsys):
286
283
  assert pytest_wrapped_e.value.code == 1
287
284
  captured = capsys.readouterr()
288
285
  assert (
289
- "ERROR:promtext_cli.main:previously known label 'existinglabel' missing, cannot update!"
290
- in captured.err
286
+ "previously known label 'existinglabel' missing, cannot update!" in captured.err
291
287
  )
292
288
 
293
289
 
@@ -318,7 +314,7 @@ def test_existing_metric_labeladd(tmp_path, capsys):
318
314
  ),
319
315
  pytest.raises(SystemExit) as pytest_wrapped_e,
320
316
  ):
321
- promtext_cli.main.promtext()
317
+ promtext_main()
322
318
  assert promfile.exists()
323
319
  assert promfile.read_text() == (
324
320
  "# HELP existing_metric metric appended by promtext-cli"
@@ -333,7 +329,7 @@ def test_existing_metric_labeladd(tmp_path, capsys):
333
329
  assert pytest_wrapped_e.value.code == 1
334
330
  captured = capsys.readouterr()
335
331
  assert (
336
- "ERROR:promtext_cli.main:labelnames for metric existing_metric not the same, cannot update"
332
+ "labelnames for metric existing_metric not the same, cannot update"
337
333
  in captured.err
338
334
  )
339
335
 
@@ -1,152 +0,0 @@
1
- """promtext_cli is providing a CLI to cleanly update prometheus textfiles from scripts"""
2
-
3
- # pylint: disable=C0116,R0914,R0912,R0915
4
- # this rules will be fixed by a object-oriented refactoring
5
-
6
- import argparse
7
- from pathlib import Path
8
- import logging
9
- import sys
10
-
11
- from prometheus_client.parser import text_string_to_metric_families
12
- from prometheus_client import CollectorRegistry, Gauge, write_to_textfile
13
-
14
-
15
- def promtext():
16
- # setup argpars
17
- # required file first
18
- parser = argparse.ArgumentParser(description="Prometheus textfile helper")
19
- parser.add_argument(
20
- "filename",
21
- type=str,
22
- help="Path to existing or new prometheus textfile, will be updated",
23
- )
24
-
25
- # metric name, required
26
- parser.add_argument("metric", type=str, help="metric name (new or updated)")
27
-
28
- # metric value as int/float, required
29
- parser.add_argument("value", type=float, help="metric value")
30
-
31
- # metric documentation as optional argument
32
- parser.add_argument(
33
- "--docs",
34
- type=str,
35
- help="metric documentation",
36
- default="metric appended by promtext-cli",
37
- )
38
-
39
- # labels, key-value, minimum 0, repeatable
40
- parser.add_argument(
41
- "--label", metavar="KEY=VALUE", help="label key=value pairs", action="append"
42
- )
43
-
44
- # log level from argparse
45
- parser.add_argument(
46
- "-v",
47
- "--verbose",
48
- action="store_const",
49
- dest="loglevel",
50
- const=logging.INFO,
51
- )
52
- args = parser.parse_args()
53
- logging.basicConfig(level=args.loglevel)
54
- logger = logging.getLogger(__name__)
55
-
56
- # processing: check if file is available, if yes, parse
57
- textfile = Path(args.filename)
58
-
59
- registry = CollectorRegistry()
60
- metrics = {}
61
-
62
- # check if args.filename exists with pathlib
63
- if textfile.is_file():
64
- for f in text_string_to_metric_families(textfile.read_text(encoding="utf-8")):
65
- # per metric: iterate over samples, create metric in registry
66
- m = False
67
- samples = []
68
- for s in f.samples:
69
- samples.append(s)
70
- if len(samples) > 0:
71
- # if we have samples, use the labelnames from them
72
- labelnames = list(samples[0].labels.keys())
73
- # metric-type specific init
74
- if f.type == "gauge":
75
- m = Gauge(
76
- f.name,
77
- f.documentation,
78
- unit=f.unit,
79
- labelnames=labelnames,
80
- registry=registry,
81
- )
82
- else:
83
- # we don't support other types yet, continue in these cases
84
- logger.warning("unsupported metric type %s, dropping", f.type)
85
- continue
86
- for s in samples:
87
- if len(labelnames) > 0:
88
- labelvalues = list(s.labels.values())
89
- m.labels(*labelvalues).set(s.value)
90
- else:
91
- m.set(s.value)
92
- metrics[f.name] = m
93
- logger.info(
94
- "copy gauge metric %s with labels %s from old file",
95
- f.name,
96
- ", ".join(labelnames),
97
- )
98
- else:
99
- logger.warning("got empty metric %s from old file, dropping", f.name)
100
-
101
- # add new metric from commandline
102
- m = False
103
-
104
- # figure out labelkey- and values
105
- labels = {}
106
- if args.label:
107
- for lpair in args.label:
108
- k, v = lpair.split("=")
109
- labels[k] = v
110
-
111
- labelvalues = []
112
- # here, we use a new metric
113
- if args.metric not in metrics:
114
- logger.info("adding new metric %s", args.metric)
115
- m = Gauge(args.metric, args.docs, registry=registry, labelnames=labels.keys())
116
- labelvalues = labels.values()
117
- else:
118
- m = metrics[args.metric]
119
-
120
- # There is no way to access existing labelnames directly
121
- # pylint: disable=W0212
122
- old_labelnames = list(m._labelnames)
123
- for la in old_labelnames:
124
- logger.info("processing label %s", la)
125
- if la in labels: # labelvalues are needed in order!
126
- labelvalues.append(labels[la])
127
- else:
128
- logger.error("previously known label '%s' missing, cannot update!", la)
129
- sys.exit(1)
130
- if len(old_labelnames) != len(labels.keys()):
131
- logger.error(
132
- "labelnames for metric %s not the same, cannot update! Old: %s, New: %s",
133
- args.metric,
134
- old_labelnames,
135
- list(labels.keys()),
136
- )
137
- sys.exit(1)
138
- logger.info("updating metric %s", args.metric)
139
-
140
- # actually set the value
141
- if len(labelvalues) > 0:
142
- m.labels(*labelvalues).set(args.value)
143
- else:
144
- m.set(args.value)
145
-
146
- # write to file
147
- write_to_textfile(args.filename, registry)
148
- logger.info("wrote to %s", args.filename)
149
-
150
-
151
- if __name__ == "__main__":
152
- promtext()