Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions src/lando/api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import json
from functools import wraps
from typing import Callable

from django import forms
from django.core.handlers.wsgi import WSGIRequest
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt

from lando.main.models import (
CommitMap,
)
from lando.main.models.revision import DiffWarning, DiffWarningStatus
from lando.main.scm import SCMType
from lando.utils.phabricator import PHABRICATOR_API_KEY_HEADER, get_phabricator_client


class APIView(View):
"""A base class for API views."""

pass


def phabricator_api_key_required(func: Callable) -> Callable:
"""A simple wrapper that checks for a valid Phabricator API token."""

@wraps(func)
def _wrapper(self: View, request: WSGIRequest, *args, **kwargs) -> Callable:
if PHABRICATOR_API_KEY_HEADER not in request.headers:
return JsonResponse(
{"error": f"{PHABRICATOR_API_KEY_HEADER} missing."}, status=400
)

api_key = request.headers[PHABRICATOR_API_KEY_HEADER]
client = get_phabricator_client(api_key=api_key)
if not client.verify_api_token():
return JsonResponse({"error": "Invalid Phabricator API token."}, status=401)

return func(self, request, *args, **kwargs)

return _wrapper


@method_decorator(csrf_exempt, name="dispatch")
class LegacyDiffWarningView(View):
"""
This class provides the API controllers for the legacy `DiffWarning` model.

These API endpoints can be used by clients (such as Code Review bot) to
get, create, or archive warnings.
"""

@phabricator_api_key_required
def post(self, request: WSGIRequest) -> JsonResponse:
"""Create a new `DiffWarning` based on provided revision and diff IDs.

Args:
data (dict): A dictionary containing data to store in the warning. `data`
should contain at least a `message` key that contains the message to
show in the warning.

Returns:
dict: a dictionary representation of the object that was created.
"""

class Form(forms.Form):
def data_validator(data):
if not data or "message" not in data:
raise forms.ValidationError(
"Provided data is missing the message value"
)

revision_id = forms.IntegerField()
diff_id = forms.IntegerField()
group = forms.CharField()
data = forms.JSONField(validators=[data_validator])

# TODO: validate whether revision/diff exist or not.
form = Form(json.loads(request.body))
if form.is_valid():
data = form.cleaned_data
warning = DiffWarning.objects.create(**data)
return JsonResponse(warning.serialize(), status=201)

return JsonResponse({"errors": dict(form.errors)}, status=400)

@phabricator_api_key_required
def delete(self, request: WSGIRequest, diff_warning_id: int) -> JsonResponse:
"""Archive a `DiffWarning` based on provided pk."""
warning = DiffWarning.objects.get(pk=diff_warning_id)
if not warning:
return JsonResponse({}, status=404)

warning.status = DiffWarningStatus.ARCHIVED
warning.save()
return JsonResponse(warning.serialize(), status=200)

@phabricator_api_key_required
def get(self, request: WSGIRequest, **kwargs) -> JsonResponse:
"""Return a list of active revision diff warnings, if any."""

class Form(forms.Form):
revision_id = forms.IntegerField()
diff_id = forms.IntegerField()
group = forms.CharField()

form = Form(request.GET)
if form.is_valid():
warnings = DiffWarning.objects.filter(**form.cleaned_data).all()
return JsonResponse(
[warning.serialize() for warning in warnings], status=200, safe=False
)

return JsonResponse({"errors": dict(form.errors)}, status=400)


class CommitMapBaseView(View):
"""CommitMap base view to be extended for bidirectional git - hg mapping."""

scm: str

def get(
self, request: WSGIRequest, git_repo_name: str, commit_hash: str
) -> JsonResponse:
try:
commit = CommitMap.map_hash_from(self.scm, git_repo_name, commit_hash)
except CommitMap.DoesNotExist as exc:
error_detail = f"No commit found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
return JsonResponse(
{"error": "No commits found", "detail": error_detail}, status=404
)
except CommitMap.MultipleObjectsReturned as exc:
error_detail = f"Multiple commits found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
return JsonResponse(
{"error": "Multiple commits found", "detail": error_detail}, status=400
)

