-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapi.py
More file actions
221 lines (192 loc) · 9.67 KB
/
Copy pathapi.py
File metadata and controls
221 lines (192 loc) · 9.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import logging
from dataclasses import fields
from flask import Flask, request, Response
from configuration.web_configuration import WebConfiguration
from configuration.monitoring_configuration import MonitoringConfiguration
from contracts.github_webhook import GithubWebhook
from contracts.pr_url import PrUrl
from contracts.gitea_webhook import GiteaWebhook
from contracts.review_task import ReviewTask
from services.gitea_service import GiteaService
from services.github_service import GithubService
from services.queue.task_queue import TaskQueue
from services.review_service import ReviewService
from monitoring import metrics
class Api:
"""
API service for handling webhooks and review requests.
This class provides endpoints for receiving webhooks from Git services,
processing code review requests, and exposing health and metrics information.
"""
START_REVIEW_COMMAND: str = "/start_review"
def __init__(self, configuration: WebConfiguration, monitoring_configuration: MonitoringConfiguration,
gitea_service: GiteaService, review_service: ReviewService, queue: TaskQueue,
github_service: GithubService):
"""
Initialize the API with required services and configuration.
Args:
configuration: Web server configuration (host, port, token).
monitoring_configuration: Credentials for metrics endpoint basic auth.
gitea_service: Service for Gitea interactions.
review_service: Service for performing code reviews.
queue: Queue for review tasks.
github_service: Service for GitHub interactions.
"""
self.configuration = configuration
self.monitoring_configuration = monitoring_configuration
self.gitea_service = gitea_service
self.github_service = github_service
self.review_service = review_service
self.queue = queue
self.logger = logging.getLogger(Api.__name__)
self.app = Flask(__name__)
self.app.before_request(self.__require_api_auth)
self.__configure_routes()
def start(self):
"""
Start the API web server.
Runs the Flask application with the configured host and port.
"""
self.app.run(host=self.configuration.host, port=self.configuration.port)
def __configure_routes(self):
"""
Configure API routes for webhook, health and metrics endpoints.
"""
self.app.add_url_rule("/webhook/gitea", view_func=self.__gitea_webhook_route, methods=["POST"])
self.app.add_url_rule("/webhook/github", view_func=self.__github_webhook_route, methods=["POST"])
self.app.add_url_rule("/review/diff", view_func=self.__review_diff_route, methods=["POST"])
self.app.add_url_rule("/health", view_func=self.__health_route, methods=["GET"])
self.app.add_url_rule("/metrics", view_func=self.__metrics_route, methods=["GET"])
def __require_api_auth(self):
"""
Middleware for API authentication.
/health and /metrics are exempt from auth. All other endpoints
require a valid token either as a query parameter (?token=...) or
as a Bearer token in the Authorization header.
"""
if request.path in ("/health", "/metrics"):
return
# Check query-string token
token = request.args.get("token")
if token is not None and token == self.configuration.token:
return
# Check Bearer token
bearer = request.headers.get("Authorization")
if bearer is None or bearer == "" or "Bearer" not in bearer:
return "Unauthorized", 401
parts = bearer.split()
if len(parts) < 2:
return "Unauthorized", 401
token = parts[1]
if token == self.configuration.token:
return
return "Unauthorized", 401
def __ensure_gitea_comment_event(self) -> GiteaWebhook:
"""
Validate and parse a Gitea issue_comment webhook event.
Returns:
GiteaWebhook instance if valid, None otherwise.
"""
gitea_event = request.headers.get("X-Gitea-Event", None)
if gitea_event is None or gitea_event != "issue_comment":
return None
request_json = request.get_json()
webhook_fields = {f.name for f in fields(GiteaWebhook)}
return GiteaWebhook(**{k: v for k, v in request_json.items() if k in webhook_fields})
def __ensure_github_comment_event(self) -> GithubWebhook:
"""
Validate and parse a GitHub issue_comment webhook event.
Returns:
GithubWebhook instance if valid, None otherwise.
"""
github_event = request.headers.get("X-Github-Event", None)
if github_event is None or github_event != "issue_comment":
return None
request_json = request.get_json()
return GithubWebhook(**request_json)
def __process_review_request(self, pull_request_url: str, git_service: str, comment_body: str) -> None:
"""
Parse the /start_review command, create a ReviewTask and enqueue it.
Args:
pull_request_url: URL of the pull request to review.
git_service: Name of the Git service ("gitea" or "github").
comment_body: The full comment text that triggered the review.
"""
self.logger.info("Processing command: %s", comment_body)
user_message = comment_body.replace(self.START_REVIEW_COMMAND, "").strip()
review_task = ReviewTask(pull_request_url, git_service, user_message if len(user_message) > 5 else None)
self.queue.enqueue(review_task)
pr_url = PrUrl.create_from_url(pull_request_url)
self.logger.info("%s Review %s/%s #%s enqueued", git_service.upper(), pr_url.owner, pr_url.repo, pr_url.pr_number)
# ---- Routes ----
def __gitea_webhook_route(self):
"""Handle incoming Gitea issue_comment webhooks."""
webhook = self.__ensure_gitea_comment_event()
if webhook is None:
return "Not allowed event %s" % (request.headers.get("X-Gitea-Event", None)), 400
if webhook.action != "created" or webhook.comment is None:
return "Is not comment create event. Ignore event", 200
if not webhook.comment.body.startswith(self.START_REVIEW_COMMAND):
return "Comment not start with /start_review. Ignore event", 200
user_email = webhook.comment.user.email if webhook.comment.user is not None else ""
if not self.gitea_service.is_allowed_user(user_email):
fail_response = f"User {user_email} not allowed to start review"
self.logger.warning(fail_response)
return fail_response, 403
self.__process_review_request(webhook.comment.pull_request_url, "gitea", webhook.comment.body)
return "Review task enqueued", 200
def __github_webhook_route(self):
"""Handle incoming GitHub issue_comment webhooks."""
webhook = self.__ensure_github_comment_event()
if webhook is None:
return "Not allowed event %s" % (request.headers.get("X-Github-Event", None)), 400
if webhook.action != "created" or webhook.comment is None:
return "Is not comment create event. Ignore event", 200
if not webhook.comment.body.startswith(self.START_REVIEW_COMMAND):
return "Comment not start with /start_review. Ignore event", 200
user_login = webhook.comment.user.login if webhook.comment.user is not None else ""
if not self.github_service.is_allowed_user(user_login):
fail_response = f"User {user_login} not allowed to start review"
self.logger.warning(fail_response)
return fail_response, 403
self.__process_review_request(webhook.issue.pull_request.html_url, "github", webhook.comment.body)
return "Review task enqueued", 200
def __review_diff_route(self):
"""
Endpoint for reviewing a git diff directly.
Request body should be plain text containing a git diff.
Query parameter ?user_message= provides optional instructions.
Returns:
JSON with status and review results.
"""
try:
diff = request.get_data(as_text=True)
if not diff or diff.strip() == "":
return {"error": "Request body must contain git diff text"}, 400
user_message = request.args.get("user_message")
self.logger.info("Received direct diff review request (diff length: %s)", len(diff))
review_results = self.review_service.review_pull_request(diff, user_message)
return {"status": "success", "review": review_results}, 200
except Exception as e:
self.logger.error("Error in diff review: %s", e, exc_info=True)
return {"error": str(e)}, 500
def __health_route(self):
"""Simple health check endpoint for Docker orchestration."""
return {"status": "healthy"}, 200
def __metrics_route(self):
"""
Prometheus metrics endpoint with optional basic auth.
If monitoring.metrics_user and monitoring.metrics_password are
configured, the endpoint requires HTTP Basic Authentication.
"""
cfg = self.monitoring_configuration
if cfg.metrics_user and cfg.metrics_password:
auth = request.authorization
if not auth or auth.username != cfg.metrics_user or auth.password != cfg.metrics_password:
return Response(
"Unauthorized",
401,
{"WWW-Authenticate": 'Basic realm="Metrics Login Required"'},
)
data = metrics.get_metrics()
return data, 200, {"Content-Type": "text/plain; charset=utf-8"}