Coverage for src/data_api/domain/departments/services.py: 100%
58 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-13 19:28 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-13 19:28 +0000
1"""
2Departments domain service.
4Handles department and personnel directory data.
5"""
7from typing import Optional
9from thefuzz import fuzz
11from data_api.data.manager import nthudata
12from data_api.data.nthudata import NTHUDataManager
14JSON_PATH = "directory.json"
15FUZZY_SEARCH_THRESHOLD_DEPT = 80
16FUZZY_SEARCH_THRESHOLD_PERSON = 80
17FUZZY_SEARCH_THRESHOLD_PERSON_TITLE = 90
20def _department_match_score(query: str, department: dict, fuzzy: bool) -> int:
21 if not fuzzy:
22 return 100 if query == department["name"] else 0
23 score = fuzz.partial_ratio(query, department["name"])
24 return score if score >= FUZZY_SEARCH_THRESHOLD_DEPT else 0
27def _person_match_score(query: str, person: dict, fuzzy: bool) -> int:
28 if not fuzzy:
29 return 100 if query == person["name"] or query == person.get("title") else 0
30 name_score = fuzz.partial_ratio(query, person["name"])
31 if name_score >= FUZZY_SEARCH_THRESHOLD_PERSON:
32 return name_score
33 title_score = fuzz.partial_ratio(query, person.get("title") or "")
34 return title_score if title_score >= FUZZY_SEARCH_THRESHOLD_PERSON_TITLE else 0
37class DepartmentsService:
38 """Service for department directory operations."""
40 def __init__(self, manager: NTHUDataManager = nthudata):
41 self.manager = manager
43 async def get_all_departments(self) -> tuple[Optional[str], list[dict]]:
44 """Get all departments."""
45 return await self.manager.get(JSON_PATH)
47 async def get_directory(
48 self, query: str | None = None, fuzzy: bool = True
49 ) -> tuple[Optional[str], list[dict]]:
50 """Return matching departments, retaining only matching people when needed."""
51 commit_hash, directory_data = await self.get_all_departments()
52 if not query:
53 return commit_hash, directory_data
55 results = []
56 for department in directory_data:
57 if department_score := _department_match_score(query, department, fuzzy):
58 results.append((department_score, department))
59 continue
61 details = department.get("details", {})
62 matching_people = [
63 (score, person)
64 for person in details.get("people", [])
65 if (score := _person_match_score(query, person, fuzzy))
66 ]
67 if matching_people:
68 results.append(
69 (
70 max(score for score, _ in matching_people),
71 {
72 **department,
73 "details": {
74 **details,
75 "people": [person for _, person in matching_people],
76 },
77 },
78 )
79 )
81 results.sort(key=lambda item: item[0], reverse=True)
82 return commit_hash, [department for _, department in results]
84 async def search_departments_and_people(
85 self, query: str, fuzzy: bool = True
86 ) -> tuple[Optional[str], dict[str, list]]:
87 """Search departments and people using fuzzy or exact matching."""
88 commit_hash, directory_data = await self.manager.get(JSON_PATH)
89 if not query:
90 return commit_hash, {"departments": [], "people": []}
92 dept_results = []
93 person_results = []
95 for department in directory_data:
96 # Search departments
97 if dept_similarity := _department_match_score(query, department, fuzzy):
98 dept_results.append((dept_similarity, department))
100 # Search people in this department
101 people = department.get("details", {}).get("people", [])
102 for person in people:
103 if score := _person_match_score(query, person, fuzzy):
104 person_results.append((score, person))
106 dept_results.sort(key=lambda x: x[0], reverse=True)
107 person_results.sort(key=lambda x: x[0], reverse=True)
109 return commit_hash, {
110 "departments": [dept for _, dept in dept_results],
111 "people": [person for _, person in person_results],
112 }
115# Global service instance
116departments_service = DepartmentsService()