feat: complete assigned issues from mobile My Work (#157)
This commit is contained in:
1 parent
30a014b619
commit
1c0cacb2b8
7 files changed
+809
-6
No files matched your search
@@ -318,6 +318,99 @@ async def reply_to_notification(thread_id: int, body: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def close_issue(repository: str, number: int) -> dict:
|
||||
response = await _get_client().patch(
|
||||
f"/api/v1/repos/{repository}/issues/{number}",
|
||||
headers=_auth(),
|
||||
json={"state": "closed"},
|
||||
)
|
||||
response.raise_for_status()
|
||||
issue = response.json()
|
||||
if not isinstance(issue, dict) or issue.get("state") != "closed":
|
||||
raise ValueError("Gitea did not confirm issue closure")
|
||||
return {
|
||||
"number": issue.get("number"),
|
||||
"state": "closed",
|
||||
"closed_at": issue.get("closed_at", "")
|
||||
if isinstance(issue.get("closed_at"), str)
|
||||
else "",
|
||||
}
|
||||
|
||||
|
||||
def _normalize_issue_comment(comment: dict) -> dict:
|
||||
user_value = comment.get("user")
|
||||
user: dict = user_value if isinstance(user_value, dict) else {}
|
||||
return {
|
||||
"id": comment.get("id"),
|
||||
"author": user.get("login", "") if isinstance(user.get("login"), str) else "",
|
||||
"body": comment.get("body", "") if isinstance(comment.get("body"), str) else "",
|
||||
"created_at": comment.get("created_at", "")
|
||||
if isinstance(comment.get("created_at"), str)
|
||||
else "",
|
||||
"url": _safe_web_url(comment.get("html_url")),
|
||||
}
|
||||
|
||||
|
||||
async def comment_on_issue(repository: str, number: int, body: str) -> dict:
|
||||
response = await _get_client().post(
|
||||
f"/api/v1/repos/{repository}/issues/{number}/comments",
|
||||
headers=_auth(),
|
||||
json={"body": body},
|
||||
)
|
||||
response.raise_for_status()
|
||||
comment = response.json()
|
||||
if not isinstance(comment, dict):
|
||||
raise ValueError("Gitea comment response was not an object")
|
||||
return _normalize_issue_comment(comment)
|
||||
|
||||
|
||||
async def issue_detail(repository: str, number: int) -> dict:
|
||||
base = f"repos/{repository}/issues/{number}"
|
||||
issue, comments = await asyncio.gather(
|
||||
fetch(base), fetch(f"{base}/comments?limit=20&page=1")
|
||||
)
|
||||
if not isinstance(issue, dict):
|
||||
raise ValueError("Gitea issue response was not an object")
|
||||
labels_value = issue.get("labels")
|
||||
labels: list = labels_value if isinstance(labels_value, list) else []
|
||||
assignees_value = issue.get("assignees")
|
||||
assignees: list = assignees_value if isinstance(assignees_value, list) else []
|
||||
comments_value: list = comments if isinstance(comments, list) else []
|
||||
normalized_comments = [
|
||||
_normalize_issue_comment(comment)
|
||||
for comment in comments_value
|
||||
if isinstance(comment, dict)
|
||||
]
|
||||
return {
|
||||
"repository": repository,
|
||||
"number": number,
|
||||
"title": issue.get("title", "") if isinstance(issue.get("title"), str) else "",
|
||||
"state": issue.get("state", "") if isinstance(issue.get("state"), str) else "",
|
||||
"body": issue.get("body", "") if isinstance(issue.get("body"), str) else "",
|
||||
"url": _safe_web_url(issue.get("html_url")),
|
||||
"labels": [
|
||||
label["name"] for label in labels
|
||||
if isinstance(label, dict) and isinstance(label.get("name"), str)
|
||||
],
|
||||
"assignees": [
|
||||
assignee["login"] for assignee in assignees
|
||||
if isinstance(assignee, dict) and isinstance(assignee.get("login"), str)
|
||||
],
|
||||
"comments": normalized_comments,
|
||||
}
|
||||
|
||||
|
||||
async def is_assigned_issue(repository: str, number: int) -> bool:
|
||||
assigned = await issues()
|
||||
return any(
|
||||
isinstance(issue, dict)
|
||||
and issue.get("number") == number
|
||||
and isinstance(issue.get("repository"), dict)
|
||||
and issue["repository"].get("full_name") == repository
|
||||
for issue in (assigned or [])
|
||||
)
|
||||
|
||||
|
||||
async def pull_requests() -> list[dict]:
|
||||
assigned, review_requested = await asyncio.gather(
|
||||
fetch("repos/issues/search?state=open&assigned=true&type=pulls&limit=50"),
|
||||
|
||||
+91
-1
@@ -52,6 +52,7 @@ CONTEXT_TIMEOUT_SECONDS = 5.0
|
||||
EVENT_STREAM_TIMEOUT_SECONDS = 5.0
|
||||
READINESS_TIMEOUT_SECONDS = 5.0
|
||||
REVIEW_DETAIL_TIMEOUT_SECONDS = 5.0
|
||||
ISSUE_ACTION_TIMEOUT_SECONDS = 5.0
|
||||
NOTIFICATION_MUTATION_TIMEOUT_SECONDS = 5.0
|
||||
NOTIFICATION_PAGE_TIMEOUT_SECONDS = 5.0
|
||||
NOTIFICATION_DETAIL_TIMEOUT_SECONDS = 5.0
|
||||
@@ -93,6 +94,18 @@ class NotificationReply(BaseModel):
|
||||
return value
|
||||
|
||||
|
||||
class IssueComment(BaseModel):
|
||||
body: str = Field(min_length=1, max_length=10_000)
|
||||
|
||||
@field_validator("body")
|
||||
@classmethod
|
||||
def strip_body(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("comment must not be blank")
|
||||
return value
|
||||
|
||||
|
||||
class PullReviewSubmission(BaseModel):
|
||||
decision: str
|
||||
body: str = Field(max_length=10_000)
|
||||
@@ -161,7 +174,10 @@ async def prevent_live_api_caching(request, call_next):
|
||||
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live"} or (
|
||||
request.url.path.startswith("/api/v1/repos/")
|
||||
and request.url.path.endswith("/review")
|
||||
) or request.url.path.startswith("/api/v1/notifications"):
|
||||
) or request.url.path.startswith("/api/v1/notifications") or (
|
||||
request.url.path.startswith("/api/v1/repos/")
|
||||
and "/issues/" in request.url.path
|
||||
):
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
@@ -664,6 +680,80 @@ async def reply_to_notification(
|
||||
return JSONResponse(result, status_code=201)
|
||||
|
||||
|
||||
@app.get("/api/v1/repos/{owner}/{repo}/issues/{number}/detail")
|
||||
async def assigned_issue_detail(owner: str, repo: str, number: int = PathParam(gt=0)):
|
||||
repository = f"{owner}/{repo}"
|
||||
|
||||
async def load_assigned_issue():
|
||||
if not await gitea_proxy.is_assigned_issue(repository, number):
|
||||
raise HTTPException(status_code=404, detail="Assigned issue not found")
|
||||
return await gitea_proxy.issue_detail(repository, number)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
load_assigned_issue(), timeout=ISSUE_ACTION_TIMEOUT_SECONDS
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except TimeoutError:
|
||||
return JSONResponse(
|
||||
{"error": "Loading the issue timed out. Please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "The issue is temporarily unavailable. Please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
|
||||
|
||||
@app.post("/api/v1/repos/{owner}/{repo}/issues/{number}/comments", status_code=201)
|
||||
async def comment_on_assigned_issue(
|
||||
comment: IssueComment, owner: str, repo: str, number: int = PathParam(gt=0)
|
||||
):
|
||||
repository = f"{owner}/{repo}"
|
||||
|
||||
async def post_comment():
|
||||
if not await gitea_proxy.is_assigned_issue(repository, number):
|
||||
raise HTTPException(status_code=404, detail="Assigned issue not found")
|
||||
return await gitea_proxy.comment_on_issue(repository, number, comment.body)
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(post_comment(), timeout=ISSUE_ACTION_TIMEOUT_SECONDS)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "The comment could not be posted. Your draft is safe; please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
return JSONResponse(result, status_code=201)
|
||||
|
||||
|
||||
@app.patch("/api/v1/repos/{owner}/{repo}/issues/{number}/close")
|
||||
async def close_assigned_issue(owner: str, repo: str, number: int = PathParam(gt=0)):
|
||||
repository = f"{owner}/{repo}"
|
||||
|
||||
async def close_issue():
|
||||
if not await gitea_proxy.is_assigned_issue(repository, number):
|
||||
raise HTTPException(status_code=404, detail="Assigned issue not found")
|
||||
return await gitea_proxy.close_issue(repository, number)
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(close_issue(), timeout=ISSUE_ACTION_TIMEOUT_SECONDS)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "The issue could not be closed. It remains in My Work; please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/v1/repos/{owner}/{repo}/pulls/{number}/review")
|
||||
async def review_detail(owner: str, repo: str, number: int):
|
||||
async def load_requested_review():
|
||||
|
||||
Reference in new issue
Block a user