Python Library Usage

March 11, 2026 ยท View on GitHub

Good Egg can be used as a Python library to integrate trust scoring into your own applications, bots, or CI pipelines.

Prerequisites

  • Python 3.12 or later
  • A GitHub personal access token (classic or fine-grained) with read access to public repositories

Installation

pip install good-egg

Basic Usage

The main entry point is the score_pr_author async function:

import asyncio
import os

from good_egg import score_pr_author

async def main() -> None:
    result = await score_pr_author(
        login="octocat",
        repo_owner="octocat",
        repo_name="Hello-World",
        token=os.environ["GITHUB_TOKEN"],
    )
    print(f"User: {result.user_login}")
    print(f"Trust level: {result.trust_level}")

    if result.flags.get("scoring_skipped"):
        pr_count = result.scoring_metadata.get("context_repo_merged_pr_count", 0)
        print(f"Scoring skipped -- {pr_count} merged PRs in repo")
    else:
        print(f"Score: {result.normalized_score:.2f}")
        print(f"Merged PRs: {result.total_merged_prs}")
        print(f"Unique repos: {result.unique_repos_contributed}")

asyncio.run(main())

Function Signature

async def score_pr_author(
    login: str,
    repo_owner: str,
    repo_name: str,
    config: GoodEggConfig | None = None,
    token: str | None = None,
    cache: object | None = None,
) -> TrustScore:

Parameters:

ParameterTypeDescription
loginstrGitHub username to score
repo_ownerstrOwner of the context repository
repo_namestrName of the context repository
configGoodEggConfig | NoneCustom configuration; defaults are used when None
tokenstr | NoneGitHub API token; falls back to GITHUB_TOKEN env var
cacheobject | NoneCache instance for response caching (see Cache Usage)

Skipping Known Contributors

By default, score_pr_author checks whether the user already has merged PRs in the target repository. If so, it returns immediately with a trust level of EXISTING_CONTRIBUTOR without running the full scoring pipeline. To force full scoring:

from good_egg import GoodEggConfig, score_pr_author

config = GoodEggConfig(skip_known_contributors=False)
result = await score_pr_author(
    login="octocat",
    repo_owner="octocat",
    repo_name="Hello-World",
    config=config,
)

When scoring is skipped, result.flags["scoring_skipped"] is True and result.scoring_metadata["context_repo_merged_pr_count"] contains the number of merged PRs found.

Custom Configuration

Pass a GoodEggConfig to customize scoring behaviour:

from good_egg import GoodEggConfig, score_pr_author

config = GoodEggConfig(
    thresholds={"high_trust": 0.8, "medium_trust": 0.4},
    graph_scoring={"alpha": 0.9},
    recency={"half_life_days": 90},
)

result = await score_pr_author(
    login="octocat",
    repo_owner="octocat",
    repo_name="Hello-World",
    config=config,
)

Scoring Model Selection

The default model is v3 (Diet Egg). To use an older model, set scoring_model on the config:

from good_egg import GoodEggConfig, score_pr_author

# v3 (default) -- merge rate only
config = GoodEggConfig()

# v2 -- graph + merge rate + account age
config = GoodEggConfig(scoring_model="v2")

# v1 -- graph only
config = GoodEggConfig(scoring_model="v1")

result = await score_pr_author(
    login="octocat",
    repo_owner="octocat",
    repo_name="Hello-World",
    config=config,
)

# v3 and v2 results include component scores
if result.component_scores:
    print(f"Merge rate: {result.component_scores.get('merge_rate')}")

# v3 includes a fresh account advisory
if result.fresh_account and result.fresh_account.is_fresh:
    print(f"Fresh account: {result.fresh_account.account_age_days} days old")

print(f"Scoring model: {result.scoring_model}")

You can also load configuration from a YAML file:

from good_egg.config import load_config
from good_egg import score_pr_author

config = load_config(".good-egg.yml")
result = await score_pr_author(
    login="octocat",
    repo_owner="octocat",
    repo_name="Hello-World",
    config=config,
)

Return Type: TrustScore

The score_pr_author function returns a TrustScore Pydantic model with the following fields:

FieldTypeDescription
user_loginstrGitHub username that was scored
context_repostrRepository used as scoring context
raw_scorefloatPre-normalization score: merge rate (v3), logit (v2), or graph score (v1)
normalized_scorefloatNormalized score (0.0 - 1.0)
trust_levelTrustLevelHIGH, MEDIUM, LOW, UNKNOWN, BOT, or EXISTING_CONTRIBUTOR
account_age_daysintAge of the GitHub account in days
total_merged_prsintTotal number of merged pull requests
unique_repos_contributedintNumber of distinct repositories
top_contributionslist[ContributionSummary]Top repositories contributed to
language_matchboolWhether the user's top language matches the context repo
flagsdict[str, bool]Flags (is_bot, is_new_account, etc.)
scoring_modelstrScoring model used: v1, v2, or v3
component_scoresdict[str, float]Component breakdown (v3: merge_rate; v2: graph_score, merge_rate, log_account_age)
scoring_metadatadict[str, Any]Internal scoring details
fresh_accountFreshAccountAdvisory | NoneAdvisory for accounts under 365 days old (None for bots and existing contributors)

TrustScore is a Pydantic model, so you can serialize it:

# To dict
data = result.model_dump()

# To JSON string
json_str = result.model_dump_json()

Cache Usage

Pass a Cache instance to avoid redundant GitHub API calls across multiple scoring operations:

from good_egg.cache import Cache
from good_egg.config import load_config
from good_egg import score_pr_author

config = load_config()
cache = Cache(ttls=config.cache_ttl.to_seconds())

try:
    result = await score_pr_author(
        login="octocat",
        repo_owner="octocat",
        repo_name="Hello-World",
        config=config,
        cache=cache,
    )
finally:
    cache.close()

The cache is backed by SQLite and persists between runs. Cache TTLs are configured in the cache_ttl section of the configuration file.

Error Handling

Good Egg defines a hierarchy of exceptions in good_egg.exceptions:

from good_egg.exceptions import (
    GoodEggError,
    GitHubAPIError,
    RateLimitExhaustedError,
    UserNotFoundError,
    RepoNotFoundError,
    CacheError,
    ConfigError,
    InsufficientDataError,
)

Exception Hierarchy

GoodEggError (base)
  GitHubAPIError (status_code, rate_limit_remaining)
    RateLimitExhaustedError (reset_at)
    UserNotFoundError (login)
    RepoNotFoundError (repo)
  CacheError
  ConfigError
  InsufficientDataError

Example

from good_egg import score_pr_author
from good_egg.exceptions import (
    RateLimitExhaustedError,
    UserNotFoundError,
)

try:
    result = await score_pr_author(
        login="octocat",
        repo_owner="octocat",
        repo_name="Hello-World",
    )
except UserNotFoundError as exc:
    print(f"User {exc.login} not found")
except RateLimitExhaustedError as exc:
    print(f"Rate limited until {exc.reset_at.isoformat()}")
except GoodEggError as exc:
    print(f"Scoring failed: {exc}")

Async Patterns

score_pr_author is an async function. If you are calling it from synchronous code, use asyncio.run():

import asyncio
from good_egg import score_pr_author

result = asyncio.run(
    score_pr_author(
        login="octocat",
        repo_owner="octocat",
        repo_name="Hello-World",
    )
)

If you already have a running event loop (e.g. inside a web framework), call it directly with await:

result = await score_pr_author(
    login="octocat",
    repo_owner="octocat",
    repo_name="Hello-World",
)