revtrend 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.
revtrend-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Simon Holesch
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,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: revtrend
3
+ Version: 0.1.0
4
+ Summary: Track numeric metrics per git commit and visualize trends in Grafana
5
+ Author: Simon Holesch
6
+ Author-email: Simon Holesch <simon@holesch.de>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Operating System :: POSIX :: Linux
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Quality Assurance
19
+ Classifier: Topic :: System :: Monitoring
20
+ Requires-Dist: fastapi
21
+ Requires-Dist: httpx
22
+ Requires-Dist: prometheus-client>=0.26.0
23
+ Requires-Dist: pydantic>=2.14.3
24
+ Requires-Dist: pyjwt[crypto]
25
+ Requires-Dist: uvicorn
26
+ Requires-Python: >=3.11
27
+ Project-URL: Homepage, https://github.com/holesch/revtrend
28
+ Project-URL: Source, https://github.com/holesch/revtrend
29
+ Project-URL: Issues, https://github.com/holesch/revtrend/issues
30
+ Description-Content-Type: text/markdown
31
+
32
+ # revtrend
33
+
34
+ Track numeric metrics per git commit (image size, binary size, test duration,
35
+ coverage, …) and visualize them over time in Grafana.
36
+
37
+ Data is keyed by commit SHA. Which commits belong to a view is answered from
38
+ a bare mirror of the project's repository: the first-parent history of the
39
+ selected ref, compared against a base ref.
40
+
41
+ ## Service
42
+
43
+ `revtrend-server --config /etc/revtrend/config.toml`, see
44
+ [contrib/config.example.toml](https://github.com/holesch/revtrend/blob/master/contrib/config.example.toml) and
45
+ [contrib/revtrend.service](https://github.com/holesch/revtrend/blob/master/contrib/revtrend.service).
46
+
47
+ All `/api/v1` endpoints require a bearer JWT from one of the configured
48
+ issuers. Roles per project: `reader` < `writer` < `admin`.
49
+
50
+ | Endpoint | Role | |
51
+ |---|---|---|
52
+ | `POST /api/v1/projects/{project}/measurements` | writer | ingest one run for a commit |
53
+ | `GET /api/v1/projects/{project}/series` | reader | flat rows for Grafana (`metric`, `ref`, `base`, `from`, `to`, `labels`, `agg`) |
54
+ | `GET /api/v1/projects/{project}/compare` | reader | head vs. nearest measured commit from the merge base with `base` |
55
+ | `GET /api/v1/projects` | any | projects the caller can read |
56
+ | `GET /api/v1/projects/{project}/refs` | reader | branches (`active_since`, `match`) |
57
+ | `GET /api/v1/projects/{project}/metrics` | reader | metric names and label values |
58
+ | `DELETE /api/v1/projects/{project}/measurements` | admin | delete by `run_id` or `metric` |
59
+ | `GET /auth/cli/{config,callback,poll}` | none | PKCE code relay for the CLI |
60
+ | `GET /healthz`, `GET /metrics` | none | health and Prometheus metrics |
61
+
62
+ `/metrics` is unauthenticated; don't expose it through the reverse proxy.
63
+
64
+ Ingest example:
65
+
66
+ ```sh
67
+ curl -fsS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
68
+ --data '{"sha": "<40 hex>", "run_id": "'"$BUILD_URL"'", "metrics": [
69
+ {"name": "image.size_bytes", "value": 81234432,
70
+ "labels": {"machine": "amd64", "image": "core-image"}}]}' \
71
+ https://revtrend.example/api/v1/projects/myproject/measurements
72
+ ```
73
+
74
+ The measurement `source` (`ci` or `manual`) is derived from the token issuer,
75
+ never from the request. Per commit, CI runs take precedence over manual runs.
76
+
77
+ Each measurement records the submitter as `<issuer name>:<sub>`, the stable
78
+ OIDC identifier. For a readable audit trail, set `display_claims` on an issuer
79
+ (e.g. `["preferred_username", "name"]`): the first claim present in the token
80
+ is stored as `submitted_name`.
81
+
82
+ ## CLI
83
+
84
+ ```sh
85
+ pipx install revtrend
86
+ export REVTREND_URL=https://revtrend.example
87
+
88
+ revtrend login
89
+ revtrend push --project myproject --sha HEAD metrics.json
90
+ revtrend compare --project myproject --head HEAD --base release/1.0
91
+ ```
92
+
93
+ `login` prints a URL to open in any browser, also from an ssh session: the
94
+ service receives the login redirect and relays the authorization code to the
95
+ CLI, which redeems it with its PKCE verifier. Tokens are cached in
96
+ `~/.cache/revtrend/sessions.json` and refreshed automatically.
97
+ `$REVTREND_TOKEN` overrides the cached session.
98
+
99
+ `push` takes a JSON array of metrics (`-` reads stdin):
100
+
101
+ ```json
102
+ [{"name": "image.size_bytes", "value": 81234432,
103
+ "labels": {"machine": "amd64", "image": "core-image"}}]
104
+ ```
105
+
106
+ `--sha` and `--head` are resolved in the current repository, so backfilling
107
+ means checking out an old commit, measuring and pushing. The commit must exist
108
+ in the remote. The run id defaults to `manual-<user>-<timestamp>`; pass
109
+ `--run-id` to make a retry idempotent.
110
+
111
+ `compare` prints head vs. baseline per series, or the raw response with
112
+ `--json`.
113
+
114
+ ## Development
115
+
116
+ ```sh
117
+ uv sync
118
+ ```
119
+
120
+ Tests:
121
+
122
+ ```sh
123
+ uv run pytest
124
+ ```
125
+
126
+ Static checks:
127
+
128
+ ```sh
129
+ uv run ruff check
130
+ uv run ruff format --check
131
+ ```
132
+
133
+ Entry points:
134
+
135
+ - `revtrend-server` — HTTP API (FastAPI + uvicorn)
136
+ - `revtrend` — CLI for pushing measurements and comparing commits
@@ -0,0 +1,105 @@
1
+ # revtrend
2
+
3
+ Track numeric metrics per git commit (image size, binary size, test duration,
4
+ coverage, …) and visualize them over time in Grafana.
5
+
6
+ Data is keyed by commit SHA. Which commits belong to a view is answered from
7
+ a bare mirror of the project's repository: the first-parent history of the
8
+ selected ref, compared against a base ref.
9
+
10
+ ## Service
11
+
12
+ `revtrend-server --config /etc/revtrend/config.toml`, see
13
+ [contrib/config.example.toml](https://github.com/holesch/revtrend/blob/master/contrib/config.example.toml) and
14
+ [contrib/revtrend.service](https://github.com/holesch/revtrend/blob/master/contrib/revtrend.service).
15
+
16
+ All `/api/v1` endpoints require a bearer JWT from one of the configured
17
+ issuers. Roles per project: `reader` < `writer` < `admin`.
18
+
19
+ | Endpoint | Role | |
20
+ |---|---|---|
21
+ | `POST /api/v1/projects/{project}/measurements` | writer | ingest one run for a commit |
22
+ | `GET /api/v1/projects/{project}/series` | reader | flat rows for Grafana (`metric`, `ref`, `base`, `from`, `to`, `labels`, `agg`) |
23
+ | `GET /api/v1/projects/{project}/compare` | reader | head vs. nearest measured commit from the merge base with `base` |
24
+ | `GET /api/v1/projects` | any | projects the caller can read |
25
+ | `GET /api/v1/projects/{project}/refs` | reader | branches (`active_since`, `match`) |
26
+ | `GET /api/v1/projects/{project}/metrics` | reader | metric names and label values |
27
+ | `DELETE /api/v1/projects/{project}/measurements` | admin | delete by `run_id` or `metric` |
28
+ | `GET /auth/cli/{config,callback,poll}` | none | PKCE code relay for the CLI |
29
+ | `GET /healthz`, `GET /metrics` | none | health and Prometheus metrics |
30
+
31
+ `/metrics` is unauthenticated; don't expose it through the reverse proxy.
32
+
33
+ Ingest example:
34
+
35
+ ```sh
36
+ curl -fsS -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
37
+ --data '{"sha": "<40 hex>", "run_id": "'"$BUILD_URL"'", "metrics": [
38
+ {"name": "image.size_bytes", "value": 81234432,
39
+ "labels": {"machine": "amd64", "image": "core-image"}}]}' \
40
+ https://revtrend.example/api/v1/projects/myproject/measurements
41
+ ```
42
+
43
+ The measurement `source` (`ci` or `manual`) is derived from the token issuer,
44
+ never from the request. Per commit, CI runs take precedence over manual runs.
45
+
46
+ Each measurement records the submitter as `<issuer name>:<sub>`, the stable
47
+ OIDC identifier. For a readable audit trail, set `display_claims` on an issuer
48
+ (e.g. `["preferred_username", "name"]`): the first claim present in the token
49
+ is stored as `submitted_name`.
50
+
51
+ ## CLI
52
+
53
+ ```sh
54
+ pipx install revtrend
55
+ export REVTREND_URL=https://revtrend.example
56
+
57
+ revtrend login
58
+ revtrend push --project myproject --sha HEAD metrics.json
59
+ revtrend compare --project myproject --head HEAD --base release/1.0
60
+ ```
61
+
62
+ `login` prints a URL to open in any browser, also from an ssh session: the
63
+ service receives the login redirect and relays the authorization code to the
64
+ CLI, which redeems it with its PKCE verifier. Tokens are cached in
65
+ `~/.cache/revtrend/sessions.json` and refreshed automatically.
66
+ `$REVTREND_TOKEN` overrides the cached session.
67
+
68
+ `push` takes a JSON array of metrics (`-` reads stdin):
69
+
70
+ ```json
71
+ [{"name": "image.size_bytes", "value": 81234432,
72
+ "labels": {"machine": "amd64", "image": "core-image"}}]
73
+ ```
74
+
75
+ `--sha` and `--head` are resolved in the current repository, so backfilling
76
+ means checking out an old commit, measuring and pushing. The commit must exist
77
+ in the remote. The run id defaults to `manual-<user>-<timestamp>`; pass
78
+ `--run-id` to make a retry idempotent.
79
+
80
+ `compare` prints head vs. baseline per series, or the raw response with
81
+ `--json`.
82
+
83
+ ## Development
84
+
85
+ ```sh
86
+ uv sync
87
+ ```
88
+
89
+ Tests:
90
+
91
+ ```sh
92
+ uv run pytest
93
+ ```
94
+
95
+ Static checks:
96
+
97
+ ```sh
98
+ uv run ruff check
99
+ uv run ruff format --check
100
+ ```
101
+
102
+ Entry points:
103
+
104
+ - `revtrend-server` — HTTP API (FastAPI + uvicorn)
105
+ - `revtrend` — CLI for pushing measurements and comparing commits
@@ -0,0 +1,108 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.7,<0.14"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "revtrend"
7
+ version = "0.1.0"
8
+ description = "Track numeric metrics per git commit and visualize trends in Grafana"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Framework :: FastAPI",
16
+ "Operating System :: POSIX :: Linux",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Software Development :: Quality Assurance",
24
+ "Topic :: System :: Monitoring",
25
+ ]
26
+ dependencies = [
27
+ "fastapi",
28
+ "httpx",
29
+ "prometheus-client>=0.26.0",
30
+ "pydantic>=2.14.3",
31
+ "PyJWT[crypto]",
32
+ "uvicorn",
33
+ ]
34
+
35
+ [[project.authors]]
36
+ name = "Simon Holesch"
37
+ email = "simon@holesch.de"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/holesch/revtrend"
41
+ Source = "https://github.com/holesch/revtrend"
42
+ Issues = "https://github.com/holesch/revtrend/issues"
43
+
44
+ [project.scripts]
45
+ revtrend = "revtrend.cli:main"
46
+ revtrend-server = "revtrend.server:main"
47
+
48
+ [dependency-groups]
49
+ dev = [
50
+ "pytest>=9.2.0",
51
+ "pytest-asyncio>=1.4.1.post1",
52
+ "ruff==0.14.7",
53
+ ]
54
+
55
+ [tool.pytest.ini_options]
56
+ testpaths = ["tests"]
57
+ asyncio_mode = "auto"
58
+ asyncio_default_fixture_loop_scope = "function"
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "F",
63
+ "E",
64
+ "W",
65
+ "I",
66
+ "N",
67
+ "UP",
68
+ "ASYNC",
69
+ "S",
70
+ "B",
71
+ "A",
72
+ "C4",
73
+ "ISC",
74
+ "LOG",
75
+ "G",
76
+ "PIE",
77
+ "T20",
78
+ "PT",
79
+ "RSE",
80
+ "SIM",
81
+ "TID",
82
+ "ARG",
83
+ "PTH",
84
+ "PL",
85
+ "FLY",
86
+ "FURB",
87
+ "RUF",
88
+ ]
89
+ ignore = [
90
+ "E501",
91
+ "PLW2901",
92
+ "PLR0402",
93
+ "PTH123",
94
+ "S603",
95
+ "SIM108",
96
+ ]
97
+
98
+ [tool.ruff.lint.per-file-ignores]
99
+ "tests/**.py" = [
100
+ "PLR2004",
101
+ "S101",
102
+ "S607",
103
+ "ARG001",
104
+ "PLR0913",
105
+ ]
106
+
107
+ [tool.ruff.lint.flake8-tidy-imports]
108
+ ban-relative-imports = "all"
@@ -0,0 +1,121 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.13.7,<0.14"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "revtrend"
7
+ version = "0.1.0"
8
+ description = "Track numeric metrics per git commit and visualize trends in Grafana"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Simon Holesch", email = "simon@holesch.de" }]
13
+ requires-python = ">=3.11"
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Framework :: FastAPI",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Programming Language :: Python :: 3.14",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ "Topic :: System :: Monitoring",
26
+ ]
27
+ dependencies = [
28
+ "fastapi",
29
+ "httpx",
30
+ "prometheus-client>=0.26.0",
31
+ "pydantic>=2.14.3",
32
+ "PyJWT[crypto]",
33
+ "uvicorn",
34
+ ]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/holesch/revtrend"
38
+ Source = "https://github.com/holesch/revtrend"
39
+ Issues = "https://github.com/holesch/revtrend/issues"
40
+
41
+ [project.scripts]
42
+ revtrend = "revtrend.cli:main"
43
+ revtrend-server = "revtrend.server:main"
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "pytest>=9.2.0",
48
+ "pytest-asyncio>=1.4.1.post1",
49
+ "ruff==0.14.7",
50
+ ]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ asyncio_mode = "auto"
55
+ asyncio_default_fixture_loop_scope = "function"
56
+
57
+ [tool.ruff.lint]
58
+ select = [
59
+ "F", # Pyflakes
60
+ "E", "W", # pycodestyle
61
+ "I", # isort
62
+ "N", # pep8-naming
63
+ "UP", # pyupgrade
64
+ "ASYNC", # flake8-async
65
+ "S", # flake8-bandit
66
+ "B", # flake8-bugbear
67
+ "A", # flake8-builtins
68
+ "C4", # flake8-comprehensions
69
+ "ISC", # flake8-implicit-str-concat
70
+ "LOG", # flake8-logging
71
+ "G", # flake8-logging-format
72
+ "PIE", # flake8-pie
73
+ "T20", # flake8-print
74
+ "PT", # flake8-pytest-style
75
+ "RSE", # flake8-raise
76
+ "SIM", # flake8-simplify
77
+ "TID", # flake8-tidy-imports
78
+ "ARG", # flake8-unused-arguments
79
+ "PTH", # flake8-use-pathlib
80
+ "PL", # Pylint
81
+ "FLY", # flynt
82
+ "FURB", # refurb
83
+ "RUF", # Ruff-specific rules
84
+ ]
85
+ ignore = [
86
+ # "Line too long"
87
+ # -> ruff format handles code, long strings are fine
88
+ "E501",
89
+ # "for loop variable overwritten by assignment target"
90
+ "PLW2901",
91
+ # "Use `from x import y` in lieu of alias"
92
+ # -> style: absolute module imports only, aliased where necessary
93
+ "PLR0402",
94
+ # "open() should be replaced by Path.open()"
95
+ # -> creating a Path object just to open a file is overkill
96
+ "PTH123",
97
+ # "subprocess call: check for execution of untrusted input"
98
+ # -> false positives, git is called with argument lists
99
+ "S603",
100
+ # "Use ternary operator instead of if-else-block"
101
+ # -> can make the code less readable
102
+ "SIM108",
103
+ ]
104
+
105
+ [tool.ruff.lint.per-file-ignores]
106
+ "tests/**.py" = [
107
+ # "Magic value used in comparison"
108
+ "PLR2004",
109
+ # "Use of assert detected"
110
+ "S101",
111
+ # "Starting a process with a partial executable path"
112
+ "S607",
113
+ # "Unused function argument"
114
+ # -> fixtures requested for their side effects
115
+ "ARG001",
116
+ # "Too many arguments in function definition"
117
+ "PLR0913",
118
+ ]
119
+
120
+ [tool.ruff.lint.flake8-tidy-imports]
121
+ ban-relative-imports = "all"
File without changes