Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit e818ef2

Browse files
Merge branch 'main' of https://github.com/codecov/codecov-api into staging
2 parents 140a0ca + dde7496 commit e818ef2

File tree

26 files changed

+275
-146
lines changed

26 files changed

+275
-146
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ jobs:
4949
test:
5050
name: Test
5151
needs: [build]
52-
uses: codecov/gha-workflows/.github/workflows/[email protected].26
52+
uses: codecov/gha-workflows/.github/workflows/[email protected].27
5353
secrets: inherit
5454
with:
5555
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}
5656

5757
build-self-hosted:
5858
name: Build Self Hosted API
5959
needs: [build, test]
60-
uses: codecov/gha-workflows/.github/workflows/[email protected].26
60+
uses: codecov/gha-workflows/.github/workflows/[email protected].27
6161
secrets: inherit
6262
with:
6363
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}
@@ -66,7 +66,7 @@ jobs:
6666
name: Push Staging Image
6767
needs: [build, test]
6868
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/staging' && github.repository_owner == 'codecov' }}
69-
uses: codecov/gha-workflows/.github/workflows/[email protected].26
69+
uses: codecov/gha-workflows/.github/workflows/[email protected].27
7070
secrets: inherit
7171
with:
7272
environment: staging
@@ -76,7 +76,7 @@ jobs:
7676
name: Push Production Image
7777
needs: [build, test]
7878
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }}
79-
uses: codecov/gha-workflows/.github/workflows/[email protected].26
79+
uses: codecov/gha-workflows/.github/workflows/[email protected].27
8080
secrets: inherit
8181
with:
8282
environment: production
@@ -87,7 +87,7 @@ jobs:
8787
needs: [build-self-hosted, test]
8888
secrets: inherit
8989
if: ${{ github.event_name == 'push' && github.event.ref == 'refs/heads/main' && github.repository_owner == 'codecov' }}
90-
uses: codecov/gha-workflows/.github/workflows/[email protected].26
90+
uses: codecov/gha-workflows/.github/workflows/[email protected].27
9191
with:
9292
push_rolling: true
9393
repo: ${{ vars.CODECOV_IMAGE_V2 || 'codecov/self-hosted-api' }}

api/public/v2/component/views.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
from typing import Any
2+
13
from drf_spectacular.types import OpenApiTypes
24
from drf_spectacular.utils import OpenApiParameter, extend_schema
35
from rest_framework import viewsets
6+
from rest_framework.request import Request
47
from rest_framework.response import Response
58

69
from api.public.v2.component.serializers import ComponentSerializer
@@ -34,7 +37,7 @@ class ComponentViewSet(viewsets.ViewSet, RepoPropertyMixin):
3437
permission_classes = [RepositoryArtifactPermissions]
3538

3639
@extend_schema(summary="Component list")
37-
def list(self, request, *args, **kwargs):
40+
def list(self, request: Request, *args: Any, **kwargs: Any) -> Response:
3841
"""
3942
Returns a list of components for the specified repository
4043
"""

billing/helpers.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf import settings
2+
from django.db.models import QuerySet
23
from shared.plan.constants import ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS
34

45
from codecov_auth.models import Owner
@@ -8,3 +9,17 @@ def on_enterprise_plan(owner: Owner) -> bool:
89
return settings.IS_ENTERPRISE or (
910
owner.plan in ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS.keys()
1011
)
12+
13+
14+
def get_all_admins_for_owners(owners: QuerySet[Owner]):
15+
admin_ids = set()
16+
for owner in owners:
17+
if owner.admins:
18+
admin_ids.update(owner.admins)
19+
20+
# Add the owner's email as well - for user owners, admins is empty.
21+
if owner.email:
22+
admin_ids.add(owner.ownerid)
23+
24+
admins: QuerySet[Owner] = Owner.objects.filter(pk__in=admin_ids)
25+
return admins

billing/views.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
from datetime import datetime
3-
from typing import List
3+
from typing import Any, List
44

55
import stripe
66
from django.conf import settings
@@ -12,6 +12,7 @@
1212
from rest_framework.views import APIView
1313
from shared.plan.service import PlanService
1414

15+
from billing.helpers import get_all_admins_for_owners
1516
from codecov_auth.models import Owner
1617
from services.task.task import TaskService
1718

@@ -66,17 +67,7 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
6667
self._log_updated(list(owners))
6768

6869
# Send failed payment email to all owner admins
69-
70-
admin_ids = set()
71-
for owner in owners:
72-
if owner.admins:
73-
admin_ids.update(owner.admins)
74-
75-
# Add the owner's email as well - for user owners, admins is empty.
76-
if owner.email:
77-
admin_ids.add(owner.ownerid)
78-
79-
admins: QuerySet[Owner] = Owner.objects.filter(pk__in=admin_ids)
70+
admins = get_all_admins_for_owners(owners)
8071

8172
task_service = TaskService()
8273
card = (
@@ -102,6 +93,15 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
10293
**template_vars,
10394
)
10495

