Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/auto-update-db.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ jobs:
if: github.event.label.name == 'request-theme'
env:
ISSUE_AUTHOR_USER_ID: ${{ github.event.issue.user.id }}
run: uv run --no-sync python -m src.queue_eligibility --submission-file submission.json --database-root database
run: uv run --no-sync python -m src.queue_eligibility

- name: Update
id: update
Expand Down
14 changes: 14 additions & 0 deletions auto_approved_users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"user_id": 30657709,
"username": "LuckyNoS7evin"
},
{
"user_id": 42013603,
"username": "ReenigneArcher"
},
{
"user_id": 88998541,
"username": "jjnether"
}
]
202 changes: 55 additions & 147 deletions src/queue_eligibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,10 @@
import os
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlparse
from typing import Iterable


CONTRIBUTION_THRESHOLD = 15
IGDB_HOST = 'igdb.com'
TMDB_HOST = 'themoviedb.org'

CATEGORY_BY_HOST_AND_PATH = {
(IGDB_HOST, 'games'): 'games',
(IGDB_HOST, 'collections'): 'game_collections',
(IGDB_HOST, 'franchises'): 'game_franchises',
(TMDB_HOST, 'movie'): 'movies',
(TMDB_HOST, 'collection'): 'movie_collections',
(TMDB_HOST, 'tv'): 'tv_shows',
}
AUTO_APPROVED_USERS_FILE = 'auto_approved_users.json'


@dataclass(frozen=True)
Expand All @@ -32,169 +21,102 @@ class QueueEligibilityResult:
----------
queue_eligible : bool
Whether the submission should receive the queue label.
category : str
Database category derived from the submission URL.
contribution_count : int
Existing contribution count for the submitter in the derived category.
user_id : str
GitHub user id of the issue author.
reason : str
Machine-readable reason for the eligibility decision.
"""

queue_eligible: bool
category: str
contribution_count: int
user_id: str
reason: str


def get_submission_category(database_url: str) -> str:
def normalize_user_id(user_id: object) -> str:
"""
Return the database category for a submission URL.
Normalize a GitHub user id for matching.

Parameters
----------
database_url : str
Database URL from the issue submission.
user_id : object
GitHub user id to normalize.

Returns
-------
str
Top-level database category directory.

Raises
------
ValueError
If the URL cannot be mapped to a known database category.
"""
parsed_url = urlparse(database_url.strip())
hostname = parsed_url.hostname or ''
hostname = hostname.removeprefix('www.')
path_parts = [part for part in parsed_url.path.split('/') if part]

if not path_parts:
raise ValueError(f'Unsupported database URL: {database_url}')

category = CATEGORY_BY_HOST_AND_PATH.get((hostname, path_parts[0]))
if not category:
raise ValueError(f'Unsupported database URL: {database_url}')

return category


def get_contribution_count(contributor_data: dict, user_id: str) -> int:
"""
Return a contributor's total contributions from contributor metadata.

Parameters
----------
contributor_data : dict
Parsed contents of a category ``contributors.json`` file.
user_id : str
GitHub user ID to look up.

Returns
-------
int
Sum of the contributor's added and edited item counts, or zero when the
metadata is missing or malformed.
Trimmed user id string.
"""
if not isinstance(contributor_data, dict):
return 0
if user_id is None:
return ''

contributor = contributor_data.get(str(user_id), {})
if not isinstance(contributor, dict):
return 0
return str(user_id).strip()

try:
return int(contributor.get('items_added', 0)) + int(contributor.get('items_edited', 0))
except (TypeError, ValueError):
return 0


def load_contribution_count(database_root: Path, category: str, user_id: str) -> int:
def load_auto_approved_user_ids(auto_approved_users_file: Path) -> frozenset[str]:
"""
Load a contributor's category contribution count from disk.
Load auto-approved GitHub user ids from a JSON file.

Parameters
----------
database_root : pathlib.Path
Root directory containing category database folders.
category : str
Category directory to inspect.
user_id : str
GitHub user ID to look up.
auto_approved_users_file : pathlib.Path
JSON file containing auto-approved user objects.

