from datetime import datetime, timezone
from html import unescape
import re
from typing import Any
import requests
from config import STACKEXCHANGE_API
def _unix_to_iso(timestamp: int | None) -> str | None:
if timestamp is None:
return None
try:
return datetime.fromtimestamp(
timestamp,
tz=timezone.utc,
).isoformat()
except (TypeError, ValueError, OSError):
return None
def _normalize_text(text: str) -> str:
"""
Karşılaştırma için metni sadeleştirir.
"""
return re.sub(
r"[^a-z0-9\s]",
" ",
text.lower(),
)
def _calculate_relevance_score(
query: str,
title: str,
tags: list[str],
) -> int:
"""
Sorgu kelimeleriyle başlık ve etiketler arasındaki
basit eşleşme puanını hesaplar.
"""
ignored_words = {
"the",
"a",
"an",
"is",
"in",
"of",
"to",
"and",
"or",
"python",
"typeerror",
"error",
}
normalized_query = _normalize_text(query)
normalized_title = _normalize_text(title)
query_words = {
word
for word in normalized_query.split()
if len(word) > 2 and word not in ignored_words
}
title_words = set(normalized_title.split())
normalized_tags = {
tag.lower()
for tag in tags
}
title_matches = len(
query_words.intersection(title_words)
)
tag_matches = len(
query_words.intersection(normalized_tags)
)
exact_phrase_bonus = (
5
if normalized_query in normalized_title
else 0
)
return (
title_matches * 3
+ tag_matches * 2
+ exact_phrase_bonus
)
def search_stackoverflow(
query: str,
limit: int = 5,
) -> dict[str, Any]:
"""
Stack Overflow üzerinde verilen sorguyla ilgili
soruları arar.
"""
query = query.strip()
if not query:
raise ValueError(
"Stack Overflow arama sorgusu boş olamaz."
)
limit = max(1, min(limit, 10))
url = f"{STACKEXCHANGE_API}/search/advanced"
# Nihai limitten daha fazla sonuç alıp
# kendi relevance sıralamamızı uyguluyoruz.
candidate_limit = min(limit * 4, 40)
params = {
"site": "stackoverflow",
"q": query,
"sort": "relevance",
"order": "desc",
"pagesize": candidate_limit,
"filter": "withbody",
}
try:
response = requests.get(
url,
params=params,
timeout=20,
)
response.raise_for_status()
except requests.Timeout:
return {
"success": False,
"query": query,
"error": (
"Stack Overflow API isteği zaman aşımına uğradı."
),
}
except requests.RequestException as exc:
return {
"success": False,
"query": query,
"error": (
"Stack Overflow API isteği başarısız oldu: "
f"{exc}"
),
}
try:
data = response.json()
except ValueError:
return {
"success": False,
"query": query,
"error": (
"Stack Overflow API geçerli bir JSON "
"yanıtı döndürmedi."
),
}
if "error_message" in data:
return {
"success": False,
"query": query,
"error_id": data.get("error_id"),
"error_name": data.get("error_name"),
"error": data.get("error_message"),
}
questions = []
for item in data.get("items", []):
owner_data = item.get("owner") or {}
tags = item.get("tags", [])
title = unescape(item.get("title", ""))
relevance_score = _calculate_relevance_score(
query=query,
title=title,
tags=tags,
)
questions.append(
{
"question_id": item.get("question_id"),
"title": title,
"link": item.get("link"),
"score": item.get("score", 0),
"relevance_score": relevance_score,
"answer_count": item.get("answer_count", 0),
"is_answered": item.get("is_answered", False),
"accepted_answer_id": item.get(
"accepted_answer_id"
),
"view_count": item.get("view_count", 0),
"tags": tags,
"owner": owner_data.get("display_name"),
"body": item.get("body"),
"creation_date": _unix_to_iso(
item.get("creation_date")
),
"last_activity_date": _unix_to_iso(
item.get("last_activity_date")
),
}
)
questions.sort(
key=lambda question: (
question["relevance_score"],
question["score"],
),
reverse=True,
)
selected_questions = questions[:limit]
return {
"success": True,
"query": query,
"returned_count": len(selected_questions),
"candidate_count": len(questions),
"has_more": data.get("has_more", False),
"quota_remaining": data.get("quota_remaining"),
"quota_max": data.get("quota_max"),
"backoff": data.get("backoff"),
"questions": selected_questions,
}