return JsonResponse(commit.serialize(), status=200)


@method_decorator(csrf_exempt, name="dispatch")
class git2hgCommitMapView(CommitMapBaseView):
"""Return corresponding CommitMap given a git hash."""

scm = SCMType.GIT


@method_decorator(csrf_exempt, name="dispatch")
class hg2gitCommitMapView(CommitMapBaseView):
"""Return corresponding CommitMap given an hg hash."""

scm = SCMType.HG
146 changes: 1 addition & 145 deletions src/lando/api/views.py → src/lando/api/views/pull_requests.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,31 @@
import json
from collections import defaultdict
from datetime import datetime
from functools import wraps
from typing import Callable

from django import forms
from django.core.handlers.wsgi import WSGIRequest
from django.http import HttpRequest, JsonResponse
from django.utils.decorators import method_decorator
from django.utils.html import escape
from django.views import View
from django.views.decorators.csrf import csrf_exempt

from lando.api.legacy.commit_message import replace_reviewers
from lando.main.auth import require_authenticated_user
from lando.main.models import (
CommitMap,
JobStatus,
LandingJob,
Repo,
Revision,
add_revisions_to_job,
)
from lando.main.models.landing_job import get_jobs_for_pull
from lando.main.models.revision import DiffWarning, DiffWarningStatus
from lando.main.scm import SCMType
from lando.utils.github import GitHubAPIClient, PullRequest, PullRequestPatchHelper
from lando.utils.github_checks import (
ALL_PULL_REQUEST_BLOCKERS,
ALL_PULL_REQUEST_WARNINGS,
PullRequestChecks,
)
from lando.utils.landing_checks import LandingChecks
from lando.utils.phabricator import PHABRICATOR_API_KEY_HEADER, get_phabricator_client


class APIView(View):
"""A base class for API views."""

pass


def phabricator_api_key_required(func: Callable) -> Callable:
"""A simple wrapper that checks for a valid Phabricator API token."""

@wraps(func)
def _wrapper(self: View, request: WSGIRequest, *args, **kwargs) -> Callable:
if PHABRICATOR_API_KEY_HEADER not in request.headers:
return JsonResponse(
{"error": f"{PHABRICATOR_API_KEY_HEADER} missing."}, status=400
)

api_key = request.headers[PHABRICATOR_API_KEY_HEADER]
client = get_phabricator_client(api_key=api_key)
if not client.verify_api_token():
return JsonResponse({"error": "Invalid Phabricator API token."}, status=401)

return func(self, request, *args, **kwargs)

return _wrapper


