tandemetry 0.0.1__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.
- tandemetry-0.0.1/PKG-INFO +93 -0
- tandemetry-0.0.1/README.md +72 -0
- tandemetry-0.0.1/pyproject.toml +83 -0
- tandemetry-0.0.1/pyproject.toml.orig +61 -0
- tandemetry-0.0.1/src/tandemetry/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/api/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/attribution/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/attribution/catalog.py +85 -0
- tandemetry-0.0.1/src/tandemetry/attribution/signatures.yaml +31 -0
- tandemetry-0.0.1/src/tandemetry/cli.py +40 -0
- tandemetry-0.0.1/src/tandemetry/events/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/ingest/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/metrics/__init__.py +0 -0
- tandemetry-0.0.1/src/tandemetry/settings.py +7 -0
- tandemetry-0.0.1/src/tandemetry/storage/__init__.py +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tandemetry
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Self-hosted analytics for AI coding agents versus human-authored work.
|
|
5
|
+
Keywords: ai-agents,analytics,github,code-review,engineering-metrics
|
|
6
|
+
Author: Yevhenii Korniev
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Topic :: Software Development
|
|
13
|
+
Requires-Dist: pydantic-settings>=2.6
|
|
14
|
+
Requires-Dist: pyyaml>=6
|
|
15
|
+
Requires-Python: >=3.13
|
|
16
|
+
Project-URL: Homepage, https://github.com/tandemetry/tandemetry
|
|
17
|
+
Project-URL: Repository, https://github.com/tandemetry/tandemetry
|
|
18
|
+
Project-URL: Documentation, https://github.com/tandemetry/tandemetry/tree/main/docs
|
|
19
|
+
Project-URL: Issues, https://github.com/tandemetry/tandemetry/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# Tandemetry
|
|
23
|
+
|
|
24
|
+
**See how humans and AI agents ship code together.**
|
|
25
|
+
|
|
26
|
+
Open-source, self-hosted analytics for AI coding agents: compare agent vs human work in your
|
|
27
|
+
repositories — merge rate, reverts, rework, CI failures, review load and cost per PR.
|
|
28
|
+
|
|
29
|
+
> Status: early development (v0.1 in progress). Not ready for production use.
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
AI agents now open a large share of pull requests, but teams can't tell whether that code is
|
|
34
|
+
merged, reverted or rewritten — or which agent works best in their codebase. Tandemetry answers
|
|
35
|
+
that from your own Git data, without sending code anywhere and without LLM calls.
|
|
36
|
+
|
|
37
|
+
## What you will see
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
Every number is traceable: each PR is labelled `agent`, `human` or `mixed` by a transparent rule
|
|
42
|
+
(bot author, commit trailer, PR description, branch name), and the matched rule is stored with the
|
|
43
|
+
label. See [agent attribution](docs/attribution.md) and the
|
|
44
|
+
[signatures research](docs/research/signatures.md).
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
GitHub webhooks and a history backfill feed Kafka. The attribution engine labels each event,
|
|
49
|
+
ClickHouse computes the metrics, and Grafana and a metrics API read them. PostgreSQL holds the
|
|
50
|
+
configuration. Details: [architecture](docs/architecture.md).
|
|
51
|
+
|
|
52
|
+
## Principles
|
|
53
|
+
|
|
54
|
+
- **Self-hosted.** Your code and data stay in your infrastructure.
|
|
55
|
+
- **Agent-agnostic.** Claude Code, Codex, Copilot coding agent, Devin, Cursor and more.
|
|
56
|
+
- **Outcomes, not activity.** Reverts and rework, not just PR counts.
|
|
57
|
+
- **No individual rankings.** Teams, repositories and agents only.
|
|
58
|
+
- **Deterministic.** Transparent attribution rules, no LLM in the core.
|
|
59
|
+
|
|
60
|
+
## Docs
|
|
61
|
+
|
|
62
|
+
- [Product](docs/product.md)
|
|
63
|
+
- [Architecture](docs/architecture.md)
|
|
64
|
+
- [Agent attribution](docs/attribution.md)
|
|
65
|
+
- [Roadmap](docs/roadmap.md)
|
|
66
|
+
- [Releasing](docs/releasing.md)
|
|
67
|
+
- [Architecture decisions](docs/adr/)
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
[Apache-2.0](LICENSE)
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
Requires [uv](https://docs.astral.sh/uv/).
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv sync
|
|
79
|
+
uv run pre-commit install
|
|
80
|
+
uv run pytest
|
|
81
|
+
uv run ruff check . && uv run ruff format --check .
|
|
82
|
+
uv run mypy
|
|
83
|
+
uv run tandemetry --version
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Local service stack (Kafka, ClickHouse, PostgreSQL):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cp deploy/.env.example deploy/.env
|
|
90
|
+
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --wait
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
See [deploy/README.md](deploy/README.md) for verification and troubleshooting.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Tandemetry
|
|
2
|
+
|
|
3
|
+
**See how humans and AI agents ship code together.**
|
|
4
|
+
|
|
5
|
+
Open-source, self-hosted analytics for AI coding agents: compare agent vs human work in your
|
|
6
|
+
repositories — merge rate, reverts, rework, CI failures, review load and cost per PR.
|
|
7
|
+
|
|
8
|
+
> Status: early development (v0.1 in progress). Not ready for production use.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
AI agents now open a large share of pull requests, but teams can't tell whether that code is
|
|
13
|
+
merged, reverted or rewritten — or which agent works best in their codebase. Tandemetry answers
|
|
14
|
+
that from your own Git data, without sending code anywhere and without LLM calls.
|
|
15
|
+
|
|
16
|
+
## What you will see
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
Every number is traceable: each PR is labelled `agent`, `human` or `mixed` by a transparent rule
|
|
21
|
+
(bot author, commit trailer, PR description, branch name), and the matched rule is stored with the
|
|
22
|
+
label. See [agent attribution](docs/attribution.md) and the
|
|
23
|
+
[signatures research](docs/research/signatures.md).
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
GitHub webhooks and a history backfill feed Kafka. The attribution engine labels each event,
|
|
28
|
+
ClickHouse computes the metrics, and Grafana and a metrics API read them. PostgreSQL holds the
|
|
29
|
+
configuration. Details: [architecture](docs/architecture.md).
|
|
30
|
+
|
|
31
|
+
## Principles
|
|
32
|
+
|
|
33
|
+
- **Self-hosted.** Your code and data stay in your infrastructure.
|
|
34
|
+
- **Agent-agnostic.** Claude Code, Codex, Copilot coding agent, Devin, Cursor and more.
|
|
35
|
+
- **Outcomes, not activity.** Reverts and rework, not just PR counts.
|
|
36
|
+
- **No individual rankings.** Teams, repositories and agents only.
|
|
37
|
+
- **Deterministic.** Transparent attribution rules, no LLM in the core.
|
|
38
|
+
|
|
39
|
+
## Docs
|
|
40
|
+
|
|
41
|
+
- [Product](docs/product.md)
|
|
42
|
+
- [Architecture](docs/architecture.md)
|
|
43
|
+
- [Agent attribution](docs/attribution.md)
|
|
44
|
+
- [Roadmap](docs/roadmap.md)
|
|
45
|
+
- [Releasing](docs/releasing.md)
|
|
46
|
+
- [Architecture decisions](docs/adr/)
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
[Apache-2.0](LICENSE)
|
|
51
|
+
|
|
52
|
+
## Development
|
|
53
|
+
|
|
54
|
+
Requires [uv](https://docs.astral.sh/uv/).
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv sync
|
|
58
|
+
uv run pre-commit install
|
|
59
|
+
uv run pytest
|
|
60
|
+
uv run ruff check . && uv run ruff format --check .
|
|
61
|
+
uv run mypy
|
|
62
|
+
uv run tandemetry --version
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Local service stack (Kafka, ClickHouse, PostgreSQL):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cp deploy/.env.example deploy/.env
|
|
69
|
+
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --wait
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
See [deploy/README.md](deploy/README.md) for verification and troubleshooting.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tandemetry"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Self-hosted analytics for AI coding agents versus human-authored work."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
keywords = [
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"analytics",
|
|
10
|
+
"github",
|
|
11
|
+
"code-review",
|
|
12
|
+
"engineering-metrics",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Software Development",
|
|
20
|
+
]
|
|
21
|
+
requires-python = ">=3.13"
|
|
22
|
+
dependencies = [
|
|
23
|
+
"pydantic-settings>=2.6",
|
|
24
|
+
"pyyaml>=6",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[[project.authors]]
|
|
28
|
+
name = "Yevhenii Korniev"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/tandemetry/tandemetry"
|
|
32
|
+
Repository = "https://github.com/tandemetry/tandemetry"
|
|
33
|
+
Documentation = "https://github.com/tandemetry/tandemetry/tree/main/docs"
|
|
34
|
+
Issues = "https://github.com/tandemetry/tandemetry/issues"
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
tandemetry = "tandemetry.cli:main"
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["uv_build>=0.9.4,<0.10"]
|
|
41
|
+
build-backend = "uv_build"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"mypy>=1.13",
|
|
46
|
+
"pre-commit>=4.0",
|
|
47
|
+
"pytest>=8.3",
|
|
48
|
+
"ruff>=0.8",
|
|
49
|
+
"types-pyyaml>=6.0.12.20261103",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
line-length = 100
|
|
54
|
+
target-version = "py313"
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = [
|
|
58
|
+
"E",
|
|
59
|
+
"F",
|
|
60
|
+
"I",
|
|
61
|
+
"UP",
|
|
62
|
+
"B",
|
|
63
|
+
"SIM",
|
|
64
|
+
"N",
|
|
65
|
+
"ANN",
|
|
66
|
+
"S",
|
|
67
|
+
"RUF",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint.per-file-ignores]
|
|
71
|
+
"tests/**" = ["S101"]
|
|
72
|
+
|
|
73
|
+
[tool.mypy]
|
|
74
|
+
strict = true
|
|
75
|
+
files = [
|
|
76
|
+
"src",
|
|
77
|
+
"tests",
|
|
78
|
+
]
|
|
79
|
+
plugins = ["pydantic.mypy"]
|
|
80
|
+
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
83
|
+
addopts = "-ra --strict-markers"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tandemetry"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Self-hosted analytics for AI coding agents versus human-authored work."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
authors = [{ name = "Yevhenii Korniev" }]
|
|
8
|
+
keywords = ["ai-agents", "analytics", "github", "code-review", "engineering-metrics"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python :: 3.13",
|
|
14
|
+
"Topic :: Software Development",
|
|
15
|
+
]
|
|
16
|
+
requires-python = ">=3.13"
|
|
17
|
+
dependencies = [
|
|
18
|
+
"pydantic-settings>=2.6",
|
|
19
|
+
"pyyaml>=6",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/tandemetry/tandemetry"
|
|
24
|
+
Repository = "https://github.com/tandemetry/tandemetry"
|
|
25
|
+
Documentation = "https://github.com/tandemetry/tandemetry/tree/main/docs"
|
|
26
|
+
Issues = "https://github.com/tandemetry/tandemetry/issues"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
tandemetry = "tandemetry.cli:main"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.9.4,<0.10"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"mypy>=1.13",
|
|
38
|
+
"pre-commit>=4.0",
|
|
39
|
+
"pytest>=8.3",
|
|
40
|
+
"ruff>=0.8",
|
|
41
|
+
"types-pyyaml>=6.0.12.20261103",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 100
|
|
46
|
+
target-version = "py313"
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint]
|
|
49
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "N", "ANN", "S", "RUF"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint.per-file-ignores]
|
|
52
|
+
"tests/**" = ["S101"]
|
|
53
|
+
|
|
54
|
+
[tool.mypy]
|
|
55
|
+
strict = true
|
|
56
|
+
files = ["src", "tests"]
|
|
57
|
+
plugins = ["pydantic.mypy"]
|
|
58
|
+
|
|
59
|
+
[tool.pytest.ini_options]
|
|
60
|
+
testpaths = ["tests"]
|
|
61
|
+
addopts = "-ra --strict-markers"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
|
8
|
+
|
|
9
|
+
DEFAULT_CATALOG_PATH = Path(__file__).with_name("signatures.yaml")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Confidence(StrEnum):
|
|
13
|
+
HIGH = "high"
|
|
14
|
+
MEDIUM = "medium"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SignalKind(StrEnum):
|
|
18
|
+
BOT_ACCOUNT = "bot_account"
|
|
19
|
+
COMMIT_TRAILER = "commit_trailer"
|
|
20
|
+
DESCRIPTION_PHRASE = "description_phrase"
|
|
21
|
+
BRANCH_PREFIX = "branch_prefix"
|
|
22
|
+
LABEL = "label"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
RULE_CONFIDENCE: Mapping[SignalKind, Confidence] = {
|
|
26
|
+
SignalKind.BOT_ACCOUNT: Confidence.HIGH,
|
|
27
|
+
SignalKind.COMMIT_TRAILER: Confidence.HIGH,
|
|
28
|
+
SignalKind.DESCRIPTION_PHRASE: Confidence.MEDIUM,
|
|
29
|
+
SignalKind.BRANCH_PREFIX: Confidence.MEDIUM,
|
|
30
|
+
SignalKind.LABEL: Confidence.MEDIUM,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CatalogError(Exception):
|
|
35
|
+
"""Raised when a signatures catalog cannot be read or is invalid."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Signal(BaseModel):
|
|
39
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
40
|
+
|
|
41
|
+
kind: SignalKind
|
|
42
|
+
value: str = Field(min_length=1)
|
|
43
|
+
confidence: Confidence
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def _confidence_follows_rules_table(self) -> Self:
|
|
47
|
+
expected = RULE_CONFIDENCE[self.kind]
|
|
48
|
+
if self.confidence != expected:
|
|
49
|
+
raise ValueError(f"{self.kind} signals must have {expected} confidence")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AgentSignature(BaseModel):
|
|
54
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
55
|
+
|
|
56
|
+
name: str = Field(min_length=1)
|
|
57
|
+
signals: tuple[Signal, ...] = Field(min_length=1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SignatureCatalog(BaseModel):
|
|
61
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
62
|
+
|
|
63
|
+
agents: tuple[AgentSignature, ...] = Field(min_length=1)
|
|
64
|
+
|
|
65
|
+
@model_validator(mode="after")
|
|
66
|
+
def _agent_names_are_unique(self) -> Self:
|
|
67
|
+
names = [agent.name for agent in self.agents]
|
|
68
|
+
duplicates = sorted({name for name in names if names.count(name) > 1})
|
|
69
|
+
if duplicates:
|
|
70
|
+
raise ValueError(f"duplicate agent names: {', '.join(duplicates)}")
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SignatureCatalogLoader:
|
|
75
|
+
"""Loads and validates a signatures catalog from a YAML file."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, path: Path = DEFAULT_CATALOG_PATH) -> None:
|
|
78
|
+
self._path = path
|
|
79
|
+
|
|
80
|
+
def load(self) -> SignatureCatalog:
|
|
81
|
+
try:
|
|
82
|
+
raw = yaml.safe_load(self._path.read_text(encoding="utf-8"))
|
|
83
|
+
return SignatureCatalog.model_validate(raw)
|
|
84
|
+
except (OSError, yaml.YAMLError, ValidationError) as error:
|
|
85
|
+
raise CatalogError(f"invalid signatures catalog {self._path}: {error}") from error
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
agents:
|
|
2
|
+
- name: Claude Code
|
|
3
|
+
signals:
|
|
4
|
+
- {kind: commit_trailer, value: "noreply@anthropic.com", confidence: high}
|
|
5
|
+
- {kind: description_phrase, value: "Generated with Claude Code", confidence: medium}
|
|
6
|
+
- {kind: branch_prefix, value: "claude/", confidence: medium}
|
|
7
|
+
- name: GitHub Copilot coding agent
|
|
8
|
+
signals:
|
|
9
|
+
- {kind: bot_account, value: "Copilot", confidence: high}
|
|
10
|
+
- {kind: bot_account, value: "copilot-swe-agent[bot]", confidence: high}
|
|
11
|
+
- {kind: commit_trailer, value: "198982749+Copilot@users.noreply.github.com", confidence: high}
|
|
12
|
+
- {kind: commit_trailer, value: "223556219+Copilot@users.noreply.github.com", confidence: high}
|
|
13
|
+
- name: Devin
|
|
14
|
+
signals:
|
|
15
|
+
- {kind: bot_account, value: "devin-ai-integration[bot]", confidence: high}
|
|
16
|
+
- name: Cursor
|
|
17
|
+
signals:
|
|
18
|
+
- {kind: bot_account, value: "cursor[bot]", confidence: high}
|
|
19
|
+
- {kind: commit_trailer, value: "cursoragent@cursor.com", confidence: high}
|
|
20
|
+
- {kind: branch_prefix, value: "cursor/", confidence: medium}
|
|
21
|
+
- name: OpenAI Codex
|
|
22
|
+
signals:
|
|
23
|
+
- {kind: commit_trailer, value: "noreply@openai.com", confidence: high}
|
|
24
|
+
- {kind: commit_trailer, value: "codex@openai.com", confidence: high}
|
|
25
|
+
- {kind: branch_prefix, value: "codex/", confidence: medium}
|
|
26
|
+
- name: Google Jules
|
|
27
|
+
signals:
|
|
28
|
+
- {kind: bot_account, value: "google-labs-jules[bot]", confidence: high}
|
|
29
|
+
- name: Open SWE
|
|
30
|
+
signals:
|
|
31
|
+
- {kind: commit_trailer, value: "open-swe@users.noreply.github.com", confidence: high}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from tandemetry.settings import Settings
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VersionProvider(Protocol):
|
|
10
|
+
def get(self) -> str: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PackageVersionProvider:
|
|
14
|
+
def __init__(self, package: str = "tandemetry") -> None:
|
|
15
|
+
self._package = package
|
|
16
|
+
|
|
17
|
+
def get(self) -> str:
|
|
18
|
+
return version(self._package)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Cli:
|
|
22
|
+
def __init__(self, version_provider: VersionProvider, settings: Settings) -> None:
|
|
23
|
+
self._parser = argparse.ArgumentParser(prog=settings.app_name)
|
|
24
|
+
self._parser.add_argument(
|
|
25
|
+
"--version",
|
|
26
|
+
action="version",
|
|
27
|
+
version=f"{settings.app_name} {version_provider.get()}",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def run(self, argv: Sequence[str] | None = None) -> int:
|
|
31
|
+
try:
|
|
32
|
+
self._parser.parse_args(argv)
|
|
33
|
+
except SystemExit as exit_request:
|
|
34
|
+
return exit_request.code if isinstance(exit_request.code, int) else 0
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
cli = Cli(PackageVersionProvider(), Settings())
|
|
40
|
+
raise SystemExit(cli.run())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|