repomatic.metrics.forge module

Read a repository’s metrics from whichever forge hosts it.

Answers one question for one repository: how many accounts follow it, when it was created, when it last shipped, and when it was last touched. GitHub, GitLab (on any instance) and Forgejo or Gitea (likewise) each expose that through a different API, and repo_metrics() picks the right one from the URL’s host.

One call per repository on every forge, which is what lets a single sampler collect every metric repomatic.metrics.store records rather than one call per metric family.

class repomatic.metrics.forge.Forge(*values)[source]

Bases: Enum

Forge software a host runs, which owns how to read its API.

Supporting a fourth forge means a new member plus its entry in REST_ENDPOINTS, which keeps the config-side validation and the dispatch from drifting: forge_of() resolves declared values through the member lookup, so a [tool.repomatic.metrics] forges entry naming anything else fails there rather than at sampling time.

FORGEJO = 'forgejo'
GITHUB = 'github'
GITLAB = 'gitlab'
metrics(host, path)[source]

Read one repository through this forge’s API.

Parameters:
  • host (str) – The instance’s hostname. Unused on GitHub, whose one host gh already addresses.

  • path (str) – The repository’s owner/name or namespace path.

Return type:

ForgeMetrics | None

Returns:

The repository’s metrics, or None when unreadable.

Raises:

RuntimeError – When a GitHub call fails.

class repomatic.metrics.forge.RestEndpoints(repository, quote_path, stars, releases, release_date, tags, tag_date, commits, commit_date)[source]

Bases: object

Where the REST API of a forge answers each reading of one repository.

GitLab and Forgejo answer the same four questions in four calls each: the repository object, then the newest release, tag and commit. Only the paths and the field names differ, so one reader, rest_metrics(), serves both through this table.

repository: str

URL of the repository object, with {host} and {path} fields.

quote_path: bool

Whether the path travels URL-encoded, as one segment.

stars: str

Field of the repository object holding its star count.

releases: str

Suffix of the endpoint listing the newest release first, one per page.

release_date: tuple[str, ...]

Keys leading to the date of a listed release.

tags: str

Suffix of the endpoint listing the newest tag first, one per page.

tag_date: tuple[str, ...]

Keys leading to the date of a listed tag.

commits: str

Suffix of the endpoint listing the newest commit first, one per page.

commit_date: tuple[str, ...]

Keys leading to the date of a listed commit.

repomatic.metrics.forge.FORGE_APIS: dict[str, Forge] = {'codeberg.org': Forge.FORGEJO, 'github.com': Forge.GITHUB, 'gitlab.com': Forge.GITLAB}

Forge software each known host runs, which is what selects the API to call.

Never guessed from the host name: an unknown host raises instead, so a subject landing on a fourth kind of forge has to declare how to read it rather than silently sampling nothing. Extend it through [tool.repomatic.metrics] forges, which a repository uses to name the self-hosted instances it tracks (salsa.debian.org runs GitLab, gitlab.archlinux.org too).

repomatic.metrics.forge.GITHUB_HOST = 'github.com'

The one host whose deep collector exists.

The star-curve reconstruction reads a star-history endpoint only GitHub serves, so a subject elsewhere is skipped by it rather than failed.

repomatic.metrics.forge.GITHUB_METRICS_QUERY = '\nquery($owner: String!, $name: String!) {\n  repository(owner: $owner, name: $name) {\n    createdAt\n    stargazerCount\n    latestRelease { publishedAt }\n    defaultBranchRef { target { ... on Commit { committedDate } } }\n    refs(refPrefix: "refs/tags/", first: 1,\n         orderBy: {field: TAG_COMMIT_DATE, direction: DESC}) {\n      nodes {\n        target {\n          ... on Commit { committedDate }\n          ... on Tag { target { ... on Commit { committedDate } } }\n        }\n      }\n    }\n  }\n}\n'

Reads a repository’s whole metric set in one call.

One call where REST needs four, and correct where REST is not: /tags answers in an order nobody should assume, so a fallback trusting it can date a live project a decade into the past. Ordering on TAG_COMMIT_DATE states the question instead of hoping the default matches it.

The commit date is read off the default branch rather than from the repository’s pushedAt, which any push to any branch bumps.

class repomatic.metrics.forge.ForgeMetrics(stars, created=None, release=None, release_source=None, commit=None)[source]

Bases: object

One repository’s metrics, as any forge reports them.

stars: int

Count of accounts following the repository on its own forge.

created: str | None = None

ISO date the repository was opened.

The one date a star count is known to be zero, which is what gives a history an origin and a by-age chart something to align on.

release: str | None = None

ISO date of the newest release or tag, None when a project has neither.

release_source: str | None = None

Where release came from: a release object, or a bare tag.

Recorded because the two are not the same claim. A release is something the project announced; a tag is only the newest thing it labelled, which is the closest available answer for the many projects that never cut a release.

commit: str | None = None

ISO date of the newest commit on the default branch.

The half of the activity reading that stays true for a rolling repository. A widely used package archive can go a decade without tagging a release while being committed to several times a day: a release date alone would report it as long dead.

