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.
Files changed (47) hide show
  1. patchrail/__init__.py +7 -0
  2. patchrail/__main__.py +7 -0
  3. patchrail/ci/__init__.py +7 -0
  4. patchrail/ci/classify.py +888 -0
  5. patchrail/cli.py +8566 -0
  6. patchrail/funded_issues/__init__.py +138 -0
  7. patchrail/funded_issues/algora_board.py +240 -0
  8. patchrail/funded_issues/blocklist.py +112 -0
  9. patchrail/funded_issues/discovery.py +4091 -0
  10. patchrail/funded_issues/importers.py +316 -0
  11. patchrail/funded_issues/source_noise.py +349 -0
  12. patchrail/funded_issues/store.py +459 -0
  13. patchrail/queue/__init__.py +75 -0
  14. patchrail/queue/server.py +273 -0
  15. patchrail/queue/status.py +756 -0
  16. patchrail/queue/store.py +600 -0
  17. patchrail/reviewer_quick_check.py +650 -0
  18. patchrail/schemas/__init__.py +1 -0
  19. patchrail/schemas/application-dossier.v1.schema.json +305 -0
  20. patchrail/schemas/ci-benchmark.v1.schema.json +174 -0
  21. patchrail/schemas/ci-fixture-check.v1.schema.json +122 -0
  22. patchrail/schemas/ci-pilot-metrics.v1.schema.json +164 -0
  23. patchrail/schemas/ci-pilot-summary.v1.schema.json +146 -0
  24. patchrail/schemas/ci-result.v1.schema.json +133 -0
  25. patchrail/schemas/funded-issues-client-report.v1.schema.json +524 -0
  26. patchrail/schemas/funded-issues-recheck-queue.v1.schema.json +333 -0
  27. patchrail/schemas/funded-issues-recheck-summary.v1.schema.json +136 -0
  28. patchrail/schemas/funded-issues-report.v1.schema.json +836 -0
  29. patchrail/schemas/funded-issues-shortlist.v1.schema.json +953 -0
  30. patchrail/schemas/funded-issues-store-status.v1.schema.json +96 -0
  31. patchrail/schemas/funded-issues-store.v1.schema.json +117 -0
  32. patchrail/schemas/queue-audit-event.v1.schema.json +44 -0
  33. patchrail/schemas/queue-audit-summary.v1.schema.json +169 -0
  34. patchrail/schemas/queue-gate-report.v1.schema.json +158 -0
  35. patchrail/schemas/queue-policy-resolution.v1.schema.json +188 -0
  36. patchrail/schemas/queue-policy-scan.v1.schema.json +175 -0
  37. patchrail/schemas/queue-proposal.v1.schema.json +61 -0
  38. patchrail/schemas/queue-review.v1.schema.json +218 -0
  39. patchrail/schemas/queue-status.v1.schema.json +179 -0
  40. patchrail/schemas/queue-work-item.v1.schema.json +64 -0
  41. patchrail/schemas/reviewer-quick-check-artifacts.v1.schema.json +104 -0
  42. patchrail/web_metrics.py +649 -0
  43. patchrail-0.1.0.dist-info/METADATA +279 -0
  44. patchrail-0.1.0.dist-info/RECORD +47 -0
  45. patchrail-0.1.0.dist-info/WHEEL +4 -0
  46. patchrail-0.1.0.dist-info/entry_points.txt +2 -0
  47. 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()