From 0b30034cb8ef53514b3e3767198b4e374d50576d Mon Sep 17 00:00:00 2001 From: widal001 Date: Thu, 30 Jan 2025 11:31:55 -0500 Subject: [PATCH 1/3] fix(analytics): Fixes how issueType was parsed --- .../analytics/integrations/github/validation.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/analytics/src/analytics/integrations/github/validation.py b/analytics/src/analytics/integrations/github/validation.py index abd8fff35..3bba33d06 100644 --- a/analytics/src/analytics/integrations/github/validation.py +++ b/analytics/src/analytics/integrations/github/validation.py @@ -5,6 +5,12 @@ from pydantic import BaseModel, Field, computed_field, model_validator +# Declares class level constants for the fields that need to be aliased +ISSUE_TYPE = "issueType" +CLOSED_AT = "closedAt" +CREATED_AT = "createdAt" +PARENT = "parent" + def safe_default_factory(data: dict, keys_to_replace: list[str]) -> dict: """ @@ -40,19 +46,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 None with default_factory instances - return safe_default_factory(values, ["type", "parent"]) + return safe_default_factory(values, [ISSUE_TYPE, PARENT]) # ############################################# From acb13c627b1bce5ba7e21c977c5dbe2b81cb395f Mon Sep 17 00:00:00 2001 From: widal001 Date: Thu, 30 Jan 2025 13:12:15 -0500 Subject: [PATCH 2/3] test: Fixed test_validation.py --- analytics/tests/integrations/github/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", }, } From 2a07295ec6863f233bd2dac14c2f26198ecf6afd Mon Sep 17 00:00:00 2001 From: widal001 Date: Thu, 30 Jan 2025 16:38:51 -0500 Subject: [PATCH 3/3] refactor: Replaces string literal aliases with constants --- .../integrations/github/validation.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/analytics/src/analytics/integrations/github/validation.py b/analytics/src/analytics/integrations/github/validation.py index 3bba33d06..24fe112d2 100644 --- a/analytics/src/analytics/integrations/github/validation.py +++ b/analytics/src/analytics/integrations/github/validation.py @@ -1,15 +1,29 @@ """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 -# Declares class level constants for the fields that need to be aliased +# 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: @@ -57,7 +71,7 @@ class IssueContent(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.""" # Replace None with default_factory instances return safe_default_factory(values, [ISSUE_TYPE, PARENT]) @@ -70,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 @@ -89,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 @@ -119,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"],