feat: add global work search (#195)
This commit is contained in:
1 parent
de20406a45
commit
7651e5b090
8 files changed
+359
-17
No files matched your search
@@ -183,6 +183,60 @@ async def work_page(stream: str, page: int = 1, limit: int = 50) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _normalize_global_search_item(item: Any, kind: str) -> dict | None:
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
repository = item.get("repository")
|
||||
repository = repository if isinstance(repository, dict) else {}
|
||||
url = _safe_web_url(item.get("html_url"))
|
||||
if not (
|
||||
isinstance(item.get("number"), int)
|
||||
and isinstance(item.get("title"), str)
|
||||
and isinstance(item.get("state"), str)
|
||||
and isinstance(repository.get("full_name"), str)
|
||||
and repository.get("full_name")
|
||||
and url
|
||||
):
|
||||
return None
|
||||
return {
|
||||
"kind": kind,
|
||||
"repository": repository["full_name"],
|
||||
"number": item["number"],
|
||||
"title": item["title"],
|
||||
"state": item["state"],
|
||||
"url": url,
|
||||
}
|
||||
|
||||
|
||||
async def global_search(query: str, limit: int = 10) -> list[dict]:
|
||||
"""Search accessible issues and pulls concurrently with a bounded result set."""
|
||||
async def load(item_type: str) -> Any:
|
||||
response = await _get_client().get(
|
||||
"/api/v1/repos/issues/search",
|
||||
headers=_auth(),
|
||||
params={"q": query, "type": item_type, "limit": limit, "page": 1},
|
||||
)
|
||||
response.raise_for_status()
|
||||
payload = response.json()
|
||||
if not isinstance(payload, list):
|
||||
raise ValueError("Gitea global search response was not a list")
|
||||
return payload
|
||||
|
||||
issues_payload, pulls_payload = await asyncio.gather(load("issues"), load("pulls"))
|
||||
results = []
|
||||
seen: set[tuple[str, str, int]] = set()
|
||||
for payload, kind in ((issues_payload, "issue"), (pulls_payload, "pull")):
|
||||
for item in payload:
|
||||
normalized = _normalize_global_search_item(item, kind)
|
||||
if normalized is not None:
|
||||
identity = (kind, normalized["repository"], normalized["number"])
|
||||
if identity in seen:
|
||||
continue
|
||||
seen.add(identity)
|
||||
results.append(normalized)
|
||||
return results
|
||||
|
||||
|
||||
def _normalize_available_issue(item: Any) -> dict | None:
|
||||
if (
|
||||
not isinstance(item, dict)
|
||||
|
||||
+24
-1
@@ -66,6 +66,7 @@ AUTHORED_ACTION_IDEMPOTENCY_MAX_ENTRIES = 256
|
||||
NOTIFICATION_MUTATION_TIMEOUT_SECONDS = 5.0
|
||||
NOTIFICATION_PAGE_TIMEOUT_SECONDS = 5.0
|
||||
WORK_PAGE_TIMEOUT_SECONDS = 5.0
|
||||
GLOBAL_SEARCH_TIMEOUT_SECONDS = 3.0
|
||||
NOTIFICATION_DETAIL_TIMEOUT_SECONDS = 5.0
|
||||
BULK_NOTIFICATION_CONCURRENCY = 5
|
||||
BULK_NOTIFICATION_DEADLINE_SECONDS = 6.0
|
||||
@@ -364,7 +365,7 @@ app.include_router(frontend_router)
|
||||
@app.middleware("http")
|
||||
async def prevent_live_api_caching(request, call_next):
|
||||
response = await call_next(request)
|
||||
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues"} or request.url.path.startswith("/api/v1/work/") or (
|
||||
if request.url.path in {"/api/v1/context", "/api/v1/events", "/api/v1/live", "/api/v1/available-issues", "/api/v1/search"} or request.url.path.startswith("/api/v1/work/") or (
|
||||
request.url.path.startswith("/api/v1/repos/")
|
||||
and request.url.path.endswith("/review")
|
||||
) or request.url.path.startswith("/api/v1/notifications") or (
|
||||
@@ -471,6 +472,28 @@ async def context() -> JSONResponse:
|
||||
return JSONResponse(_context_payload(user_data, repo_data, issues_data, prs_data))
|
||||
|
||||
|
||||
@app.get("/api/v1/search")
|
||||
async def global_search(
|
||||
q: str = Query(min_length=2, max_length=100),
|
||||
limit: int = Query(default=10, ge=1, le=25),
|
||||
) -> JSONResponse:
|
||||
query = q.strip()
|
||||
if len(query) < 2:
|
||||
raise HTTPException(status_code=422, detail="Search query must contain at least 2 characters")
|
||||
try:
|
||||
items = await asyncio.wait_for(
|
||||
gitea_proxy.global_search(query, limit),
|
||||
timeout=GLOBAL_SEARCH_TIMEOUT_SECONDS,
|
||||
)
|
||||
except Exception:
|
||||
return JSONResponse(
|
||||
{"error": "Search is temporarily unavailable. Please retry."},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "1"},
|
||||
)
|
||||
return JSONResponse({"query": query, "items": items})
|
||||
|
||||
|
||||
@app.get("/api/v1/work/{stream}")
|
||||
async def paged_work(
|
||||
stream: Literal["issue", "pull", "review"],
|
||||
|
||||
Reference in new issue
Block a user