Commit ·
b60327d
1
Parent(s): 41a91ba
fix(opportunities): normalize unknown categories and support programs
Browse files- app/schemas/opportunity.py +61 -1
app/schemas/opportunity.py
CHANGED
|
@@ -8,6 +8,7 @@ from pydantic import (
|
|
| 8 |
ConfigDict,
|
| 9 |
Field,
|
| 10 |
HttpUrl,
|
|
|
|
| 11 |
)
|
| 12 |
|
| 13 |
|
|
@@ -20,6 +21,7 @@ class OpportunityCategory(StrEnum):
|
|
| 20 |
HACKATHON = "hackathon"
|
| 21 |
GRANT = "grant"
|
| 22 |
JOB = "job"
|
|
|
|
| 23 |
OTHER = "other"
|
| 24 |
|
| 25 |
|
|
@@ -29,12 +31,62 @@ class OpportunityStatus(StrEnum):
|
|
| 29 |
EXPIRED = "expired"
|
| 30 |
ARCHIVED = "archived"
|
| 31 |
REJECTED = "rejected"
|
| 32 |
-
|
|
|
|
| 33 |
class OpportunitySort(StrEnum):
|
| 34 |
RECENT = "recent"
|
| 35 |
DEADLINE = "deadline"
|
| 36 |
FEATURED = "featured"
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
class SourceRead(BaseModel):
|
| 39 |
model_config = ConfigDict(from_attributes=True)
|
| 40 |
|
|
@@ -70,6 +122,14 @@ class OpportunityListItem(BaseModel):
|
|
| 70 |
verified_at: datetime | None
|
| 71 |
source: SourceRead
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
class OpportunityDetail(OpportunityListItem):
|
| 75 |
description: str | None
|
|
|
|
| 8 |
ConfigDict,
|
| 9 |
Field,
|
| 10 |
HttpUrl,
|
| 11 |
+
field_validator,
|
| 12 |
)
|
| 13 |
|
| 14 |
|
|
|
|
| 21 |
HACKATHON = "hackathon"
|
| 22 |
GRANT = "grant"
|
| 23 |
JOB = "job"
|
| 24 |
+
PROGRAM = "program"
|
| 25 |
OTHER = "other"
|
| 26 |
|
| 27 |
|
|
|
|
| 31 |
EXPIRED = "expired"
|
| 32 |
ARCHIVED = "archived"
|
| 33 |
REJECTED = "rejected"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
class OpportunitySort(StrEnum):
|
| 37 |
RECENT = "recent"
|
| 38 |
DEADLINE = "deadline"
|
| 39 |
FEATURED = "featured"
|
| 40 |
|
| 41 |
+
|
| 42 |
+
CATEGORY_ALIASES: dict[str, OpportunityCategory] = {
|
| 43 |
+
# Français
|
| 44 |
+
"bourse": OpportunityCategory.SCHOLARSHIP,
|
| 45 |
+
"stage": OpportunityCategory.INTERNSHIP,
|
| 46 |
+
"formation": OpportunityCategory.TRAINING,
|
| 47 |
+
"concours": OpportunityCategory.COMPETITION,
|
| 48 |
+
"subvention": OpportunityCategory.GRANT,
|
| 49 |
+
"emploi": OpportunityCategory.JOB,
|
| 50 |
+
"programme": OpportunityCategory.PROGRAM,
|
| 51 |
+
|
| 52 |
+
# Variantes anglaises
|
| 53 |
+
"scholarships": OpportunityCategory.SCHOLARSHIP,
|
| 54 |
+
"internships": OpportunityCategory.INTERNSHIP,
|
| 55 |
+
"fellowships": OpportunityCategory.FELLOWSHIP,
|
| 56 |
+
"trainings": OpportunityCategory.TRAINING,
|
| 57 |
+
"competitions": OpportunityCategory.COMPETITION,
|
| 58 |
+
"hackathons": OpportunityCategory.HACKATHON,
|
| 59 |
+
"grants": OpportunityCategory.GRANT,
|
| 60 |
+
"jobs": OpportunityCategory.JOB,
|
| 61 |
+
"programs": OpportunityCategory.PROGRAM,
|
| 62 |
+
"programme": OpportunityCategory.PROGRAM,
|
| 63 |
+
"programmes": OpportunityCategory.PROGRAM,
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def normalize_opportunity_category(
|
| 68 |
+
value: Any,
|
| 69 |
+
) -> OpportunityCategory:
|
| 70 |
+
if isinstance(value, OpportunityCategory):
|
| 71 |
+
return value
|
| 72 |
+
|
| 73 |
+
if not isinstance(value, str):
|
| 74 |
+
return OpportunityCategory.OTHER
|
| 75 |
+
|
| 76 |
+
normalized_value = value.strip().lower()
|
| 77 |
+
|
| 78 |
+
if not normalized_value:
|
| 79 |
+
return OpportunityCategory.OTHER
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
return OpportunityCategory(normalized_value)
|
| 83 |
+
except ValueError:
|
| 84 |
+
return CATEGORY_ALIASES.get(
|
| 85 |
+
normalized_value,
|
| 86 |
+
OpportunityCategory.OTHER,
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
class SourceRead(BaseModel):
|
| 91 |
model_config = ConfigDict(from_attributes=True)
|
| 92 |
|
|
|
|
| 122 |
verified_at: datetime | None
|
| 123 |
source: SourceRead
|
| 124 |
|
| 125 |
+
@field_validator("category", mode="before")
|
| 126 |
+
@classmethod
|
| 127 |
+
def validate_category(
|
| 128 |
+
cls,
|
| 129 |
+
value: Any,
|
| 130 |
+
) -> OpportunityCategory:
|
| 131 |
+
return normalize_opportunity_category(value)
|
| 132 |
+
|
| 133 |
|
| 134 |
class OpportunityDetail(OpportunityListItem):
|
| 135 |
description: str | None
|