| 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" |
|
|
| |
| |
| 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, |
| } |