diff --git a/CHANGELOG.md b/CHANGELOG.md index cd26941..ce70a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## [Unreleased] +* Add: new `active` status under `ReviewModel` which is set to `False` if the `"archived"` label is present on a review to mark the package as inactive (@banesullivan) + [v1.4] - 2024-11-22 Notes: it looks like i may have mistakenly bumped to 1.3.7 in august. rather than try to fix on pypi we will just go with it to ensure our release cycles are smooth given no one else uses this package except pyopensci. diff --git a/src/pyosmeta/models/base.py b/src/pyosmeta/models/base.py index 63a4a18..2beb88d 100644 --- a/src/pyosmeta/models/base.py +++ b/src/pyosmeta/models/base.py @@ -279,6 +279,7 @@ class ReviewModel(BaseModel): partners: Optional[list[Partnerships]] = None gh_meta: Optional[GhMeta] = None labels: list[str] = Field(default_factory=list) + active: bool = True # To indicate if package is maintained or archived @field_validator( "date_accepted", diff --git a/src/pyosmeta/models/github.py b/src/pyosmeta/models/github.py index 4a35c86..6f2c438 100644 --- a/src/pyosmeta/models/github.py +++ b/src/pyosmeta/models/github.py @@ -17,9 +17,10 @@ from __future__ import annotations from datetime import datetime +from enum import Enum from typing import Any, List, Literal, Optional, Union -from pydantic import AnyUrl, BaseModel, ConfigDict, Field +from pydantic import AnyUrl, BaseModel, ConfigDict, Field, model_validator class User(BaseModel): @@ -61,14 +62,45 @@ class ClosedBy(User): ... class Owner(User): ... +class LabelType(str, Enum): + """Enum for the different types of labels that can be assigned to an issue. + + This enum is not meant to be exhaustive, but rather capture a few important + labels for life cycle of approved reviews. + + For now, this only includes the "archived" label, which is used to mark + packages that are no longer maintained ("inactive"). The "archived" label + corresponds to setting ``active=False`` on the ReviewModel + """ + + ARCHIVED = "archived" + + class Labels(BaseModel): + name: str id: Optional[int] = None node_id: Optional[str] = None url: Optional[AnyUrl] = None - name: Optional[str] = None description: Optional[str] = None color: Optional[str] = None default: Optional[bool] = None + type: Optional[LabelType] = None + + @model_validator(mode="before") + def parse_label_type(cls, data): + """Parse the label type from the name before validation. + + This will parse the label name into an available LabelType enum value. + Not all labels will have a corresponding LabelType, so this will + gracefully fail. This was implemented for assigning the LabelType.ARCHIVED + value to the "archived" label so that we can easily filter out archived + issues. + """ + try: + data["type"] = LabelType(data["name"]) + except ValueError: + pass + return data class Issue(BaseModel): diff --git a/src/pyosmeta/parse_issues.py b/src/pyosmeta/parse_issues.py index 5e1696e..1b8372c 100644 --- a/src/pyosmeta/parse_issues.py +++ b/src/pyosmeta/parse_issues.py @@ -7,7 +7,7 @@ from pydantic import ValidationError from pyosmeta.models import ReviewModel, ReviewUser -from pyosmeta.models.github import Issue +from pyosmeta.models.github import Issue, Labels, LabelType from .github_api import GitHubAPI from .utils_clean import clean_date_accepted_key @@ -221,6 +221,31 @@ def _postprocess_meta(self, meta: dict, body: List[str]) -> dict: return meta + def _postprocess_labels(self, meta: dict) -> dict: + """ + Process specific labels for attributes in the review model. + + Presently, this method only checks if the review has the "archived" + (LabelType.ARCHIVED) label and sets the active attribute to False + if it does. We may add more label processing in the future. + + The intention behind this is to assign specific ReviewModel attributes + based on the presence of certain labels in the review issue. + """ + + def _is_archived(label: str | Labels) -> bool: + """Internal helper to check if a label is the "archived" label""" + if isinstance(label, Labels): + return label.type == LabelType.ARCHIVED + return "archived" in label.lower() + + # Check if the review has the "archived" label + if "labels" in meta and [ + label for label in meta["labels"] if _is_archived(label) + ]: + meta["active"] = False + return meta + def _parse_field(self, key: str, val: str) -> Any: """ Method dispatcher for parsing specific header fields. @@ -277,6 +302,7 @@ def parse_issue(self, issue: Issue | str) -> ReviewModel: # Finalize review model before casting model = self._postprocess_meta(model, body) + model = self._postprocess_labels(model) return ReviewModel(**model) diff --git a/tests/integration/test_parse_issues.py b/tests/integration/test_parse_issues.py index 237ebfe..6eecf8d 100644 --- a/tests/integration/test_parse_issues.py +++ b/tests/integration/test_parse_issues.py @@ -69,3 +69,28 @@ def test_parse_labels(issue_list, process_issues): issue.labels = labels review = process_issues.parse_issue(issue) assert review.labels == ["6/pyOS-approved", "another_label"] + assert review.active + + # Now add an archive label + label_inst = Labels( + id=1196238794, + node_id="MDU6TGFiZWwxMTk2MjM4Nzk0", + url="https://api.github.com/repos/pyOpenSci/software-submission/labels/archived", + name="archived", + description="", + color="006B75", + default=False, + ) + labels = [label_inst, "another_label"] + for issue in issue_list: + issue.labels = labels + review = process_issues.parse_issue(issue) + assert not review.active + + # Handle label with missing details + label_inst = Labels(name="test") + labels = [label_inst, "another_label"] + for issue in issue_list: + issue.labels = labels + review = process_issues.parse_issue(issue) + assert review.labels == ["test", "another_label"]