96+
# temporary just making sure these look okay in the real world
97+
task_service.send_email(
98+
to_addr="[email protected]",
99+
subject="Your Codecov payment failed",
100+
template_name="failed-payment",
101+
name="spalmurray-codecov",
102+
**template_vars,
103+
)
104+
105105
def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None:
106106
log.info(
107107
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
@@ -410,7 +410,7 @@ def checkout_session_completed(
410410

411411
self._log_updated([owner])
412412

413-
def post(self, request: HttpRequest, *args, **kwargs) -> Response:
413+
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
414414
if settings.STRIPE_ENDPOINT_SECRET is None:
415415
log.critical(
416416
"Stripe endpoint secret improperly configured -- webhooks will not be processed."

codecov_auth/authentication/repo_auth.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
import logging
3-
from typing import List
3+
from typing import Any, Dict, List, Optional, Tuple
44
from uuid import UUID
55

66
from django.core.exceptions import ObjectDoesNotExist
@@ -10,6 +10,7 @@
1010
from jwt import PyJWTError
1111
from rest_framework import authentication, exceptions, serializers
1212
from rest_framework.exceptions import NotAuthenticated
13+
from rest_framework.response import Response
1314
from rest_framework.views import exception_handler
1415
from shared.django_apps.codecov_auth.models import Owner
1516

@@ -32,7 +33,9 @@
3233
log = logging.getLogger(__name__)
3334

3435

35-
def repo_auth_custom_exception_handler(exc, context):
36+
def repo_auth_custom_exception_handler(
37+
exc: Exception, context: Dict[str, Any]
38+
) -> Response:
3639
"""
3740
User arrives here if they have correctly supplied a Token or the Tokenless Headers,
3841
but their Token has not matched with any of our Authentication methods. The goal is to
@@ -60,17 +63,17 @@ def repo_auth_custom_exception_handler(exc, context):
6063

6164

6265
class LegacyTokenRepositoryAuth(RepositoryAuthInterface):
63-
def __init__(self, repository, auth_data):
66+
def __init__(self, repository: Repository, auth_data: Dict[str, Any]) -> None:
6467
self._auth_data = auth_data
6568
self._repository = repository
6669

67-
def get_scopes(self):
70+
def get_scopes(self) -> List[TokenTypeChoices]:
6871
return [TokenTypeChoices.UPLOAD]
6972

70-
def get_repositories(self):
73+
def get_repositories(self) -> List[Repository]:
7174
return [self._repository]
7275

73-
def allows_repo(self, repository):
76+
def allows_repo(self, repository: Repository) -> bool:
7477
return repository in self.get_repositories()
7578

7679

@@ -79,17 +82,17 @@ class OIDCTokenRepositoryAuth(LegacyTokenRepositoryAuth):
7982

8083

8184
class TableTokenRepositoryAuth(RepositoryAuthInterface):
82-
def __init__(self, repository, token):
85+
def __init__(self, repository: Repository, token: RepositoryToken) -> None:
8386
self._token = token
8487
self._repository = repository
8588

86-
def get_scopes(self):
89+
def get_scopes(self) -> List[str]:
8790
return [self._token.token_type]
8891

89-
def get_repositories(self):
92+
def get_repositories(self) -> List[Repository]:
9093
return [self._repository]
9194

92-
def allows_repo(self, repository):
95+
def allows_repo(self, repository: Repository) -> bool:
9396
return repository in self.get_repositories()
9497

9598

@@ -98,10 +101,10 @@ def __init__(self, token: OrganizationLevelToken) -> None:
98101
self._token = token
99102
self._org = token.owner
100103

101-
def get_scopes(self):
104+
def get_scopes(self) -> List[str]:
102105
return [self._token.token_type]
103106

104-
def allows_repo(self, repository):
107+
def allows_repo(self, repository: Repository) -> bool:
105108
return repository.author.ownerid == self._org.ownerid
106109

107110
def get_repositories_queryset(self) -> QuerySet:
@@ -120,18 +123,20 @@ class TokenlessAuth(RepositoryAuthInterface):
120123
def __init__(self, repository: Repository) -> None:
121124
self._repository = repository
122125

123-
def get_scopes(self):
126+
def get_scopes(self) -> List[TokenTypeChoices]:
124127
return [TokenTypeChoices.UPLOAD]
125128

126-
def allows_repo(self, repository):
129+
def allows_repo(self, repository: Repository) -> bool:
127130
return repository in self.get_repositories()
128131

129132
def get_repositories(self) -> List[Repository]:
130133
return [self._repository]
131134

132135

133136
class RepositoryLegacyQueryTokenAuthentication(authentication.BaseAuthentication):
134-
def authenticate(self, request):
137+
def authenticate(
138+
self, request: HttpRequest
139+
) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]:
135140
token = request.GET.get("token")
136141
if not token:
137142
return None
@@ -150,22 +155,26 @@ def authenticate(self, request):
150155

151156

152157
class RepositoryLegacyTokenAuthentication(authentication.TokenAuthentication):
153-
def authenticate_credentials(self, token):
158+
def authenticate_credentials(
159+
self, token: str
160+
) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]:
154161
try:
155-
token = UUID(token)
156-
repository = Repository.objects.get(upload_token=token)
162+
token_uuid = UUID(token)
163+
repository = Repository.objects.get(upload_token=token_uuid)
157164
except (ValueError, TypeError, Repository.DoesNotExist):
158165
return None # continue to next auth class
159166
return (
160167
RepositoryAsUser(repository),
161-
LegacyTokenRepositoryAuth(repository, {"token": token}),
168+
LegacyTokenRepositoryAuth(repository, {"token": token_uuid}),
162169
)
163170

164171

165172
class RepositoryTokenAuthentication(authentication.TokenAuthentication):
166173
keyword = "Repotoken"
167174

168-
def authenticate_credentials(self, key):
175+
def authenticate_credentials(
176+
self, key: str
177+
) -> Optional[Tuple[RepositoryAsUser, TableTokenRepositoryAuth]]:
169178
try:
170179
token = RepositoryToken.objects.select_related("repository").get(key=key)
171180
except RepositoryToken.DoesNotExist:
@@ -182,7 +191,9 @@ def authenticate_credentials(self, key):
182191

183192

184193
class GlobalTokenAuthentication(authentication.TokenAuthentication):
185-
def authenticate(self, request):
194+
def authenticate(
195+
self, request: HttpRequest
196+
) -> Optional[Tuple[RepositoryAsUser, LegacyTokenRepositoryAuth]]:
186197
global_tokens = get_global_tokens()
187198
token = self.get_token(request)
188199
using_global_token = token in global_tokens
@@ -219,7 +230,9 @@ def get_token(self, request: HttpRequest) -> str | None:
219230

220231

221232
class OrgLevelTokenAuthentication(authentication.TokenAuthentication):
222-
def authenticate_credentials(self, key):
233+
def authenticate_credentials(
234+
self, key: str
235+
) -> Optional[Tuple[Owner, OrgLevelTokenRepositoryAuth]]:
223236
if is_uuid(key): # else, continue to next auth class
224237
# Actual verification for org level tokens
225238
token = OrganizationLevelToken.objects.filter(token=key).first()
@@ -236,7 +249,9 @@ def authenticate_credentials(self, key):
236249

237250

238251
class GitHubOIDCTokenAuthentication(authentication.TokenAuthentication):
239-
def authenticate_credentials(self, token):
252+
def authenticate_credentials(
253+
self, token: str
254+
) -> Optional[Tuple[RepositoryAsUser, OIDCTokenRepositoryAuth]]:
240255
if not token or is_uuid(token):
241256
return None # continue to next auth class
242257

@@ -283,7 +298,12 @@ def _get_info_from_request_path(
283298

284299
return repo, commitid
285300

286-
def get_branch(self, request, repoid=None, commitid=None):
301+
def get_branch(
302+
self,
303+
request: HttpRequest,
304+
repoid: Optional[int] = None,
305+
commitid: Optional[str] = None,
306+
) -> Optional[str]:
287307
if repoid and commitid:
288308
commit = Commit.objects.filter(
289309
repository_id=repoid, commitid=commitid
@@ -299,7 +319,9 @@ def get_branch(self, request, repoid=None, commitid=None):
299319
else:
300320
return body.get("branch")
301321

302-
def authenticate(self, request):
322+
def authenticate(
323+
self, request: HttpRequest
324+
) -> Tuple[RepositoryAsUser, TokenlessAuth]:
303325
repository, commitid = self._get_info_from_request_path(request)
304326

305327
if repository is None or repository.private:
@@ -341,7 +363,12 @@ def _get_info_from_request_path(
341363
# Validate provider
342364
raise exceptions.AuthenticationFailed(self.auth_failed_message)
343365

344-
def get_branch(self, request, repoid=None, commitid=None):
366+
def get_branch(
367+
self,
368+
request: HttpRequest,
369+
repoid: Optional[int] = None,
370+
commitid: Optional[str] = None,
371+
) -> str:
345372
body = json.loads(str(request.body, "utf8"))
346373

347374
# If commit is not created yet (ie first upload for this commit), we just validate branch format.
@@ -419,7 +446,7 @@ class UploadTokenRequiredGetFromBodyAuthenticationCheck(
419446
then use the same authenticate() as parent class.
420447
"""
421448

422-
def _get_git(self, validated_data):
449+
def _get_git(self, validated_data: Dict[str, str]) -> Optional[str]:
423450
"""
424451
BA sends this in as git_service, TA sends this in as service.
425452
Use this function so this Check class can be used by both views.

codecov_auth/commands/owner/interactors/get_uploads_number_per_user.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Optional
2+
13
from shared.plan.service import PlanService
24
from shared.upload.utils import query_monthly_coverage_measurements
35

@@ -11,7 +13,7 @@
1113

1214
class GetUploadsNumberPerUserInteractor(BaseInteractor):
1315
@sync_to_async
14-
def execute(self, owner: Owner):
16+
def execute(self, owner: Owner) -> Optional[int]:
1517
plan_service = PlanService(current_org=owner)
1618
monthly_limit = plan_service.monthly_uploads_limit
1719
if monthly_limit is not None:

0 commit comments

Comments
 (0)