readings()[source]

Yield each metric this reading carries, as (metric id, value).

The bridge between a typed forge answer and the metric store, which holds every value as text. A metric the forge did not answer yields nothing rather than an empty string, so a project with no release adds no row instead of a blank one.

created is deliberately absent: it is not a metric but the origin of one, recorded by the sampler as a stars reading of zero on that date.

Return type:

Iterator[tuple[str, str]]

repomatic.metrics.forge.split_repo_url(url)[source]

Split a repository URL into its host and its owner/name path.

Parameters:

url (str) – An https://host/owner/name repository URL.

Return type:

tuple[str, str]

Returns:

The (host, path) pair.

Raises:

ValueError – When the URL carries no host or no owner/name path.

repomatic.metrics.forge.canonical_url(subject)[source]

Normalize a configured subject into the URL the store keys on.

A bare owner/name is GitHub, which is what a repository declaring a handful of peers writes. Anything else is already a URL and only needs its trailing decoration removed. One spelling in the store keeps a subject addressable whichever way its configuration named it.

Parameters:

subject (str) – An owner/name slug or a full repository URL.

Return type:

str

Returns:

The canonical https://host/owner/name URL.

Raises:

ValueError – When neither shape parses.

repomatic.metrics.forge.forge_of(url, extra_forges=None)[source]

Name the forge software running the host of url.

Parameters:
  • url (str) – An https://host/owner/name repository URL.

  • extra_forges (Mapping[str, str] | None) – Host-to-forge entries a repository declared for the self-hosted instances it tracks, merged over FORGE_APIS.

Return type:

Forge

Returns:

The Forge the host runs.

Raises:

ValueError – When the host is not declared anywhere, which is deliberate: a silently unsampled subject is worse than a loud one.

repomatic.metrics.forge.newest_dated(release, tag)[source]

Pick whichever of a project’s newest release and newest tag is more recent.

Not a preference for releases: plenty of projects carry a tag newer than their latest release object, some by close to a year, so always reading the release would report them as idle. ISO dates compare as strings, which is the whole of the arithmetic here.

Parameters:
  • release (str | None) – ISO date of the newest release, or None.

  • tag (str | None) – ISO date of the newest tag, or None.

Return type:

tuple[str | None, str | None]

Returns:

The (date, source) pair, both None when a project has neither.

repomatic.metrics.forge.forge_json(url)[source]

Read one JSON document from a forge’s public API.

Covers every forge but GitHub, whose authentication gh already carries. The instances read here (GitLab and Forgejo) serve their project metadata to anonymous callers, so no token is involved and none is asked for.

The request identifies itself with repomatic.http.USER_AGENT, like every fetch of repomatic.http. Several self-hosted GitLab instances answer a browser identity with an HTML page instead of the JSON payload, and nothing then errors: the subject only goes missing from the readings.

Parameters:

url (str) – The API endpoint to read.

Return type:

Any | None

Returns:

The parsed payload, or None when the call or the parse failed.

repomatic.metrics.forge.github_metrics(path)[source]

Read a GitHub repository through GITHUB_METRICS_QUERY.

Parameters:

path (str) – The repository’s owner/name path.

Return type:

ForgeMetrics

Returns:

The repository’s metrics.

Raises:

RuntimeError – When the gh call fails.

repomatic.metrics.forge.REST_ENDPOINTS: dict[Forge, RestEndpoints] = {Forge.FORGEJO: RestEndpoints(repository='https://{host}/api/v1/repos/{path}', quote_path=False, stars='stars_count', releases='/releases?limit=1', release_date=('published_at',), tags='/tags?limit=1', tag_date=('commit', 'created'), commits='/commits?limit=1', commit_date=('commit', 'committer', 'date')), Forge.GITLAB: RestEndpoints(repository='https://{host}/api/v4/projects/{path}', quote_path=True, stars='star_count', releases='/releases?per_page=1', release_date=('released_at',), tags='/repository/tags?per_page=1&order_by=updated&sort=desc', tag_date=('commit', 'created_at'), commits='/repository/commits?per_page=1', commit_date=('committed_date',))}

How to read each forge served over REST, which is every forge but GitHub.

Gitea instances read as Forgejo, whose API they share.

repomatic.metrics.forge.rest_metrics(endpoints, host, path)[source]

Read a repository through the REST API of a forge, on any instance.

Parameters:
  • endpoints (RestEndpoints) – Where that forge answers each reading.

  • host (str) – The instance’s hostname.

  • path (str) – The repository’s owner/name or namespace path.

Return type:

ForgeMetrics | None

Returns:

The repository’s metrics, or None when unreadable.

repomatic.metrics.forge.repo_metrics(url, extra_forges=None)[source]

Read one repository, through whichever API its host speaks.

Parameters:
  • url (str) – An https://host/owner/name repository URL.

  • extra_forges (Mapping[str, str] | None) – Host-to-forge entries for self-hosted instances.

Return type:

ForgeMetrics | None

Returns:

The repository’s metrics, or None when the forge could not be read.

Raises: