|
1 | 1 | import logging |
2 | 2 | from json.decoder import JSONDecodeError |
| 3 | +from typing import TypedDict |
3 | 4 |
|
4 | 5 | import httpx |
5 | 6 | from django.core.handlers.wsgi import WSGIRequest |
|
14 | 15 | log = logging.getLogger(__name__) |
15 | 16 |
|
16 | 17 |
|
| 18 | +class RepoAPIData(TypedDict): |
| 19 | + """ |
| 20 | + Partial typing for the GitHub API response for a repository. |
| 21 | +
|
| 22 | + Only the fields we care about are included here. |
| 23 | + """ |
| 24 | + |
| 25 | + full_name: str |
| 26 | + description: str |
| 27 | + language: str |
| 28 | + forks_count: int |
| 29 | + stargazers_count: int |
| 30 | + |
| 31 | + |
17 | 32 | class HomeView(View): |
18 | 33 | """The main landing page for the website.""" |
19 | 34 |
|
@@ -45,7 +60,7 @@ def __init__(self): |
45 | 60 | else: |
46 | 61 | self.headers = {} |
47 | 62 |
|
48 | | - def _get_api_data(self) -> dict[str, dict[str, str]]: |
| 63 | + def _get_api_data(self) -> dict[str, RepoAPIData]: |
49 | 64 | """ |
50 | 65 | Call the GitHub API and get information about our repos. |
51 | 66 |
|
@@ -139,26 +154,32 @@ def _get_repo_data(self) -> list[RepositoryMetadata]: |
139 | 154 | api_repositories = self._get_api_data() |
140 | 155 |
|
141 | 156 | if not api_repositories: |
142 | | - return RepositoryMetadata.objects.all() |
| 157 | + return list(RepositoryMetadata.objects.all()) |
143 | 158 |
|
144 | | - # Update or create all RepoData objects in self.repos |
| 159 | + # Build updated objects and bulk_update in two queries (fetch + update) |
| 160 | + existing = {r.repo_name: r for r in RepositoryMetadata.objects.all()} |
145 | 161 | database_repositories = [] |
| 162 | + new_repos = [] |
146 | 163 | for api_data in api_repositories.values(): |
147 | | - repo_data, _created = RepositoryMetadata.objects.update_or_create( |
148 | | - repo_name=api_data["full_name"], |
149 | | - defaults={ |
150 | | - 'repo_name': api_data["full_name"], |
151 | | - 'description': api_data["description"], |
152 | | - 'forks': api_data["forks_count"], |
153 | | - 'stargazers': api_data["stargazers_count"], |
154 | | - 'language': api_data["language"], |
155 | | - } |
156 | | - ) |
157 | | - database_repositories.append(repo_data) |
| 164 | + repo = existing.get(api_data["full_name"]) |
| 165 | + if repo is None: |
| 166 | + repo = RepositoryMetadata(repo_name=api_data["full_name"]) |
| 167 | + new_repos.append(repo) |
| 168 | + repo.description = api_data["description"] |
| 169 | + repo.forks = api_data["forks_count"] |
| 170 | + repo.stargazers = api_data["stargazers_count"] |
| 171 | + repo.language = api_data["language"] |
| 172 | + database_repositories.append(repo) |
| 173 | + RepositoryMetadata.objects.bulk_update( |
| 174 | + [r for r in database_repositories if r not in new_repos], |
| 175 | + fields=["description", "forks", "stargazers", "language"], |
| 176 | + ) |
| 177 | + if new_repos: |
| 178 | + RepositoryMetadata.objects.bulk_create(new_repos) |
158 | 179 | return database_repositories |
159 | 180 |
|
160 | 181 | # Otherwise, if the data is fresher than 2 minutes old, we should just return it. |
161 | | - return RepositoryMetadata.objects.all() |
| 182 | + return list(RepositoryMetadata.objects.all()) |
162 | 183 |
|
163 | 184 | def get(self, request: WSGIRequest) -> HttpResponse: |
164 | 185 | """Collect repo data and render the homepage view.""" |
|
0 commit comments