angeetoile commited on
Commit
b60327d
·
1 Parent(s): 41a91ba

fix(opportunities): normalize unknown categories and support programs

Browse files
Files changed (1) hide show
  1. 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