|
1 | | -# utils/data_loader.py |
2 | | -# Handles all reading and lookup of project data from the JSON file. |
3 | | - |
4 | 1 | import json |
5 | 2 | import os |
| 3 | +import threading |
6 | 4 |
|
7 | | -# Build the path to projects.json relative to this file's location |
8 | 5 | DATA_FILE = os.path.join(os.path.dirname(__file__), "..", "data", "projects.json") |
9 | 6 |
|
| 7 | +_projects_cache = None |
| 8 | +_cache_lock = threading.Lock() |
| 9 | + |
10 | 10 |
|
11 | 11 | def load_all_projects(): |
12 | | - """Read and return the full list of projects from the JSON file.""" |
13 | | - with open(DATA_FILE, "r", encoding="utf-8") as f: |
14 | | - return json.load(f) |
| 12 | + """Read and return the full list of projects from the JSON file. |
| 13 | +
|
| 14 | + Results are cached in memory after the first read so subsequent calls |
| 15 | + do not hit the filesystem. |
| 16 | + """ |
| 17 | + global _projects_cache |
| 18 | + if _projects_cache is not None: |
| 19 | + return _projects_cache |
| 20 | + with _cache_lock: |
| 21 | + if _projects_cache is None: |
| 22 | + with open(DATA_FILE, "r", encoding="utf-8") as f: |
| 23 | + _projects_cache = json.load(f) |
| 24 | + return _projects_cache |
15 | 25 |
|
16 | 26 |
|
17 | 27 | def find_project_by_id(project_id): |
18 | | - """ |
19 | | - Return the project dict whose 'id' matches the given integer. |
20 | | - Returns None if no match is found. |
21 | | - """ |
| 28 | + """Return the project whose 'id' matches project_id, or None.""" |
22 | 29 | for project in load_all_projects(): |
23 | 30 | if project.get("id") == project_id: |
24 | 31 | return project |
25 | 32 | return None |
26 | 33 |
|
| 34 | + |
27 | 35 | def get_project_stats(): |
28 | | - """ |
29 | | - Calculate and return statistics about the projects. |
30 | | - Returns: { total_projects, unique_skills, beginner_friendly } |
31 | | - """ |
| 36 | + """Return total_projects, unique_skills, and beginner_friendly counts.""" |
32 | 37 | projects = load_all_projects() |
33 | | - total_projects = len(projects) |
34 | 38 |
|
35 | | - # Collect all unique skills |
36 | 39 | all_skills = set() |
| 40 | + beginner_friendly = 0 |
37 | 41 | for p in projects: |
38 | 42 | for s in p.get("skills", []): |
39 | 43 | all_skills.add(s) |
40 | | - unique_skills = len(all_skills) |
41 | | - |
42 | | - # Count beginner projects |
43 | | - beginner_friendly = len([p for p in projects if p.get("level") == "Beginner"]) |
| 44 | + if p.get("level") == "Beginner": |
| 45 | + beginner_friendly += 1 |
44 | 46 |
|
45 | 47 | return { |
46 | | - "total_projects": total_projects, |
47 | | - "unique_skills": unique_skills, |
48 | | - "beginner_friendly": beginner_friendly |
| 48 | + "total_projects": len(projects), |
| 49 | + "unique_skills": len(all_skills), |
| 50 | + "beginner_friendly": beginner_friendly, |
49 | 51 | } |
50 | 52 |
|
51 | | -# Cache for loaded projects |
52 | | -_projects_cache = None |
53 | | - |
54 | 53 |
|
55 | 54 | def clear_cache(): |
56 | | - """Reset the in-memory project cache.""" |
| 55 | + """Reset the in-memory project cache (used in tests).""" |
57 | 56 | global _projects_cache |
58 | | - _projects_cache = None |
| 57 | + with _cache_lock: |
| 58 | + _projects_cache = None |
0 commit comments