Returns
-------
int
Contributor count for the category, or zero when the contributors file
is absent or unreadable.
frozenset[str]
Normalized GitHub user ids.
"""
contributors_file = database_root / category / 'contributors.json'
if not contributors_file.exists():
return 0

try:
with contributors_file.open() as contributor_f:
contributor_data = json.load(contributor_f)
with auto_approved_users_file.open(encoding='utf-8') as auto_approved_users_f:
auto_approved_users = json.load(auto_approved_users_f)
except (OSError, json.JSONDecodeError):
return 0
return frozenset()

if not isinstance(auto_approved_users, list):
return frozenset()

return get_contribution_count(contributor_data=contributor_data, user_id=user_id)
return frozenset(
normalized_user_id
for user in auto_approved_users
if isinstance(user, dict)
if (normalized_user_id := normalize_user_id(user.get('user_id', '')))
)


def evaluate_queue_eligibility(submission: dict,
database_root: Path,
user_id: str,
threshold: int = CONTRIBUTION_THRESHOLD) -> QueueEligibilityResult:
def evaluate_queue_eligibility(user_id: object,
auto_approved_user_ids: Iterable[object]) -> QueueEligibilityResult:
"""
Evaluate whether a submission is eligible to be queued automatically.

Parameters
----------
submission : dict
Parsed issue submission data.
database_root : pathlib.Path
Root directory containing category database folders.
user_id : str
GitHub user ID of the issue author.
threshold : int, optional
Minimum prior category contribution count that must be exceeded.
user_id : object
GitHub user id of the issue author.
auto_approved_user_ids : Iterable[object]
GitHub user ids eligible for automatic queueing.

Returns
-------
QueueEligibilityResult
Eligibility decision and supporting metadata.
"""
if not user_id:
normalized_user_id = normalize_user_id(user_id=user_id)
if not normalized_user_id:
return QueueEligibilityResult(
queue_eligible=False,
category='',
contribution_count=0,
user_id='',
reason='missing-user-id',
)

try:
category = get_submission_category(database_url=submission['database_url'])
except (KeyError, ValueError):
return QueueEligibilityResult(
queue_eligible=False,
category='',
contribution_count=0,
reason='unsupported-category',
)

contribution_count = load_contribution_count(
database_root=database_root,
category=category,
user_id=user_id,
)
queue_eligible = contribution_count > threshold
reason = 'eligible' if queue_eligible else 'below-threshold'
queue_eligible = normalized_user_id in {
normalize_user_id(user_id=auto_approved_user_id)
for auto_approved_user_id in auto_approved_user_ids
}
reason = 'auto-approved-user' if queue_eligible else 'not-auto-approved-user'

return QueueEligibilityResult(
queue_eligible=queue_eligible,
category=category,
contribution_count=contribution_count,
user_id=normalized_user_id,
reason=reason,
)

Expand All @@ -214,8 +136,7 @@ def write_github_outputs(result: QueueEligibilityResult) -> None:
"""
outputs = {
'queue_eligible': str(result.queue_eligible).lower(),
'category': result.category,
'contribution_count': str(result.contribution_count),
'user_id': result.user_id,
'reason': result.reason,
}
output_lines = [f'{key}={value}' for key, value in outputs.items()]
Expand All @@ -239,9 +160,7 @@ def parse_args() -> argparse.Namespace:
Parsed command-line arguments.
"""
parser = argparse.ArgumentParser(description='Check whether a submission is eligible for queueing.')
parser.add_argument('--submission-file', default='submission.json')
parser.add_argument('--database-root', default='database')
parser.add_argument('--threshold', type=int, default=CONTRIBUTION_THRESHOLD)
parser.add_argument('--auto-approved-users-file', default=AUTO_APPROVED_USERS_FILE)
parser.add_argument('--user-id', default=os.environ.get('ISSUE_AUTHOR_USER_ID', ''))
return parser.parse_args()

Expand All @@ -255,24 +174,13 @@ def main() -> None:
None
"""
args = parse_args()

try:
with open(args.submission_file) as submission_f:
submission = json.load(submission_f)
except (OSError, json.JSONDecodeError):
result = QueueEligibilityResult(
queue_eligible=False,
category='',
contribution_count=0,
reason='submission-read-error',
)
else:
result = evaluate_queue_eligibility(
submission=submission,
database_root=Path(args.database_root),
user_id=args.user_id,
threshold=args.threshold,
)
auto_approved_user_ids = load_auto_approved_user_ids(
auto_approved_users_file=Path(args.auto_approved_users_file),
)
result = evaluate_queue_eligibility(
user_id=args.user_id,
auto_approved_user_ids=auto_approved_user_ids,
)
write_github_outputs(result=result)


Expand Down
Loading