patchrail 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- patchrail/__init__.py +7 -0
- patchrail/__main__.py +7 -0
- patchrail/ci/__init__.py +7 -0
- patchrail/ci/classify.py +888 -0
- patchrail/cli.py +8566 -0
- patchrail/funded_issues/__init__.py +138 -0
- patchrail/funded_issues/algora_board.py +240 -0
- patchrail/funded_issues/blocklist.py +112 -0
- patchrail/funded_issues/discovery.py +4091 -0
- patchrail/funded_issues/importers.py +316 -0
- patchrail/funded_issues/source_noise.py +349 -0
- patchrail/funded_issues/store.py +459 -0
- patchrail/queue/__init__.py +75 -0
- patchrail/queue/server.py +273 -0
- patchrail/queue/status.py +756 -0
- patchrail/queue/store.py +600 -0
- patchrail/reviewer_quick_check.py +650 -0
- patchrail/schemas/__init__.py +1 -0
- patchrail/schemas/application-dossier.v1.schema.json +305 -0
- patchrail/schemas/ci-benchmark.v1.schema.json +174 -0
- patchrail/schemas/ci-fixture-check.v1.schema.json +122 -0
- patchrail/schemas/ci-pilot-metrics.v1.schema.json +164 -0
- patchrail/schemas/ci-pilot-summary.v1.schema.json +146 -0
- patchrail/schemas/ci-result.v1.schema.json +133 -0
- patchrail/schemas/funded-issues-client-report.v1.schema.json +524 -0
- patchrail/schemas/funded-issues-recheck-queue.v1.schema.json +333 -0
- patchrail/schemas/funded-issues-recheck-summary.v1.schema.json +136 -0
- patchrail/schemas/funded-issues-report.v1.schema.json +836 -0
- patchrail/schemas/funded-issues-shortlist.v1.schema.json +953 -0
- patchrail/schemas/funded-issues-store-status.v1.schema.json +96 -0
- patchrail/schemas/funded-issues-store.v1.schema.json +117 -0
- patchrail/schemas/queue-audit-event.v1.schema.json +44 -0
- patchrail/schemas/queue-audit-summary.v1.schema.json +169 -0
- patchrail/schemas/queue-gate-report.v1.schema.json +158 -0
- patchrail/schemas/queue-policy-resolution.v1.schema.json +188 -0
- patchrail/schemas/queue-policy-scan.v1.schema.json +175 -0
- patchrail/schemas/queue-proposal.v1.schema.json +61 -0
- patchrail/schemas/queue-review.v1.schema.json +218 -0
- patchrail/schemas/queue-status.v1.schema.json +179 -0
- patchrail/schemas/queue-work-item.v1.schema.json +64 -0
- patchrail/schemas/reviewer-quick-check-artifacts.v1.schema.json +104 -0
- patchrail/web_metrics.py +649 -0
- patchrail-0.1.0.dist-info/METADATA +279 -0
- patchrail-0.1.0.dist-info/RECORD +47 -0
- patchrail-0.1.0.dist-info/WHEEL +4 -0
- patchrail-0.1.0.dist-info/entry_points.txt +2 -0
- patchrail-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import parse_qs, urlparse
|
|
8
|
+
|
|
9
|
+
from patchrail.queue.status import SAFE_QUEUE_REQUIREMENTS, queue_status_payload
|
|
10
|
+
from patchrail.queue.store import (
|
|
11
|
+
DEFAULT_QUEUE_PATH,
|
|
12
|
+
SCHEMA_VERSION,
|
|
13
|
+
add_proposal,
|
|
14
|
+
add_work_item,
|
|
15
|
+
approve_proposal,
|
|
16
|
+
approve_work_item,
|
|
17
|
+
export_audit_events,
|
|
18
|
+
init_queue,
|
|
19
|
+
list_proposals,
|
|
20
|
+
list_work_items,
|
|
21
|
+
reject_proposal,
|
|
22
|
+
reject_work_item,
|
|
23
|
+
show_proposal,
|
|
24
|
+
show_work_item,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _json_response(payload: dict[str, Any], status: int = 200) -> tuple[int, dict[str, Any]]:
|
|
29
|
+
return status, payload
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _not_found(message: str) -> tuple[int, dict[str, Any]]:
|
|
33
|
+
return _json_response({"error": message}, 404)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _bad_request(message: str) -> tuple[int, dict[str, Any]]:
|
|
37
|
+
return _json_response({"error": message}, 400)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _approval_note(payload: dict[str, Any]) -> str | None:
|
|
41
|
+
note = payload.get("note")
|
|
42
|
+
return str(note) if note is not None else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _decode_json_body(body: bytes) -> dict[str, Any]:
|
|
46
|
+
if not body:
|
|
47
|
+
return {}
|
|
48
|
+
payload = json.loads(body.decode("utf-8"))
|
|
49
|
+
if not isinstance(payload, dict):
|
|
50
|
+
raise ValueError("request body must be a JSON object")
|
|
51
|
+
return payload
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def handle_queue_api_request(
|
|
55
|
+
*,
|
|
56
|
+
method: str,
|
|
57
|
+
raw_path: str,
|
|
58
|
+
body: bytes = b"",
|
|
59
|
+
db_path: Path = DEFAULT_QUEUE_PATH,
|
|
60
|
+
) -> tuple[int, dict[str, Any]]:
|
|
61
|
+
parsed = urlparse(raw_path)
|
|
62
|
+
path_parts = [part for part in parsed.path.strip("/").split("/") if part]
|
|
63
|
+
query = parse_qs(parsed.query)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
payload = _decode_json_body(body)
|
|
67
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
|
|
68
|
+
return _bad_request(f"invalid JSON body: {exc}")
|
|
69
|
+
|
|
70
|
+
if method == "GET" and path_parts == ["health"]:
|
|
71
|
+
return _json_response(
|
|
72
|
+
{
|
|
73
|
+
"status": "ok",
|
|
74
|
+
"schema_version": "patchrail.queue_api.v1",
|
|
75
|
+
"local_first": True,
|
|
76
|
+
"requirements": SAFE_QUEUE_REQUIREMENTS,
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if method == "GET" and path_parts == ["status"]:
|
|
81
|
+
return _json_response(queue_status_payload(db_path, include_api_compat=True))
|
|
82
|
+
|
|
83
|
+
if method == "GET" and path_parts == ["work-items"]:
|
|
84
|
+
try:
|
|
85
|
+
items = [
|
|
86
|
+
item.to_dict()
|
|
87
|
+
for item in list_work_items(
|
|
88
|
+
db_path=db_path,
|
|
89
|
+
status=(query.get("status") or [None])[0],
|
|
90
|
+
approval_state=(query.get("approval_state") or [None])[0],
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
return _bad_request(str(exc))
|
|
95
|
+
return _json_response(
|
|
96
|
+
{
|
|
97
|
+
"schema_version": SCHEMA_VERSION,
|
|
98
|
+
"local_first": True,
|
|
99
|
+
"work_items": items,
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if method == "POST" and path_parts == ["work-items"]:
|
|
104
|
+
kind = str(payload.get("kind") or "")
|
|
105
|
+
title = str(payload.get("title") or "")
|
|
106
|
+
if not kind or not title:
|
|
107
|
+
return _bad_request("work item requires kind and title")
|
|
108
|
+
item = add_work_item(
|
|
109
|
+
db_path=db_path,
|
|
110
|
+
kind=kind,
|
|
111
|
+
title=title,
|
|
112
|
+
source=str(payload.get("source") or "api"),
|
|
113
|
+
payload=dict(payload.get("payload") or {}),
|
|
114
|
+
)
|
|
115
|
+
return _json_response(item.to_dict(), 201)
|
|
116
|
+
|
|
117
|
+
if len(path_parts) == 2 and path_parts[0] == "work-items":
|
|
118
|
+
item_id = path_parts[1]
|
|
119
|
+
if method == "GET":
|
|
120
|
+
try:
|
|
121
|
+
return _json_response(show_work_item(db_path=db_path, item_id=item_id).to_dict())
|
|
122
|
+
except KeyError:
|
|
123
|
+
return _not_found(f"unknown work item: {item_id}")
|
|
124
|
+
|
|
125
|
+
if len(path_parts) == 3 and path_parts[0] == "work-items":
|
|
126
|
+
item_id = path_parts[1]
|
|
127
|
+
action = path_parts[2]
|
|
128
|
+
if method == "POST" and action == "approve":
|
|
129
|
+
try:
|
|
130
|
+
item = approve_work_item(
|
|
131
|
+
db_path=db_path,
|
|
132
|
+
item_id=item_id,
|
|
133
|
+
decision_note=_approval_note(payload),
|
|
134
|
+
)
|
|
135
|
+
return _json_response(item.to_dict())
|
|
136
|
+
except KeyError:
|
|
137
|
+
return _not_found(f"unknown work item: {item_id}")
|
|
138
|
+
if method == "POST" and action == "reject":
|
|
139
|
+
try:
|
|
140
|
+
item = reject_work_item(
|
|
141
|
+
db_path=db_path,
|
|
142
|
+
item_id=item_id,
|
|
143
|
+
decision_note=_approval_note(payload),
|
|
144
|
+
)
|
|
145
|
+
return _json_response(item.to_dict())
|
|
146
|
+
except KeyError:
|
|
147
|
+
return _not_found(f"unknown work item: {item_id}")
|
|
148
|
+
|
|
149
|
+
if method == "GET" and path_parts == ["proposals"]:
|
|
150
|
+
try:
|
|
151
|
+
proposals = [
|
|
152
|
+
proposal.to_dict()
|
|
153
|
+
for proposal in list_proposals(
|
|
154
|
+
db_path=db_path,
|
|
155
|
+
work_item_id=(query.get("work_item_id") or [None])[0],
|
|
156
|
+
approval_state=(query.get("approval_state") or [None])[0],
|
|
157
|
+
)
|
|
158
|
+
]
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
return _bad_request(str(exc))
|
|
161
|
+
return _json_response(
|
|
162
|
+
{
|
|
163
|
+
"schema_version": SCHEMA_VERSION,
|
|
164
|
+
"local_first": True,
|
|
165
|
+
"proposals": proposals,
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if method == "POST" and path_parts == ["proposals"]:
|
|
170
|
+
work_item_id = str(payload.get("work_item_id") or "")
|
|
171
|
+
title = str(payload.get("title") or "")
|
|
172
|
+
summary = str(payload.get("summary") or "")
|
|
173
|
+
patch_plan = str(payload.get("patch_plan") or "")
|
|
174
|
+
if not work_item_id or not title or not summary or not patch_plan:
|
|
175
|
+
return _bad_request("proposal requires work_item_id, title, summary and patch_plan")
|
|
176
|
+
try:
|
|
177
|
+
proposal = add_proposal(
|
|
178
|
+
db_path=db_path,
|
|
179
|
+
work_item_id=work_item_id,
|
|
180
|
+
title=title,
|
|
181
|
+
summary=summary,
|
|
182
|
+
patch_plan=patch_plan,
|
|
183
|
+
risk_level=str(payload.get("risk_level") or "medium"),
|
|
184
|
+
)
|
|
185
|
+
return _json_response(proposal.to_dict(), 201)
|
|
186
|
+
except KeyError:
|
|
187
|
+
return _not_found(f"unknown work item: {work_item_id}")
|
|
188
|
+
|
|
189
|
+
if len(path_parts) == 2 and path_parts[0] == "proposals":
|
|
190
|
+
proposal_id = path_parts[1]
|
|
191
|
+
if method == "GET":
|
|
192
|
+
try:
|
|
193
|
+
return _json_response(
|
|
194
|
+
show_proposal(db_path=db_path, proposal_id=proposal_id).to_dict()
|
|
195
|
+
)
|
|
196
|
+
except KeyError:
|
|
197
|
+
return _not_found(f"unknown proposal: {proposal_id}")
|
|
198
|
+
|
|
199
|
+
if len(path_parts) == 3 and path_parts[0] == "proposals":
|
|
200
|
+
proposal_id = path_parts[1]
|
|
201
|
+
action = path_parts[2]
|
|
202
|
+
if method == "POST" and action == "approve":
|
|
203
|
+
try:
|
|
204
|
+
proposal = approve_proposal(
|
|
205
|
+
db_path=db_path,
|
|
206
|
+
proposal_id=proposal_id,
|
|
207
|
+
decision_note=_approval_note(payload),
|
|
208
|
+
)
|
|
209
|
+
return _json_response(proposal.to_dict())
|
|
210
|
+
except KeyError:
|
|
211
|
+
return _not_found(f"unknown proposal: {proposal_id}")
|
|
212
|
+
if method == "POST" and action == "reject":
|
|
213
|
+
try:
|
|
214
|
+
proposal = reject_proposal(
|
|
215
|
+
db_path=db_path,
|
|
216
|
+
proposal_id=proposal_id,
|
|
217
|
+
decision_note=_approval_note(payload),
|
|
218
|
+
)
|
|
219
|
+
return _json_response(proposal.to_dict())
|
|
220
|
+
except KeyError:
|
|
221
|
+
return _not_found(f"unknown proposal: {proposal_id}")
|
|
222
|
+
|
|
223
|
+
if method == "GET" and path_parts == ["audit-events"]:
|
|
224
|
+
events = export_audit_events(
|
|
225
|
+
db_path=db_path,
|
|
226
|
+
work_item_id=(query.get("work_item_id") or [None])[0],
|
|
227
|
+
)
|
|
228
|
+
return _json_response(events)
|
|
229
|
+
|
|
230
|
+
return _not_found(f"unknown route: {method} {parsed.path}")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def make_queue_api_handler(db_path: Path) -> type[BaseHTTPRequestHandler]:
|
|
234
|
+
class QueueAPIHandler(BaseHTTPRequestHandler):
|
|
235
|
+
server_version = "PatchRailQueueAPI/0.1"
|
|
236
|
+
|
|
237
|
+
def do_GET(self) -> None: # noqa: N802
|
|
238
|
+
self._handle()
|
|
239
|
+
|
|
240
|
+
def do_POST(self) -> None: # noqa: N802
|
|
241
|
+
self._handle()
|
|
242
|
+
|
|
243
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
def _handle(self) -> None:
|
|
247
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
248
|
+
body = self.rfile.read(length) if length else b""
|
|
249
|
+
status, payload = handle_queue_api_request(
|
|
250
|
+
method=self.command,
|
|
251
|
+
raw_path=self.path,
|
|
252
|
+
body=body,
|
|
253
|
+
db_path=db_path,
|
|
254
|
+
)
|
|
255
|
+
encoded = (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
|
256
|
+
self.send_response(status)
|
|
257
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
258
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
259
|
+
self.end_headers()
|
|
260
|
+
self.wfile.write(encoded)
|
|
261
|
+
|
|
262
|
+
return QueueAPIHandler
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def serve_queue_api(*, host: str = "127.0.0.1", port: int = 8765, db_path: Path) -> None:
|
|
266
|
+
if host not in {"127.0.0.1", "localhost"}:
|
|
267
|
+
raise ValueError("PatchRail queue API is local-only; use host 127.0.0.1 or localhost")
|
|
268
|
+
init_queue(db_path)
|
|
269
|
+
server = ThreadingHTTPServer((host, port), make_queue_api_handler(db_path))
|
|
270
|
+
try:
|
|
271
|
+
server.serve_forever()
|
|
272
|
+
finally:
|
|
273
|
+
server.server_close()
|