From 2994df095473a4787f5e21813dd0aab7ff61cb98 Mon Sep 17 00:00:00 2001 From: Billy Daly Date: Fri, 31 Jan 2025 10:58:42 -0500 Subject: [PATCH] [Issue #3730] Fix parsing `issueType` bug (#3731) Fixes #3730 - Fixes typo in `IssueType` field alias (it was incorrectly aliased as `"type"` instead of `"issueType"`) that caused a parsing issue. - Replaces the string literal field aliases with module level constants to make it easier to update and monitor field aliases. --- .../integrations/github/validation.py | 39 ++++++++++++++----- .../integrations/github/test_validation.py | 2 +- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/analytics/src/analytics/integrations/github/validation.py b/analytics/src/analytics/integrations/github/validation.py index abd8fff35..24fe112d2 100644 --- a/analytics/src/analytics/integrations/github/validation.py +++ b/analytics/src/analytics/integrations/github/validation.py @@ -1,10 +1,30 @@ """Pydantic schemas for validating GitHub API responses.""" # pylint: disable=no-self-argument +# mypy: disable-error-code="literal-required" +# This mypy disable is needed so we can use constants for field aliases + from datetime import datetime, timedelta from pydantic import BaseModel, Field, computed_field, model_validator +# Declare constants for the fields that need to be aliased from the GitHub data +# so that we only have to change these values in one place. +# +# We need to declare them at the module-level instead of class-level +# because pydantic throws an error when using class level constants. + +# Issue content aliases +ISSUE_TYPE = "issueType" +CLOSED_AT = "closedAt" +CREATED_AT = "createdAt" +PARENT = "parent" +# Iteration aliases +ITERATION_ID = "iterationId" +START_DATE = "startDate" +# Single select aliases +OPTION_ID = "optionId" + def safe_default_factory(data: dict, keys_to_replace: list[str]) -> dict: """ @@ -40,19 +60,20 @@ class IssueType(BaseModel): class IssueContent(BaseModel): """Schema for core issue metadata.""" + # The fields that we're parsing from the raw GitHub output title: str url: str closed: bool - created_at: str = Field(alias="createdAt") - closed_at: str | None = Field(alias="closedAt", default=None) - issue_type: IssueType = Field(alias="type", default_factory=IssueType) + created_at: str = Field(alias=CREATED_AT) + closed_at: str | None = Field(alias=CLOSED_AT, default=None) + issue_type: IssueType = Field(alias=ISSUE_TYPE, default_factory=IssueType) parent: IssueParent = Field(default_factory=IssueParent) @model_validator(mode="before") def replace_none_with_defaults(cls, values) -> dict: # noqa: ANN001, N805 - """Replace None with default_factory instances.""" + """Replace keys that are set to None with default_factory instances.""" # Replace None with default_factory instances - return safe_default_factory(values, ["type", "parent"]) + return safe_default_factory(values, [ISSUE_TYPE, PARENT]) # ############################################# @@ -63,9 +84,9 @@ def replace_none_with_defaults(cls, values) -> dict: # noqa: ANN001, N805 class IterationValue(BaseModel): """Schema for iteration field values like Sprint or Quad.""" - iteration_id: str | None = Field(alias="iterationId", default=None) + iteration_id: str | None = Field(alias=ITERATION_ID, default=None) title: str | None = None - start_date: str | None = Field(alias="startDate", default=None) + start_date: str | None = Field(alias=START_DATE, default=None) duration: int | None = None @computed_field @@ -82,7 +103,7 @@ def end_date(self) -> str | None: class SingleSelectValue(BaseModel): """Schema for single select field values like Status or Pillar.""" - option_id: str | None = Field(alias="optionId", default=None) + option_id: str | None = Field(alias=OPTION_ID, default=None) name: str | None = None @@ -112,7 +133,7 @@ class ProjectItem(BaseModel): @model_validator(mode="before") def replace_none_with_defaults(cls, values) -> dict: # noqa: ANN001, N805 - """Replace None with default_factory instances.""" + """Replace keys that are set to None with default_factory instances.""" return safe_default_factory( values, ["sprint", "points", "quad", "pillar", "status"], diff --git a/analytics/tests/integrations/github/test_validation.py b/analytics/tests/integrations/github/test_validation.py index 7f9dadee7..24543bb32 100644 --- a/analytics/tests/integrations/github/test_validation.py +++ b/analytics/tests/integrations/github/test_validation.py @@ -24,7 +24,7 @@ "title": "Test Parent", "url": "https://github.com/test/repo/issues/2", }, - "type": { + "issueType": { "name": "Bug", }, }