def generate_warnings_and_blockers(
Expand Down Expand Up @@ -92,117 +59,6 @@ def generate_warnings_and_blockers(
return {"warnings": warnings, "blockers": blockers}


@method_decorator(csrf_exempt, name="dispatch")
class LegacyDiffWarningView(View):
"""
This class provides the API controllers for the legacy `DiffWarning` model.

These API endpoints can be used by clients (such as Code Review bot) to
get, create, or archive warnings.
"""

@phabricator_api_key_required
def post(self, request: WSGIRequest) -> JsonResponse:
"""Create a new `DiffWarning` based on provided revision and diff IDs.

Args:
data (dict): A dictionary containing data to store in the warning. `data`
should contain at least a `message` key that contains the message to
show in the warning.

Returns:
dict: a dictionary representation of the object that was created.
"""

class Form(forms.Form):
def data_validator(data):
if not data or "message" not in data:
raise forms.ValidationError(
"Provided data is missing the message value"
)

revision_id = forms.IntegerField()
diff_id = forms.IntegerField()
group = forms.CharField()
data = forms.JSONField(validators=[data_validator])

# TODO: validate whether revision/diff exist or not.
form = Form(json.loads(request.body))
if form.is_valid():
data = form.cleaned_data
warning = DiffWarning.objects.create(**data)
return JsonResponse(warning.serialize(), status=201)

return JsonResponse({"errors": dict(form.errors)}, status=400)

@phabricator_api_key_required
def delete(self, request: WSGIRequest, diff_warning_id: int) -> JsonResponse:
"""Archive a `DiffWarning` based on provided pk."""
warning = DiffWarning.objects.get(pk=diff_warning_id)
if not warning:
return JsonResponse({}, status=404)

warning.status = DiffWarningStatus.ARCHIVED
warning.save()
return JsonResponse(warning.serialize(), status=200)

@phabricator_api_key_required
def get(self, request: WSGIRequest, **kwargs) -> JsonResponse:
"""Return a list of active revision diff warnings, if any."""

class Form(forms.Form):
revision_id = forms.IntegerField()
diff_id = forms.IntegerField()
group = forms.CharField()

form = Form(request.GET)
if form.is_valid():
warnings = DiffWarning.objects.filter(**form.cleaned_data).all()
return JsonResponse(
[warning.serialize() for warning in warnings], status=200, safe=False
)

return JsonResponse({"errors": dict(form.errors)}, status=400)


class CommitMapBaseView(View):
"""CommitMap base view to be extended for bidirectional git - hg mapping."""

scm: str

def get(
self, request: WSGIRequest, git_repo_name: str, commit_hash: str
) -> JsonResponse:
try:
commit = CommitMap.map_hash_from(self.scm, git_repo_name, commit_hash)
except CommitMap.DoesNotExist as exc:
error_detail = f"No commit found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
return JsonResponse(
{"error": "No commits found", "detail": error_detail}, status=404
)
except CommitMap.MultipleObjectsReturned as exc:
error_detail = f"Multiple commits found in {self.scm} for {commit_hash} in {git_repo_name}: {exc}"
return JsonResponse(
{"error": "Multiple commits found", "detail": error_detail}, status=400
)

return JsonResponse(commit.serialize(), status=200)


@method_decorator(csrf_exempt, name="dispatch")
class git2hgCommitMapView(CommitMapBaseView):
"""Return corresponding CommitMap given a git hash."""

scm = SCMType.GIT


@method_decorator(csrf_exempt, name="dispatch")
class hg2gitCommitMapView(CommitMapBaseView):
"""Return corresponding CommitMap given an hg hash."""

scm = SCMType.HG


class PullRequestAPIView(View):
"""Set various common attributes for views that extend this one."""

Expand Down Expand Up @@ -314,7 +170,7 @@ class Form(forms.Form):
return JsonResponse({"id": job.id}, status=201)


class PullRequestChecksAPIView(PullRequestAPIView):
class ChecksPullRequestAPIView(PullRequestAPIView):
def get(
self, request: WSGIRequest, repo_name: str, pull_number: int
) -> JsonResponse:
Expand Down
8 changes: 5 additions & 3 deletions src/lando/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@
from lando.api.legacy.api import landing_jobs
from lando.api.uplift_api import api as uplift_api
from lando.api.views import (
LandingJobPullRequestAPIView,
LegacyDiffWarningView,
PullRequestChecksAPIView,
git2hgCommitMapView,
hg2gitCommitMapView,
)
from lando.api.views.pull_requests import (
LandingJobPullRequestAPIView,
ChecksPullRequestAPIView,
)
from lando.headless_api.api import (
api as headless_api,
)
Expand Down Expand Up @@ -122,7 +124,7 @@
),
path(
"api/pulls/<str:repo_name>/<int:pull_number>/checks",
PullRequestChecksAPIView.as_view(),
ChecksPullRequestAPIView.as_view(),
name="api-pull-request-checks",
),
]
Expand Down
Loading