Changeset View
Changeset View
Standalone View
Standalone View
swh/web/save_origin_webhooks/generic_receiver.py
- This file was added.
# Copyright (C) 2022 The Software Heritage developers | ||||||||||||
# See the AUTHORS file at the top-level directory of this distribution | ||||||||||||
# License: GNU Affero General Public License version 3, or any later version | ||||||||||||
# See top-level LICENSE file for more information | ||||||||||||
import abc | ||||||||||||
from typing import Any, Dict, Tuple | ||||||||||||
from rest_framework.request import Request | ||||||||||||
from swh.web.api.apidoc import api_doc | ||||||||||||
from swh.web.api.apiurls import api_route | ||||||||||||
from swh.web.save_code_now.origin_save import create_save_origin_request | ||||||||||||
from swh.web.utils.exc import BadInputExc | ||||||||||||
class OriginSaveWebhookReceiver(abc.ABC): | ||||||||||||
FORGE_TYPE: str | ||||||||||||
WEBHOOK_GUIDE_URL: str | ||||||||||||
REPO_TYPES: str | ||||||||||||
@abc.abstractmethod | ||||||||||||
def is_forge_request(self, request: Request) -> bool: | ||||||||||||
... | ||||||||||||
@abc.abstractmethod | ||||||||||||
def is_push_event(self, request: Request) -> bool: | ||||||||||||
... | ||||||||||||
@abc.abstractmethod | ||||||||||||
def extract_repo_url_and_visit_type(self, request: Request) -> Tuple[str, str]: | ||||||||||||
... | ||||||||||||
def __init__(self): | ||||||||||||
self.__doc__ = f""" | ||||||||||||
.. http:post:: /api/1/origin/save/webhook/{self.FORGE_TYPE.lower()}/ | ||||||||||||
Webhook receiver for {self.FORGE_TYPE} to request or update the archival of | ||||||||||||
a repository when new commits are pushed to it. | ||||||||||||
To add such webhook to one of your {self.REPO_TYPES} repository hosted on | ||||||||||||
{self.FORGE_TYPE}, please follow `{self.FORGE_TYPE}'s webhooks guide | ||||||||||||
vlorentz: it's best to make link text self-descriptive | ||||||||||||
<{self.WEBHOOK_GUIDE_URL}>`_. | ||||||||||||
The expected content type for the webhook payload must be ``application/json``. | ||||||||||||
Done Inline Actions
vlorentz: | ||||||||||||
:>json string origin_url: the url of the origin to save | ||||||||||||
:>json string visit_type: the type of visit to perform | ||||||||||||
:>json string save_request_date: the date (in iso format) the save | ||||||||||||
request was issued | ||||||||||||
:>json string save_request_status: the status of the save request, | ||||||||||||
either **accepted**, **rejected** or **pending** | ||||||||||||
Done Inline ActionsShouldn't it return the same values as https://archive.softwareheritage.org/1/origin/save/doc/ , for the sake of consistency? vlorentz: Shouldn't it return the same values as https://archive.softwareheritage.org/1/origin/save/doc/… | ||||||||||||
Not Done Inline ActionsI do not think we should return the other fields as most of them will be null (visit date and status for instance) so it is quite pointless to include them imho. anlambert: I do not think we should return the other fields as most of them will be null (visit date and… | ||||||||||||
:statuscode 200: save request for repository has been successfully created | ||||||||||||
from the webhook payload. | ||||||||||||
:statuscode 400: no save request has been created due to invalid POST | ||||||||||||
request or missing data in webhook payload | ||||||||||||
""" | ||||||||||||
self.__name__ = "api_origin_save_webhook_{self.FORGE_TYPE.lower()}" | ||||||||||||
api_doc( | ||||||||||||
f"/origin/save/webhook/{self.FORGE_TYPE.lower()}/", | ||||||||||||
category="Request archival", | ||||||||||||
)(self) | ||||||||||||
api_route( | ||||||||||||
f"/origin/save/webhook/{self.FORGE_TYPE.lower()}/", | ||||||||||||
f"api-1-origin-save-webhook-{self.FORGE_TYPE.lower()}", | ||||||||||||
methods=["POST"], | ||||||||||||
)(self) | ||||||||||||
def __call__( | ||||||||||||
self, | ||||||||||||
request: Request, | ||||||||||||
) -> Dict[str, Any]: | ||||||||||||
if not self.is_forge_request(request): | ||||||||||||
raise BadInputExc( | ||||||||||||
f"POST request was not sent by a {self.FORGE_TYPE} webhook and " | ||||||||||||
"has not been processed." | ||||||||||||
Done Inline Actions
Preterit feels more correct than present perfect here. It's also consistent with other errors below vlorentz: Preterit feels more correct than present perfect here. It's also consistent with other errors… | ||||||||||||
) | ||||||||||||
if not self.is_push_event(request): | ||||||||||||
raise BadInputExc( | ||||||||||||
f"Event sent by {self.FORGE_TYPE} webhook is not a push one, request " | ||||||||||||
"has not been processed." | ||||||||||||
) | ||||||||||||
content_type = request.headers.get("Content-Type") | ||||||||||||
if content_type != "application/json": | ||||||||||||
raise BadInputExc( | ||||||||||||
f"Invalid content type '{content_type}' for the POST request sent by " | ||||||||||||
f"{self.FORGE_TYPE} webhook, it should be 'application/json'." | ||||||||||||
) | ||||||||||||
repo_url, visit_type = self.extract_repo_url_and_visit_type(request) | ||||||||||||
if not repo_url: | ||||||||||||
raise BadInputExc( | ||||||||||||
f"Repository URL could not be extracted from {self.FORGE_TYPE} webhook " | ||||||||||||
f"payload." | ||||||||||||
) | ||||||||||||
if not visit_type: | ||||||||||||
raise BadInputExc( | ||||||||||||
f"Visit type could not be determined for repository {repo_url}." | ||||||||||||
) | ||||||||||||
save_request = create_save_origin_request( | ||||||||||||
visit_type=visit_type, origin_url=repo_url | ||||||||||||
) | ||||||||||||
return { | ||||||||||||
"origin_url": save_request["origin_url"], | ||||||||||||
"visit_type": save_request["visit_type"], | ||||||||||||
"save_request_date": save_request["save_request_date"], | ||||||||||||
"save_request_status": save_request["save_request_status"], | ||||||||||||
} |
it's best to make link text self-descriptive